Merge pull request #849 from coreos-inc/vulnerability-tool-priority

Only send vulnerability events if the minimum priority is gte to that…
This commit is contained in:
josephschorr 2015-11-10 16:15:08 -05:00
commit 744ad9e79b
13 changed files with 175 additions and 156 deletions

View file

@ -22,12 +22,19 @@ def notification_view(note):
except: except:
config = {} config = {}
event_config = {}
try:
event_config = json.loads(note.event_config_json)
except:
event_config = {}
return { return {
'uuid': note.uuid, 'uuid': note.uuid,
'event': note.event.name, 'event': note.event.name,
'method': note.method.name, 'method': note.method.name,
'config': config, 'config': config,
'title': note.title, 'title': note.title,
'event_config': event_config,
} }
@ -160,7 +167,7 @@ class TestRepositoryNotification(RepositoryParamResource):
raise NotFound() raise NotFound()
event_info = NotificationEvent.get_event(test_note.event.name) event_info = NotificationEvent.get_event(test_note.event.name)
sample_data = event_info.get_sample_data(repository=test_note.repository) sample_data = event_info.get_sample_data(test_note)
notification_data = build_notification_data(test_note, sample_data) notification_data = build_notification_data(test_note, sample_data)
notification_queue.put([test_note.repository.namespace_user.username, repository, notification_queue.put([test_note.repository.namespace_user.username, repository,
test_note.event.name], json.dumps(notification_data)) test_note.event.name], json.dumps(notification_data))

View file

@ -22,6 +22,7 @@ from werkzeug.routing import BaseConverter
from functools import wraps from functools import wraps
from config import frontend_visible_config from config import frontend_visible_config
from external_libraries import get_external_javascript, get_external_css from external_libraries import get_external_javascript, get_external_css
from util.secscan.api import PRIORITY_LEVELS
import features import features
@ -183,6 +184,7 @@ def render_page_template(name, **kwargs):
config_set=json.dumps(frontend_visible_config(app.config)), config_set=json.dumps(frontend_visible_config(app.config)),
oauth_set=json.dumps(get_oauth_config()), oauth_set=json.dumps(get_oauth_config()),
scope_set=json.dumps(scopes.app_scopes(app.config)), scope_set=json.dumps(scopes.app_scopes(app.config)),
vuln_priority_set=json.dumps(PRIORITY_LEVELS),
mixpanel_key=app.config.get('MIXPANEL_KEY', ''), mixpanel_key=app.config.get('MIXPANEL_KEY', ''),
google_analytics_key=app.config.get('GOOGLE_ANALYTICS_KEY', ''), google_analytics_key=app.config.get('GOOGLE_ANALYTICS_KEY', ''),
sentry_public_dsn=app.config.get('SENTRY_PUBLIC_DSN', ''), sentry_public_dsn=app.config.get('SENTRY_PUBLIC_DSN', ''),

View file

