Use the registry API for security scanning
when the storage engine doesn't support direct download url
This commit is contained in:
parent
1ef7008d85
commit
9221a515de
9 changed files with 149 additions and 106 deletions
|
@ -1,15 +1,23 @@
|
|||
import logging
|
||||
import requests
|
||||
|
||||
from flask import url_for
|
||||
from urlparse import urljoin
|
||||
|
||||
from data.database import CloseForLongOperation
|
||||
from data import model
|
||||
from data.model.storage import get_storage_locations
|
||||
|
||||
from urlparse import urljoin
|
||||
from util.secscan.validator import SecurityConfigValidator
|
||||
from util.security.registry_jwt import generate_jwt_object, build_context_and_subject
|
||||
from util import get_app_url
|
||||
|
||||
|
||||
TOKEN_VALIDITY_LIFETIME_S = 60 # Amount of time the security scanner has to call the layer URL
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AnalyzeLayerException(Exception):
|
||||
""" Exception raised when a layer fails to analyze due to a *client-side* issue. """
|
||||
|
||||
|
@ -26,13 +34,14 @@ _API_METHOD_PING = 'metrics'
|
|||
|
||||
class SecurityScannerAPI(object):
|
||||
""" Helper class for talking to the Security Scan service (Clair). """
|
||||
def __init__(self, config, storage, client=None, skip_validation=False):
|
||||
def __init__(self, app, config, storage, client=None, skip_validation=False):
|
||||
if not skip_validation:
|
||||
config_validator = SecurityConfigValidator(config)
|
||||
if not config_validator.valid():
|
||||
logger.warning('Invalid config provided to SecurityScannerAPI')
|
||||
return
|
||||
|
||||
self._app = app
|
||||
self._config = config
|
||||
self._client = client or config['HTTPCLIENT']
|
||||
self._storage = storage
|
||||
|
@ -40,9 +49,10 @@ class SecurityScannerAPI(object):
|
|||
self._target_version = config.get('SECURITY_SCANNER_ENGINE_VERSION_TARGET', 2)
|
||||
|
||||
|
||||
def _get_image_url(self, image):
|
||||
""" Gets the download URL for an image and if the storage doesn't exist,
|
||||
returns None.
|
||||
def _get_image_url_and_auth(self, image):
|
||||
""" Returns a tuple of the url and the auth header value that must be used
|
||||
to fetch the layer data itself. If the image can't be addressed, we return
|
||||
None.
|
||||
"""
|
||||
path = model.storage.get_layer_path(image.storage)
|
||||
locations = self._default_storage_locations
|
||||
|
@ -53,47 +63,60 @@ class SecurityScannerAPI(object):
|
|||
if not locations or not self._storage.exists(locations, path):
|
||||
logger.warning('Could not find a valid location to download layer %s.%s out of %s',
|
||||
image.docker_image_id, image.storage.uuid, locations)
|
||||
return None
|
||||
return None, None
|
||||
|
||||
uri = self._storage.get_direct_download_url(locations, path)
|
||||
auth_header = None
|
||||
if uri is None:
|
||||
# Handle local storage.
|
||||
local_storage_enabled = False
|
||||
for storage_type, _ in self._config.get('DISTRIBUTED_STORAGE_CONFIG', {}).values():
|
||||
if storage_type == 'LocalStorage':
|
||||
local_storage_enabled = True
|
||||
# Use the registry API instead, with a signed JWT giving access
|
||||
repo_name = image.repository.name
|
||||
namespace_name = image.repository.namespace_user.username
|
||||
repository_and_namespace = '/'.join([namespace_name, repo_name])
|
||||
|
||||
if local_storage_enabled:
|
||||
# TODO: fix to use the proper local storage path.
|
||||
uri = path
|
||||
else:
|
||||
logger.warning('Could not get image URL and local storage was not enabled')
|
||||
return None
|
||||
# Generate the JWT which will authorize this
|
||||
audience = 'security_scanner'
|
||||
context, subject = build_context_and_subject(None, None, None)
|
||||
access = [{
|
||||
'type': 'repository',
|
||||
'name': repository_and_namespace,
|
||||
'actions': ['pull'],
|
||||
}]
|
||||
auth_jwt = generate_jwt_object(audience, subject, context, access, TOKEN_VALIDITY_LIFETIME_S,
|
||||
self._config)
|
||||
auth_header = 'Bearer: {}'.format(auth_jwt)
|
||||
|
||||
return uri
|
||||
with self._app.test_request_context('/'):
|
||||
relative_layer_url = url_for('v2.download_blob', repository=repository_and_namespace,
|
||||
digest=image.storage.content_checksum)
|
||||
uri = urljoin(get_app_url(self._config), relative_layer_url)
|
||||
|
||||
return uri, auth_header
|
||||
|
||||
|
||||
def _new_analyze_request(self, image):
|
||||
""" Create the request body to submit the given image for analysis. If the image's URL cannot
|
||||
be found, returns None.
|
||||
"""
|
||||
url = self._get_image_url(image)
|
||||
url, auth_header = self._get_image_url_and_auth(image)
|
||||
if url is None:
|
||||
return None
|
||||
|
||||
request = {
|
||||
'Layer': {
|
||||
'Name': '%s.%s' % (image.docker_image_id, image.storage.uuid),
|
||||
'Path': url,
|
||||
'Format': 'Docker'
|
||||
}
|
||||
layer_request = {
|
||||
'Name': '%s.%s' % (image.docker_image_id, image.storage.uuid),
|
||||
'Path': url,
|
||||
'Format': 'Docker',
|
||||
}
|
||||
|
||||
if image.parent.docker_image_id and image.parent.storage.uuid:
|
||||
request['Layer']['ParentName'] = '%s.%s' % (image.parent.docker_image_id,
|
||||
image.parent.storage.uuid)
|
||||
if auth_header is not None:
|
||||
layer_request['Authorization'] = auth_header
|
||||
|
||||
return request
|
||||
if image.parent.docker_image_id and image.parent.storage.uuid:
|
||||
layer_request['ParentName'] = '%s.%s' % (image.parent.docker_image_id,
|
||||
image.parent.storage.uuid)
|
||||
|
||||
return {
|
||||
'Layer': layer_request,
|
||||
}
|
||||
|
||||
|
||||
def ping(self):
|
||||
|
|
Reference in a new issue