Skip to content

Commit 40c2d7a

Browse files
authored
Insecure registry (#13)
* support insecure registry
1 parent 8411580 commit 40c2d7a

File tree

5 files changed

+106
-35
lines changed

5 files changed

+106
-35
lines changed

README.md

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
[![Build Status](https://travis-ci.com/joelee2012/claircli.svg?branch=master)](https://travis-ci.com/joelee2012/claircli)
22
[![Coverage Status](https://coveralls.io/repos/github/joelee2012/claircli/badge.svg?branch=master)](https://coveralls.io/github/joelee2012/claircli?branch=master)
33
# claircli
4-
## claircli is a command line tool to interact with [CoreOS Clair](https://github.com/quay/clair)
5-
- analyze loacl/remote docker image with [Clair](https://github.com/quay/clair)
6-
- generate HTML/JSON report, the html report template is from [analysis-template.html](https://github.com/jgsqware/clairctl/blob/master/clair/templates/analysis-template.html)
4+
## claircli is a command line tool to interact with [Quay Clair](https://github.com/quay/clair), which has following functionalities:
5+
- analyze docker images in local host
6+
- analyze docker images in remote host
7+
- analyze docker images in secure/insecure registry
8+
- support threshold/whitelist for vulnerabilities
9+
- report to HTML/JSON, the html report is based on [template](https://github.com/jgsqware/clairctl/blob/master/clair/templates/analysis-template.html)
710

811
# Installation
912

@@ -15,45 +18,51 @@ pip install claircli
1518

1619
```
1720
claircli -h
18-
usage: claircli [-h] [-V] [-c CLAIR] [-w WHITE_LIST] [-T THRESHOLD]
19-
[-f {html,json}] [-L LOG_FILE] [-d] [-l LOCAL_IP | -r]
20-
images [images ...]
21+
usage: claircli [-h] [-c CLAIR] [-f {html,json}] [-T THRESHOLD]
22+
[-w WHITE_LIST] [-l LOCAL_IP | -r] [-i REGISTRY] [-L LOG_FILE]
23+
[-d] [-V]
24+
IMAGE [IMAGE ...]
2125
22-
Command line tool to interact with CoreOS Clair, analyze docker image with
23-
clair in different ways
26+
Command line tool to interact with Quay Clair to analyze docker image in different ways
2427
2528
positional arguments:
26-
images docker images or regular expression
29+
IMAGE docker images or regular expression
2730
2831
optional arguments:
2932
-h, --help show this help message and exit
30-
-V, --version show program's version number and exit
3133
-c CLAIR, --clair CLAIR
3234
clair url, default: http://localhost:6060
33-
-w WHITE_LIST, --white-list WHITE_LIST
34-
path to the whitelist file
35+
-f {html,json}, --formats {html,json}
36+
output report file with give format, default: ['html']
3537
-T THRESHOLD, --threshold THRESHOLD
3638
cvd severity threshold, if any servity of
3739
vulnerability above of threshold, will return non-
3840
zero, default: Unknown, choices are: ['Defcon1',
3941
'Critical', 'High', 'Medium', 'Low', 'Negligible',
4042
'Unknown']
41-
-f {html,json}, --formats {html,json}
42-
output report file with give format, default: ['html']
43-
-L LOG_FILE, --log-file LOG_FILE
44-
save log to file
45-
-d, --debug print more logs
43+
-w WHITE_LIST, --white-list WHITE_LIST
44+
path to the whitelist file
4645
-l LOCAL_IP, --local-ip LOCAL_IP
4746
ip address of local host
4847
-r, --regex if set, repository and tag of images will be treated
4948
as regular expression
49+
-i REGISTRY, --insecure-registry REGISTRY
50+
domain of insecure registry
51+
-L LOG_FILE, --log-file LOG_FILE
52+
save log to file
53+
-d, --debug print more logs
54+
-V, --version show program's version number and exit
5055
5156
Examples:
5257
5358
# analyze and output report to html
5459
# clair is running at http://localhost:6060
5560
claircli example.reg.com/myimage1:latest example.reg.com/myimage2:latest
5661
62+
# analyze image in insecure registry
63+
# clair is running at http://localhost:6060
64+
claircli -i example.reg.com example.reg.com/myimage1:latest
65+
5766
# analyze and output report to html
5867
# clair is running at https://example.clair.com:6060
5968
claircli -c https://example.clair.com:6060 example.reg.com/myimage1:latest

claircli/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -*- coding: utf-8 -*-
2-
__version__ = '1.0'
2+
__version__ = '1.1'
33
__title__ = 'claircli'
44
__description__ = 'Command line tool to interact with Clair'
55
__url__ = 'https://github.com/joelee2012/claircli'

claircli/cli.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,18 @@
2222

2323

2424
class ClairCli(object):
25-
description = textwrap.dedent('''
26-
Command line tool to interact with CoreOS Clair, analyze docker image with
27-
clair in different ways''')
25+
description = 'Command line tool to interact with Quay Clair' \
26+
' to analyze docker image in different ways'
2827
epilog = '''Examples:
2928
3029
# analyze and output report to html
3130
# clair is running at http://localhost:6060
3231
claircli example.reg.com/myimage1:latest example.reg.com/myimage2:latest
3332
33+
# analyze image in insecure registry
34+
# clair is running at http://localhost:6060
35+
claircli -i example.reg.com example.reg.com/myimage1:latest
36+
3437
# analyze and output report to html
3538
# clair is running at https://example.clair.com:6060
3639
claircli -c https://example.clair.com:6060 example.reg.com/myimage1:latest
@@ -64,26 +67,22 @@ def __init__(self):
6467
description=self.description,
6568
formatter_class=argparse.RawDescriptionHelpFormatter,
6669
epilog=self.epilog)
67-
parser.add_argument(
68-
'-V', '--version', action='version', version=__version__)
70+
6971
parser.add_argument(
7072
'-c', '--clair', default='http://localhost:6060',
7173
help='clair url, default: %(default)s')
7274
parser.add_argument(
73-
'-w', '--white-list', help='path to the whitelist file')
75+
'-f', '--formats', choices=['html', 'json'],
76+
action='append', default=['html'],
77+
help='output report file with give format, default: %(default)s')
7478
parser.add_argument(
7579
'-T', '--threshold', choices=SEVERITIES,
7680
default='Unknown', metavar='THRESHOLD',
7781
help='cvd severity threshold, if any servity of vulnerability'
7882
' above of threshold, will return non-zero, default: %(default)s'
7983
', choices are: {}'.format(SEVERITIES))
8084
parser.add_argument(
81-
'-f', '--formats', choices=['html', 'json'],
82-
action='append', default=['html'],
83-
help='output report file with give format, default: %(default)s')
84-
parser.add_argument('-L', '--log-file', help='save log to file')
85-
parser.add_argument(
86-
'-d', '--debug', action='store_true', help='print more logs')
85+
'-w', '--white-list', help='path to the whitelist file')
8786
group = parser.add_mutually_exclusive_group()
8887
group.add_argument(
8988
'-l', '--local-ip', help='ip address of local host')
@@ -92,9 +91,22 @@ def __init__(self):
9291
help='if set, repository and tag of images will be '
9392
'treated as regular expression')
9493
parser.add_argument(
95-
'images', nargs='+', help='docker images or regular expression')
94+
'-i', '--insecure-registry', action='append',
95+
dest='insec_regs', metavar='REGISTRY', default=[],
96+
help='domain of insecure registry')
97+
parser.add_argument('-L', '--log-file', help='save log to file')
98+
parser.add_argument(
99+
'-d', '--debug', action='store_true', help='print more logs')
100+
parser.add_argument(
101+
'-V', '--version', action='version', version=__version__)
102+
parser.add_argument(
103+
'images', nargs='+', metavar='IMAGE',
104+
help='docker images or regular expression')
96105
parser.set_defaults(func=self.analyze_image)
97106
self.args = parser.parse_args()
107+
if self.args.local_ip and self.args.insec_regs:
108+
parser.error('argument --local-ip: not allowed with'
109+
' argument --insecure-registry')
98110
self.setup_logging()
99111

100112
def setup_logging(self):
@@ -126,6 +138,7 @@ def resolve_images(self, images):
126138
def analyze_image(self):
127139
args = self.args
128140
registry = None
141+
RemoteRegistry.insec_regs = set(args.insec_regs)
129142
if args.local_ip:
130143
registry = LocalRegistry(args.local_ip)
131144
elif args.regex:

claircli/docker_registry.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,12 @@ class RemoteRegistry(object):
7171
tokens = defaultdict(dict)
7272
token_pattern = re.compile(r'Bearer realm="(?P<realm>[^"]+)".*'
7373
r'service="(?P<service>[^"]+).*')
74+
insec_regs = set()
7475

7576
def __init__(self, domain):
7677
self.domain = domain
77-
self.url = 'https://{}/v2/'.format(self.domain)
78+
schema = 'http' if domain in self.insec_regs else 'https'
79+
self.url = '{}://{}/v2/'.format(schema, domain)
7880

7981
def __str__(self):
8082
return self.domain

tests/test_claircli.py

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
from claircli.docker_image import Image
1717
from claircli.docker_registry import LocalRegistry, RemoteRegistry
1818
from claircli.report import Report, WhiteList
19-
from mock import patch
19+
try:
20+
from unittest.mock import patch
21+
except:
22+
from mock import patch
2023

2124
logger = logging.getLogger(__name__)
2225

@@ -193,7 +196,7 @@ def test_read_white_list(self):
193196

194197
@responses.activate
195198
def test_analyze_images(self):
196-
with patch('sys.argv', ['claircli.py', '-c',
199+
with patch('sys.argv', ['claircli', '-c',
197200
self.clair_url, self.name]):
198201
cli = ClairCli()
199202
cli.run()
@@ -205,6 +208,50 @@ def test_analyze_images(self):
205208
self.assertEqual(req_body['Layer']['Name'], layer)
206209
self.assertTrue(isfile(self.html))
207210

211+
@responses.activate
212+
def test_analyze_images_in_insecure_registry(self):
213+
214+
reg_url = 'http://%s/v2/' % self.reg
215+
token_url = reg_url + 'token'
216+
auth = 'Bearer realm="%s",service="%s"' % (token_url, self.reg)
217+
headers = {'WWW-Authenticate': auth}
218+
params = {'service': self.reg,
219+
'client_id': 'claircli',
220+
'scope': 'repository:%s:pull' % self.repo}
221+
token_url = token_url + '?' + urlencode(params)
222+
manifest_url = reg_url + 'org/image-name/manifests/version'
223+
responses.reset()
224+
responses.add(responses.GET, reg_url,
225+
json={'message': 'authentication required'},
226+
status=401, headers=headers)
227+
responses.add(responses.GET, token_url,
228+
json={'token': 'test-token'}, status=200)
229+
230+
responses.add(responses.GET, manifest_url,
231+
json=self.manifest, status=200)
232+
self.layers = [e['digest'] for e in self.manifest['layers']]
233+
responses.add(responses.DELETE, '%s/%s' %
234+
(self.v1_analyze_url, self.layers[0]))
235+
responses.add(responses.POST, self.v1_analyze_url)
236+
responses.add(responses.GET, '%s/%s?features&vulnerabilities' %
237+
(self.v1_analyze_url, self.layers[-1]),
238+
json=self.origin_data)
239+
240+
with patch('sys.argv', ['claircli', '-c',
241+
self.clair_url, '-i', self.reg, self.name]):
242+
cli = ClairCli()
243+
cli.run()
244+
for index, url in enumerate([reg_url, token_url, manifest_url]):
245+
self.assertEqual(responses.calls[index].request.url, url)
246+
247+
for index, layer in enumerate(self.layers, start=4):
248+
self.assertEqual(
249+
responses.calls[index].request.url, self.v1_analyze_url)
250+
req_body = json.loads(responses.calls[index].request.body)
251+
self.assertEqual(req_body['Layer']['Name'], layer)
252+
self.assertTrue(isfile(self.html))
253+
self.assertIn(self.reg, RemoteRegistry.insec_regs)
254+
208255
@patch('docker.from_env')
209256
@responses.activate
210257
def test_analyze_local_images(self, mock_docker):
@@ -216,7 +263,7 @@ def test_analyze_local_images(self, mock_docker):
216263
(self.v1_analyze_url, layers[0]))
217264
responses.add(responses.GET, '%s/%s?features&vulnerabilities' %
218265
(self.v1_analyze_url, layers[-1]), json=self.origin_data)
219-
with patch('sys.argv', ['claircli.py', '-l', 'localhost',
266+
with patch('sys.argv', ['claircli', '-l', 'localhost',
220267
'-c', self.clair_url, self.name]):
221268
cli = ClairCli()
222269
cli.run()

0 commit comments

Comments
 (0)