2015-10-26 19:13:58 +00:00
|
|
|
import logging
|
|
|
|
import requests
|
|
|
|
|
2015-11-10 20:08:14 +00:00
|
|
|
from data.database import CloseForLongOperation
|
2016-02-24 21:01:27 +00:00
|
|
|
from data import model
|
|
|
|
from data.model.storage import get_storage_locations
|
|
|
|
|
2015-10-26 19:13:58 +00:00
|
|
|
from urlparse import urljoin
|
2016-02-24 21:01:27 +00:00
|
|
|
from util.secscan.validator import SecurityConfigValidator
|
2015-10-26 19:13:58 +00:00
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2016-02-24 21:01:27 +00:00
|
|
|
class AnalyzeLayerException(Exception):
|
|
|
|
""" Exception raised when a layer fails to analyze due to a *client-side* issue. """
|
2015-10-26 19:13:58 +00:00
|
|
|
|
2016-02-24 21:01:27 +00:00
|
|
|
class APIRequestFailure(Exception):
|
|
|
|
""" Exception raised when there is a failure to conduct an API request. """
|
2015-10-26 19:13:58 +00:00
|
|
|
|
|
|
|
|
2016-02-24 21:01:27 +00:00
|
|
|
_API_METHOD_INSERT = 'layers'
|
|
|
|
_API_METHOD_GET_LAYER = 'layers/%s'
|
|
|
|
_API_METHOD_GET_WITH_VULNERABILITIES_FLAG = '?vulnerabilities'
|
|
|
|
_API_METHOD_GET_WITH_FEATURES_FLAG = '?features'
|
2015-10-26 19:13:58 +00:00
|
|
|
|
|
|
|
|
2016-02-24 21:01:27 +00:00
|
|
|
class SecurityScannerAPI(object):
|
|
|
|
""" Helper class for talking to the Security Scan service (Clair). """
|
|
|
|
def __init__(self, config, config_provider, storage):
|
|
|
|
self.config = config
|
|
|
|
self.config_provider = config_provider
|
2015-10-26 19:13:58 +00:00
|
|
|
|
2016-02-24 21:01:27 +00:00
|
|
|
self._storage = storage
|
|
|
|
self._security_config = None
|
2015-10-26 19:13:58 +00:00
|
|
|
|
2016-02-24 21:01:27 +00:00
|
|
|
config_validator = SecurityConfigValidator(config, config_provider)
|
|
|
|
if not config_validator.valid():
|
|
|
|
logger.warning('Invalid config provided to SecurityScannerAPI')
|
|
|
|
return
|
2015-11-12 20:46:31 +00:00
|
|
|
|
2016-02-24 21:01:27 +00:00
|
|
|
self._default_storage_locations = config['DISTRIBUTED_STORAGE_PREFERENCE']
|
2015-11-12 20:46:31 +00:00
|
|
|
|
2016-02-24 21:01:27 +00:00
|
|
|
self._security_config = config.get('SECURITY_SCANNER')
|
|
|
|
self._target_version = self._security_config['ENGINE_VERSION_TARGET']
|
2015-11-12 22:47:19 +00:00
|
|
|
|
2016-02-24 21:01:27 +00:00
|
|
|
self._certificate = config_validator.cert()
|
|
|
|
self._keys = config_validator.keypair()
|
2015-11-12 22:47:19 +00:00
|
|
|
|
|
|
|
|
2016-02-24 21:01:27 +00:00
|
|
|
def _get_image_url(self, image):
|
|
|
|
""" Gets the download URL for an image and if the storage doesn't exist,
|
|
|
|
returns None.
|
|
|
|
"""
|
|
|
|
path = model.storage.get_layer_path(image.storage)
|
|
|
|
locations = self._default_storage_locations
|
|
|
|
|
|
|
|
if not self._storage.exists(locations, path):
|
|
|
|
locations = get_storage_locations(image.storage.uuid)
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
uri = self._storage.get_direct_download_url(locations, path)
|
|
|
|
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
|
|
|
|
|
|
|
|
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
|
2015-11-12 22:47:19 +00:00
|
|
|
|
2016-02-24 21:01:27 +00:00
|
|
|
return uri
|
2015-11-12 20:46:31 +00:00
|
|
|
|
|
|
|
|
2016-02-24 21:01:27 +00:00
|
|
|
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)
|
|
|
|
if url is None:
|
|
|
|
return None
|
|
|
|
|
|
|
|
request = {
|
|
|
|
'Layer': {
|
|
|
|
'Name': '%s.%s' % (image.docker_image_id, image.storage.uuid),
|
|
|
|
'Path': url,
|
|
|
|
'Format': 'Docker'
|
|
|
|
}
|
|
|
|
}
|
2015-11-12 20:46:31 +00:00
|
|
|
|
2016-02-24 21:01:27 +00:00
|
|
|
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)
|
2015-11-12 20:46:31 +00:00
|
|
|
|
2016-02-24 21:01:27 +00:00
|
|
|
return request
|
2015-11-12 20:46:31 +00:00
|
|
|
|
|
|
|
|
2016-02-24 21:01:27 +00:00
|
|
|
def analyze_layer(self, layer):
|
|
|
|
""" Posts the given layer to the security scanner for analysis, blocking until complete.
|
|
|
|
Returns a tuple containing the analysis version (on success, None on failure) and
|
|
|
|
whether the request should be retried.
|
|
|
|
"""
|
|
|
|
request = self._new_analyze_request(layer)
|
|
|
|
if not request:
|
|
|
|
return None, False
|
|
|
|
|
|
|
|
logger.info('Analyzing layer %s', request['Layer']['Name'])
|
2015-11-09 22:12:22 +00:00
|
|
|
try:
|
2016-02-24 21:01:27 +00:00
|
|
|
response = self._call(_API_METHOD_INSERT, request)
|
|
|
|
json_response = response.json()
|
|
|
|
except requests.exceptions.Timeout:
|
|
|
|
logger.exception('Timeout when trying to post layer data response for %s', layer.id)
|
|
|
|
return None, True
|
|
|
|
except requests.exceptions.ConnectionError:
|
|
|
|
logger.exception('Connection error when trying to post layer data response for %s', layer.id)
|
|
|
|
return None, True
|
|
|
|
except (requests.exceptions.RequestException, ValueError):
|
|
|
|
logger.exception('Failed to post layer data response for %s', layer.id)
|
|
|
|
return None, False
|
|
|
|
|
|
|
|
# Handle any errors from the security scanner.
|
|
|
|
if response.status_code != 201:
|
|
|
|
message = json_response.get('Error').get('Message', '')
|
|
|
|
logger.warning('A warning event occurred when analyzing layer %s (status code %s): %s',
|
|
|
|
request['Layer']['Name'], response.status_code, message)
|
|
|
|
|
|
|
|
# 400 means the layer could not be analyzed due to a bad request.
|
|
|
|
if response.status_code == 400:
|
|
|
|
logger.error('Bad request when calling security scanner for layer %s: %s',
|
|
|
|
response.status_code, json_response)
|
|
|
|
raise AnalyzeLayerException('Bad request to security scanner')
|
|
|
|
|
|
|
|
# 422 means that the layer could not be analyzed:
|
|
|
|
# - the layer could not be extracted (manifest?)
|
|
|
|
# - the layer operating system / package manager is unsupported
|
|
|
|
return None, response.status_code != 422
|
|
|
|
|
|
|
|
api_version = json_response['Layer']['IndexedByVersion']
|
|
|
|
return api_version, False
|
|
|
|
|
|
|
|
|
|
|
|
def get_layer_data(self, layer, include_features=False, include_vulnerabilities=False):
|
|
|
|
""" Returns the layer data for the specified layer. On error, returns None. """
|
|
|
|
layer_id = '%s.%s' % (layer.docker_image_id, layer.storage.uuid)
|
|
|
|
try:
|
|
|
|
flag = ''
|
|
|
|
if include_features:
|
|
|
|
flag = _API_METHOD_GET_WITH_FEATURES_FLAG
|
2015-11-09 22:12:22 +00:00
|
|
|
|
2016-02-24 21:01:27 +00:00
|
|
|
if include_vulnerabilities:
|
|
|
|
flag = _API_METHOD_GET_WITH_VULNERABILITIES_FLAG
|
2015-11-09 22:12:22 +00:00
|
|
|
|
2016-02-24 21:01:27 +00:00
|
|
|
response = self._call(_API_METHOD_GET_LAYER + flag, None, layer_id)
|
|
|
|
logger.debug('Got response %s for vulnerabilities for layer %s',
|
|
|
|
response.status_code, layer_id)
|
|
|
|
except requests.exceptions.Timeout:
|
|
|
|
raise APIRequestFailure('API call timed out')
|
|
|
|
except requests.exceptions.ConnectionError:
|
|
|
|
raise APIRequestFailure('Could not connect to security service')
|
|
|
|
except (requests.exceptions.RequestException, ValueError):
|
|
|
|
logger.exception('Failed to get layer data response for %s', layer.id)
|
|
|
|
raise APIRequestFailure()
|
|
|
|
|
|
|
|
if response.status_code == 404:
|
|
|
|
return None
|
2015-11-09 22:12:22 +00:00
|
|
|
|
2016-02-24 21:01:27 +00:00
|
|
|
return response.json()
|
2015-11-09 22:12:22 +00:00
|
|
|
|
|
|
|
|
2016-02-24 21:01:27 +00:00
|
|
|
def _call(self, relative_url, body=None, *args, **kwargs):
|
2015-11-10 20:01:33 +00:00
|
|
|
""" Issues an HTTP call to the sec API at the given relative URL.
|
|
|
|
This function disconnects from the database while awaiting a response
|
|
|
|
from the API server.
|
|
|
|
"""
|
2015-11-12 22:02:18 +00:00
|
|
|
security_config = self._security_config
|
2015-11-12 22:47:19 +00:00
|
|
|
if security_config is None:
|
|
|
|
raise Exception('Cannot call unconfigured security system')
|
|
|
|
|
2015-10-26 19:13:58 +00:00
|
|
|
api_url = urljoin(security_config['ENDPOINT'], '/' + security_config['API_VERSION']) + '/'
|
|
|
|
url = urljoin(api_url, relative_url % args)
|
|
|
|
|
2016-02-24 21:01:27 +00:00
|
|
|
client = self.config['HTTPCLIENT']
|
2015-10-26 19:13:58 +00:00
|
|
|
timeout = security_config.get('API_TIMEOUT_SECONDS', 1)
|
|
|
|
logger.debug('Looking up sec information: %s', url)
|
|
|
|
|
2016-02-24 21:01:27 +00:00
|
|
|
with CloseForLongOperation(self.config):
|
2015-11-10 20:01:33 +00:00
|
|
|
if body is not None:
|
2016-02-24 21:01:27 +00:00
|
|
|
logger.debug('POSTing security URL %s', url)
|
2015-11-12 22:02:18 +00:00
|
|
|
return client.post(url, json=body, params=kwargs, timeout=timeout, cert=self._keys,
|
|
|
|
verify=self._certificate)
|
2015-11-10 20:01:33 +00:00
|
|
|
else:
|
2016-02-24 21:01:27 +00:00
|
|
|
logger.debug('GETing security URL %s', url)
|
2015-11-12 22:02:18 +00:00
|
|
|
return client.get(url, params=kwargs, timeout=timeout, cert=self._keys,
|
|
|
|
verify=self._certificate)
|