@ -1,9 +1,11 @@
import logging import logging
import time import time
import json
from datetime import datetime from datetime import datetime
from notificationhelper import build_event_data from notificationhelper import build_event_data
from util.jinjautil import get_template_env from util.jinjautil import get_template_env
from util.secscan.api import PRIORITY_LEVELS, get_priority_for_index
template_env = get_template_env("events") template_env = get_template_env("events")
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -37,13 +39,18 @@ class NotificationEvent(object):
'notification_data': notification_data 'notification_data': notification_data
}) })
def get_sample_data(self, repository=None): def get_sample_data(self, notification):
""" """
Returns sample data for testing the raising of this notification, with an optional Returns sample data for testing the raising of this notification, with an example notification.
repository.
""" """
raise NotImplementedError raise NotImplementedError
def should_perform(self, event_data, notification_data):
"""
Whether a notification for this event should be performed. By default returns True.
"""
return True
@classmethod @classmethod
def event_name(cls): def event_name(cls):
""" """
@ -71,8 +78,8 @@ class RepoPushEvent(NotificationEvent):
def get_summary(self, event_data, notification_data): def get_summary(self, event_data, notification_data):
return 'Repository %s updated' % (event_data['repository']) return 'Repository %s updated' % (event_data['repository'])
def get_sample_data(self, repository): def get_sample_data(self, notification):
return build_event_data(repository, { return build_event_data(notification.repository, {
'updated_tags': {'latest': 'someimageid', 'foo': 'anotherimage'}, 'updated_tags': {'latest': 'someimageid', 'foo': 'anotherimage'},
'pruned_image_count': 3 'pruned_image_count': 3
}) })
@ -99,18 +106,27 @@ class VulnerabilityFoundEvent(NotificationEvent):
return 'info' return 'info'
def get_sample_data(self, repository): def get_sample_data(self, notification):
return build_event_data(repository, { event_config = json.loads(notification.event_config_json)
return build_event_data(notification.repository, {
'tags': ['latest', 'prod'], 'tags': ['latest', 'prod'],
'image': 'some-image-id', 'image': 'some-image-id',
'vulnerability': { 'vulnerability': {
'id': 'CVE-FAKE-CVE', 'id': 'CVE-FAKE-CVE',
'description': 'A futurist vulnerability', 'description': 'A futurist vulnerability',
'link': 'https://security-tracker.debian.org/tracker/CVE-FAKE-CVE', 'link': 'https://security-tracker.debian.org/tracker/CVE-FAKE-CVE',
'priority': 'Critical', 'priority': get_priority_for_index(event_config['level'])
}, },
}) })
def should_perform(self, event_data, notification_data):
event_config = json.loads(notification_data.event_config_json)
expected_level_index = event_config['level']
priority = PRIORITY_LEVELS[event_data['vulnerability']['priority']]
actual_level_index = priority['index']
return expected_level_index <= actual_level_index
def get_summary(self, event_data, notification_data): def get_summary(self, event_data, notification_data):
msg = '%s vulnerability detected in repository %s in tags %s' msg = '%s vulnerability detected in repository %s in tags %s'
return msg % (event_data['vulnerability']['priority'], return msg % (event_data['vulnerability']['priority'],
@ -126,10 +142,10 @@ class BuildQueueEvent(NotificationEvent):
def get_level(self, event_data, notification_data): def get_level(self, event_data, notification_data):
return 'info' return 'info'
def get_sample_data(self, repository): def get_sample_data(self, notification):
build_uuid = 'fake-build-id' build_uuid = 'fake-build-id'
return build_event_data(repository, { return build_event_data(notification.repository, {
'is_manual': False, 'is_manual': False,
'build_id': build_uuid, 'build_id': build_uuid,
'build_name': 'some-fake-build', 'build_name': 'some-fake-build',
@ -165,10 +181,10 @@ class BuildStartEvent(NotificationEvent):
def get_level(self, event_data, notification_data): def get_level(self, event_data, notification_data):
return 'info' return 'info'
def get_sample_data(self, repository): def get_sample_data(self, notification):
build_uuid = 'fake-build-id' build_uuid = 'fake-build-id'
return build_event_data(repository, { return build_event_data(notification.repository, {
'build_id': build_uuid, 'build_id': build_uuid,
'build_name': 'some-fake-build', 'build_name': 'some-fake-build',
'docker_tags': ['latest', 'foo', 'bar'], 'docker_tags': ['latest', 'foo', 'bar'],
@ -193,10 +209,10 @@ class BuildSuccessEvent(NotificationEvent):
def get_level(self, event_data, notification_data): def get_level(self, event_data, notification_data):
return 'success' return 'success'
def get_sample_data(self, repository): def get_sample_data(self, notification):
build_uuid = 'fake-build-id' build_uuid = 'fake-build-id'
return build_event_data(repository, { return build_event_data(notification.repository, {
'build_id': build_uuid, 'build_id': build_uuid,
'build_name': 'some-fake-build', 'build_name': 'some-fake-build',
'docker_tags': ['latest', 'foo', 'bar'], 'docker_tags': ['latest', 'foo', 'bar'],
@ -222,10 +238,10 @@ class BuildFailureEvent(NotificationEvent):
def get_level(self, event_data, notification_data): def get_level(self, event_data, notification_data):
return 'error' return 'error'
def get_sample_data(self, repository): def get_sample_data(self, notification):
build_uuid = 'fake-build-id' build_uuid = 'fake-build-id'
return build_event_data(repository, { return build_event_data(notification.repository, {
'build_id': build_uuid, 'build_id': build_uuid,
'build_name': 'some-fake-build', 'build_name': 'some-fake-build',
'docker_tags': ['latest', 'foo', 'bar'], 'docker_tags': ['latest', 'foo', 'bar'],

View file

@ -1,3 +1,13 @@
.repository-events-table-element .notification-row i.fa { .repository-events-table-element .notification-row i.fa {
margin-right: 6px; margin-right: 6px;
} }
.repository-events-table-element .notification-event-fields {
list-style: none;
padding: 0px;
margin-left: 28px;
margin-top: 3px;
font-size: 13px;
color: #888;
margin-bottom: 0px;
}

View file

@ -44,6 +44,17 @@
<i class="fa fa-lg" ng-class="getEventInfo(notification).icon"></i> <i class="fa fa-lg" ng-class="getEventInfo(notification).icon"></i>
{{ getEventInfo(notification).title }} {{ getEventInfo(notification).title }}
</span> </span>
<ul class="notification-event-fields" ng-if="getEventInfo(notification).fields.length">
<li ng-repeat="field in getEventInfo(notification).fields">
{{ field.title }}:
<span ng-switch on="field.type">
<span ng-switch-when="enum">
{{ findEnumValue(field.values, notification.event_config[field.name]).title }}
</span>
</span>
</li>
</ul>
</td> </td>
<td> <td>

View file

@ -43,6 +43,18 @@ angular.module('quay').directive('repositoryEventsTable', function () {
$scope.showNewNotificationCounter++; $scope.showNewNotificationCounter++;
}; };
$scope.findEnumValue = function(values, index) {
var found = null;
Object.keys(values).forEach(function(key) {
if (values[key]['index'] == index) {
found = values[key];
return
}
});
return found
};
$scope.getEventInfo = function(notification) { $scope.getEventInfo = function(notification) {
return ExternalNotificationData.getEventInfo(notification.event); return ExternalNotificationData.getEventInfo(notification.event);
}; };

View file

@ -129,7 +129,8 @@ function($rootScope, $interval, UserService, ApiService, StringBuilderService, P
'message': 'A {vulnerability.priority} vulnerability was detected in repository {repository}', 'message': 'A {vulnerability.priority} vulnerability was detected in repository {repository}',
'page': function(metadata) { 'page': function(metadata) {
return '/repository/' + metadata.repository + '?tab=tags'; return '/repository/' + metadata.repository + '?tab=tags';
} },
'dismissable': true
} }
}; };

View file

@ -3,89 +3,7 @@
*/ */
angular.module('quay').factory('VulnerabilityService', ['Config', function(Config) { angular.module('quay').factory('VulnerabilityService', ['Config', function(Config) {
var vulnService = {}; var vulnService = {};
vulnService.LEVELS = window.__vuln_priority;
// NOTE: This objects are used directly in the external-notification-data service, so make sure
// to update that code if the format here is changed.
vulnService.LEVELS = {
'Unknown': {
'title': 'Unknown',
'index': '6',
'level': 'info',
'description': 'Unknown is either a security problem that has not been assigned ' +
'to a priority yet or a priority that our system did not recognize',
'banner_required': false
},
'Negligible': {
'title': 'Negligible',
'index': '5',
'level': 'info',
'description': 'Negligible is technically a security problem, but is only theoretical ' +
'in nature, requires a very special situation, has almost no install base, ' +
'or does no real damage.',
'banner_required': false
},
'Low': {
'title': 'Low',
'index': '4',
'level': 'warning',
'description': 'Low is a security problem, but is hard to exploit due to environment, ' +
'requires a user-assisted attack, a small install base, or does very ' +
'little damage.',
'banner_required': false
},
'Medium': {
'title': 'Medium',
'value': 'Medium',
'index': '3',
'level': 'warning',
'description': 'Medium is a real security problem, and is exploitable for many people. ' +
'Includes network daemon denial of service attacks, cross-site scripting, ' +
'and gaining user privileges.',
'banner_required': false
},
'High': {
'title': 'High',
'value': 'High',
'index': '2',
'level': 'warning',
'description': 'High is a real problem, exploitable for many people in a default installation. ' +
'Includes serious remote denial of services, local root privilege escalations, ' +
'or data loss.',
'banner_required': false
},
'Critical': {
'title': 'Critical',
'value': 'Critical',
'index': '1',
'level': 'error',
'description': 'Critical is a world-burning problem, exploitable for nearly all people in ' +
'a installation of the package. Includes remote root privilege escalations, ' +
'or massive data loss.',
'banner_required': true
},
'Defcon1': {
'title': 'Defcon 1',
'value': 'Defcon1',
'index': '0',
'level': 'error',
'description': 'Defcon1 is a Critical problem which has been manually highlighted ' +
'by the Quay team. It requires immediate attention.',
'banner_required': true
}
};
vulnService.getLevels = function() { vulnService.getLevels = function() {
return Object.keys(vulnService.LEVELS).map(function(key) { return Object.keys(vulnService.LEVELS).map(function(key) {

View file

@ -38,6 +38,7 @@
window.__config = {{ config_set|safe }}; window.__config = {{ config_set|safe }};
window.__oauth = {{ oauth_set|safe }}; window.__oauth = {{ oauth_set|safe }};
window.__auth_scopes = {{ scope_set|safe }}; window.__auth_scopes = {{ scope_set|safe }};
window.__vuln_priority = {{ vuln_priority_set|safe }}
window.__token = '{{ csrf_token() }}'; window.__token = '{{ csrf_token() }}';
</script> </script>

View file

View file

@ -1,51 +0,0 @@
import features
import logging
import requests
import json
from urlparse import urljoin
logger = logging.getLogger(__name__)
class SecEndpoint(object):
""" Helper class for talking to the Sec API. """
def __init__(self, app, config_provider):
self.app = app
self.config_provider = config_provider
if not features.SECURITY_SCANNER:
return
self.security_config = app.config['SECURITY_SCANNER']
self.certificate = self._getfilepath('CA_CERTIFICATE_FILENAME') or False
self.public_key = self._getfilepath('PUBLIC_KEY_FILENAME')
self.private_key = self._getfilepath('PRIVATE_KEY_FILENAME')
if self.public_key and self.private_key:
self.keys = (self.public_key, self.private_key)
else:
self.keys = None
def _getfilepath(self, config_key):
security_config = self.security_config
if config_key in security_config:
with self.config_provider.get_volume_file(security_config[config_key]) as f:
return f.name
return None
def call_api(self, relative_url, *args, **kwargs):
""" Issues an HTTP call to the sec API at the given relative URL. """
security_config = self.security_config
api_url = urljoin(security_config['ENDPOINT'], '/' + security_config['API_VERSION']) + '/'
url = urljoin(api_url, relative_url % args)
client = self.app.config['HTTPCLIENT']
timeout = security_config.get('API_CALL_TIMEOUT', 1)
logger.debug('Looking up sec information: %s', url)
return client.get(url, params=kwargs, timeout=timeout, cert=self.keys,
verify=self.certificate)

View file

@ -2,12 +2,103 @@ import features
import logging import logging
import requests import requests
from app import app from data.database import CloseForLongOperation
from database import CloseForLongOperation
from urlparse import urljoin from urlparse import urljoin
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# NOTE: This objects are used directly in the external-notification-data and vulnerability-service
# on the frontend, so be careful with changing their existing keys.
PRIORITY_LEVELS = {
'Unknown': {
'title': 'Unknown',
'index': '6',
'level': 'info',
'description': 'Unknown is either a security problem that has not been assigned ' +
'to a priority yet or a priority that our system did not recognize',
'banner_required': False
},
'Negligible': {
'title': 'Negligible',
'index': '5',
'level': 'info',
'description': 'Negligible is technically a security problem, but is only theoretical ' +
'in nature, requires a very special situation, has almost no install base, ' +
'or does no real damage.',
'banner_required': False
},
'Low': {
'title': 'Low',
'index': '4',
'level': 'warning',
'description': 'Low is a security problem, but is hard to exploit due to environment, ' +
'requires a user-assisted attack, a small install base, or does very ' +
'little damage.',
'banner_required': False
},
'Medium': {
'title': 'Medium',
'value': 'Medium',
'index': '3',
'level': 'warning',
'description': 'Medium is a real security problem, and is exploitable for many people. ' +
'Includes network daemon denial of service attacks, cross-site scripting, ' +
'and gaining user privileges.',
'banner_required': False
},
'High': {
'title': 'High',
'value': 'High',
'index': '2',
'level': 'warning',
'description': 'High is a real problem, exploitable for many people in a default installation. ' +
'Includes serious remote denial of services, local root privilege escalations, ' +
'or data loss.',
'banner_required': False
},
'Critical': {
'title': 'Critical',
'value': 'Critical',
'index': '1',
'level': 'error',
'description': 'Critical is a world-burning problem, exploitable for nearly all people in ' +
'a installation of the package. Includes remote root privilege escalations, ' +
'or massive data loss.',
'banner_required': True
},
'Defcon1': {
'title': 'Defcon 1',
'value': 'Defcon1',
'index': '0',
'level': 'error',
'description': 'Defcon1 is a Critical problem which has been manually highlighted ' +
'by the Quay team. It requires immediate attention.',
'banner_required': True
}
}
def get_priority_for_index(index):
for priority in PRIORITY_LEVELS:
if PRIORITY_LEVELS[priority]['index'] == index:
return priority
return 'Unknown'
class SecurityScannerAPI(object): class SecurityScannerAPI(object):
""" Helper class for talking to the Security Scan service (Clair). """ """ Helper class for talking to the Security Scan service (Clair). """
def __init__(self, app, config_provider): def __init__(self, app, config_provider):
@ -76,7 +167,7 @@ class SecurityScannerAPI(object):
timeout = security_config.get('API_TIMEOUT_SECONDS', 1) timeout = security_config.get('API_TIMEOUT_SECONDS', 1)
logger.debug('Looking up sec information: %s', url) logger.debug('Looking up sec information: %s', url)
with CloseForLongOperation(app.config): with CloseForLongOperation(self.app.config):
if body is not None: if body is not None:
return client.post(url, json=body, params=kwargs, timeout=timeout, cert=self.keys, return client.post(url, json=body, params=kwargs, timeout=timeout, cert=self.keys,
verify=self.certificate) verify=self.certificate)

View file

@ -34,6 +34,7 @@ class NotificationWorker(QueueWorker):
logger.exception('Cannot find notification event: %s', ex.message) logger.exception('Cannot find notification event: %s', ex.message)
raise JobException('Cannot find notification event: %s' % ex.message) raise JobException('Cannot find notification event: %s' % ex.message)
if event_handler.should_perform(job_details['event_data'], notification):
method_handler.perform(notification, event_handler, job_details) method_handler.perform(notification, event_handler, job_details)