Skip to content

Commit db5d1ec

Browse files
authored
Fix for #9 (register token for registry) (#16)
* Add support for basic token authentication, such as with an AWS ECR repository.
1 parent 40c2d7a commit db5d1ec

File tree

6 files changed

+83
-2
lines changed

6 files changed

+83
-2
lines changed

.gitignore

Whitespace-only changes.

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ optional arguments:
4848
as regular expression
4949
-i REGISTRY, --insecure-registry REGISTRY
5050
domain of insecure registry
51+
-k REGISTRY:TOKEN, --registry-token REGISTRY:TOKEN
52+
uses the token for login to the given Docker registry
5153
-L LOG_FILE, --log-file LOG_FILE
5254
save log to file
5355
-d, --debug print more logs
@@ -89,6 +91,9 @@ Examples:
8991
# example.reg.com/myimage1:latest only
9092
claircli -r example.reg.com/^myimage1$:^latest$
9193
94+
# analyze an image stored in an Amazon ECR repository
95+
# This uses the registry token generated by the aws cli tool
96+
claircli -k 123456789012.dkr.ecr.us-east-1.amazonaws.com:$( aws ecr get-authorization-token --output text --query 'authorizationData[].authorizationToken' ) 123456789012.dkr.ecr.us-east-1.amazonaws.com/myimage:latest
9297
```
9398

9499
## Optional whitelist yaml file

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.1'
2+
__version__ = '1.2'
33
__title__ = 'claircli'
44
__description__ = 'Command line tool to interact with Clair'
55
__url__ = 'https://github.com/joelee2012/claircli'

claircli/cli.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ class ClairCli(object):
5959
# analyze with regular expression, following will match
6060
# example.reg.com/myimage1:latest only
6161
claircli -r example.reg.com/^myimage1$:^latest$
62+
63+
# analyze an image stored in an Amazon ECR repository
64+
# This uses the registry token generated by the aws cli tool,
65+
# such as by running "aws ecr get-authorization-token
66+
# --output text --query 'authorizationData[].authorizationToken'"
67+
claircli \
68+
-k 123456789012.dkr.ecr.us-east-1.amazonaws.com:$MYTOKEN \
69+
123456789012.dkr.ecr.us-east-1.amazonaws.com/myimage:latest
6270
'''
6371

6472
def __init__(self):
@@ -94,6 +102,10 @@ def __init__(self):
94102
'-i', '--insecure-registry', action='append',
95103
dest='insec_regs', metavar='REGISTRY', default=[],
96104
help='domain of insecure registry')
105+
parser.add_argument(
106+
'-k', '--registry-token', action='append',
107+
dest='domain_tokens', default=[],
108+
help='docker registry token; in the form "domain:token"')
97109
parser.add_argument('-L', '--log-file', help='save log to file')
98110
parser.add_argument(
99111
'-d', '--debug', action='store_true', help='print more logs')
@@ -143,6 +155,20 @@ def analyze_image(self):
143155
registry = LocalRegistry(args.local_ip)
144156
elif args.regex:
145157
args.images = self.resolve_images(args.images)
158+
for domain_token in args.domain_tokens:
159+
domain_token_split = domain_token.split(':', 1)
160+
if (
161+
len(domain_token_split) != 2 or
162+
not domain_token_split[0] or
163+
not domain_token_split[1]
164+
):
165+
logger.warning(
166+
'registry token value must be in the form "domain:token"' +
167+
'; found "%s"', domain_token)
168+
else:
169+
RemoteRegistry.tokens[domain_token_split[0]] = {
170+
'': 'Basic ' + domain_token_split[1]
171+
}
146172

147173
clair = Clair(args.clair)
148174
if args.white_list:
@@ -186,6 +212,7 @@ def analyze_image(self):
186212
except Exception as exp:
187213
stats['IMAGES WERE ANALYZED WITH ERROR'].append(image.name)
188214
logger.warning(str(exp))
215+
logger.debug('Underlying problem:', exc_info=exp)
189216
finally:
190217
image.clean()
191218
return self.print_stats(stats)

claircli/docker_registry.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,13 @@ def __str__(self):
8282
return self.domain
8383

8484
def get_auth(self, repository):
85-
if not self.tokens[self.domain].get(repository):
85+
if (
86+
not self.tokens[self.domain].get(repository) and
87+
self.tokens[self.domain].get('')
88+
):
89+
self.tokens[self.domain][repository] = \
90+
self.tokens[self.domain].get('')
91+
elif not self.tokens[self.domain].get(repository):
8692
resp = request('GET', self.url)
8793
if resp.status_code not in (200, 401):
8894
resp.raise_for_status()

tests/test_claircli.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,49 @@ def test_analyze_images_in_insecure_registry(self):
252252
self.assertTrue(isfile(self.html))
253253
self.assertIn(self.reg, RemoteRegistry.insec_regs)
254254

255+
@responses.activate
256+
def test_analyze_images_in_secure_registry(self):
257+
258+
reg_url = 'https://%s/v2/' % self.reg
259+
token = 'just-some-auth-token-which-is-really-long'
260+
auth = 'Basic %s' % token
261+
headers = {'WWW-Authenticate': auth}
262+
manifest_url = reg_url + 'org/image-name/manifests/version'
263+
responses.reset()
264+
responses.add(responses.GET, manifest_url,
265+
json=self.manifest, status=200, headers=headers)
266+
self.layers = [e['digest'] for e in self.manifest['layers']]
267+
responses.add(responses.DELETE, '%s/%s' %
268+
(self.v1_analyze_url, self.layers[0]))
269+
responses.add(responses.POST, self.v1_analyze_url)
270+
responses.add(responses.GET, '%s/%s?features&vulnerabilities' %
271+
(self.v1_analyze_url, self.layers[-1]),
272+
json=self.origin_data)
273+
274+
with patch('sys.argv', ['claircli', '-c',
275+
self.clair_url,
276+
'-k', self.reg + ':' + token,
277+
# Include a check for ignored arguments
278+
'-k', '1234', '-k', 'ab:', '-k', ':',
279+
self.name]):
280+
cli = ClairCli()
281+
cli.run()
282+
for index, url in enumerate([manifest_url, ]):
283+
self.assertEqual(responses.calls[index].request.url, url)
284+
285+
for index, layer in enumerate(self.layers, start=2):
286+
self.assertEqual(
287+
responses.calls[index].request.url, self.v1_analyze_url)
288+
req_body = json.loads(responses.calls[index].request.body)
289+
self.assertEqual(req_body['Layer']['Name'], layer)
290+
self.assertTrue(isfile(self.html))
291+
self.assertEqual(0, len(RemoteRegistry.insec_regs))
292+
self.assertIn(self.reg, RemoteRegistry.tokens)
293+
self.assertIn('', RemoteRegistry.tokens[self.reg])
294+
self.assertEqual(auth, RemoteRegistry.tokens[self.reg][''])
295+
self.assertIn(self.repo, RemoteRegistry.tokens[self.reg])
296+
self.assertEqual(auth, RemoteRegistry.tokens[self.reg][self.repo])
297+
255298
@patch('docker.from_env')
256299
@responses.activate
257300
def test_analyze_local_images(self, mock_docker):

0 commit comments

Comments
 (0)