Update quay sec code to fix problems identified in previous review

- Change get_repository_images_recursive to operate over a single docker image and storage uuid
- Move endpoints/sec to endpoints/secscan
- Change notification system to work with new Quay-sec format

Fixes #768
This commit is contained in:
Joseph Schorr 2015-11-09 17:12:22 -05:00
parent 16c364a90c
commit a69c9e12fd
7 changed files with 146 additions and 79 deletions

View file

@ -18,7 +18,7 @@ logger = logging.getLogger(__name__)
def _call_security_api(relative_url, *args, **kwargs):
""" Issues an HTTP call to the sec API at the given relative URL. """
try:
response = secscan_endpoint.call_api(relative_url, *args, **kwargs)
response = secscan_endpoint.call_api(relative_url, body=None, *args, **kwargs)
except requests.exceptions.Timeout:
raise DownstreamIssue(payload=dict(message='API call timed out'))
except requests.exceptions.ConnectionError:

View file

@ -1,58 +0,0 @@
import logging
from flask import request, make_response, Blueprint
from data import model
from data.database import RepositoryNotification, Repository, ExternalNotificationEvent, RepositoryTag, Image
from endpoints.notificationhelper import spawn_notification
from collections import defaultdict
logger = logging.getLogger(__name__)
sec = Blueprint('sec', __name__)
@sec.route('/notification', methods=['POST'])
def sec_notification():
data = request.get_json()
# Find all tags that contain the layer(s) introducing the vulnerability.
# TODO: remove this check once fixed.
if not 'IntroducingLayersIDs' in data['Content']:
return make_response('Okay')
# TODO: fix this for the image_id.storage thing properly.
layer_ids = [full_id.split('.')[0] for full_id in data['Content']['IntroducingLayersIDs']]
if not layer_ids:
return make_response('Okay')
tags = model.tag.get_matching_tags(layer_ids, RepositoryTag, Repository, Image)
# For any repository that has a notification setup, issue a notification.
event = ExternalNotificationEvent.get(name='vulnerability_found')
matching = (tags.switch(RepositoryTag)
.join(Repository)
.join(RepositoryNotification)
.where(RepositoryNotification.event == event))
repository_map = defaultdict(list)
for tag in matching:
repository_map[tag.repository_id].append(tag)
for repository_id in repository_map:
tags = repository_map[repository_id]
# TODO(jschorr): Pull out the other metadata once added.
event_data = {
'tags': [tag.name for tag in tags],
'vulnerability': {
'id': data['Name'],
'description': 'Some description',
'link': 'https://security-tracker.debian.org/tracker/CVE-FAKE-CVE',
'priority': 'High',
},
}
spawn_notification(tags[0].repository, 'vulnerability_found', event_data)
return make_response('Okay')

88
endpoints/secscan.py Normal file
View file

@ -0,0 +1,88 @@
import logging
import features
from app import secscan_endpoint
from flask import request, make_response, Blueprint
from data import model
from data.database import (RepositoryNotification, Repository, ExternalNotificationEvent,
RepositoryTag, Image, ImageStorage)
from endpoints.common import route_show_if
from endpoints.notificationhelper import spawn_notification
from collections import defaultdict
logger = logging.getLogger(__name__)
secscan = Blueprint('secscan', __name__)
@route_show_if(features.SECURITY_SCANNER)
@secscan.route('/notification', methods=['POST'])
def secscan_notification():
data = request.get_json()
logger.debug('Got notification from Clair: %s', data)
# Find all tags that contain the layer(s) introducing the vulnerability.
content = data['Content']
layer_ids = content.get('NewIntroducingLayersIDs', content.get('IntroducingLayersIDs', []))
if not layer_ids:
return make_response('Okay')
# TODO(jzelinkskie): Write a queueitem for these layer ids, and do the rest of this
# in a worker.
cve_id = data['Name']
vulnerability = data['Content']['Vulnerability']
priority = vulnerability['Priority']
# Lookup the external event for when we have vulnerabilities.
event = ExternalNotificationEvent.get(name='vulnerability_found')
# For each layer, retrieving the matching tags and join with repository to determine which
# require new notifications.
tag_map = defaultdict(set)
repository_map = {}
for layer_id in layer_ids:
(docker_image_id, storage_uuid) = layer_id.split('.', 2)
tags = model.tag.get_matching_tags(docker_image_id, storage_uuid, RepositoryTag,
Repository, Image, ImageStorage)
# Additionally filter to tags only in repositories that have the event setup.
matching = (tags.switch(RepositoryTag)
.join(Repository)
.join(RepositoryNotification)
.where(RepositoryNotification.event == event))
check_map = {}
for tag in matching:
# Verify that the tag's root image has the vulnerability.
tag_layer_id = '%s.%s' % (tag.image.docker_image_id, tag.image.storage.uuid)
logger.debug('Checking if layer %s is vulnerable to %s', tag_layer_id, cve_id)
if not tag_layer_id in check_map:
is_vulerable = secscan_endpoint.check_layer_vulnerable(tag_layer_id, cve_id)
check_map[tag_layer_id] = is_vulerable
logger.debug('Result of layer %s is vulnerable to %s check: %s', tag_layer_id, cve_id,
check_map[tag_layer_id])
if check_map[tag_layer_id]:
# Add the vulnerable tag to the list.
tag_map[tag.repository_id].add(tag.name)
repository_map[tag.repository_id] = tag.repository
# For each of the tags found, issue a notification.
for repository_id in tag_map:
tags = tag_map[repository_id]
event_data = {
'tags': list(tags),
'vulnerability': {
'id': data['Name'],
'description': vulnerability['Description'],
'link': vulnerability['Link'],
'priority': priority,
},
}
# TODO: only add this notification if the repository's event(s) defined meet the priority
# minimum.
spawn_notification(repository_map[repository_id], 'vulnerability_found', event_data)
return make_response('Okay')