diff --git a/endpoints/api/repositorynotification.py b/endpoints/api/repositorynotification.py index 30c71cf54..538bbe25e 100644 --- a/endpoints/api/repositorynotification.py +++ b/endpoints/api/repositorynotification.py @@ -22,12 +22,19 @@ def notification_view(note): except: config = {} + event_config = {} + try: + event_config = json.loads(note.event_config_json) + except: + event_config = {} + return { 'uuid': note.uuid, 'event': note.event.name, 'method': note.method.name, 'config': config, 'title': note.title, + 'event_config': event_config, } @@ -160,7 +167,7 @@ class TestRepositoryNotification(RepositoryParamResource): raise NotFound() 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_queue.put([test_note.repository.namespace_user.username, repository, test_note.event.name], json.dumps(notification_data)) diff --git a/endpoints/common.py b/endpoints/common.py index 7469c58be..fba900580 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -22,6 +22,7 @@ from werkzeug.routing import BaseConverter from functools import wraps from config import frontend_visible_config from external_libraries import get_external_javascript, get_external_css +from util.secscan.api import PRIORITY_LEVELS import features @@ -183,6 +184,7 @@ def render_page_template(name, **kwargs): config_set=json.dumps(frontend_visible_config(app.config)), oauth_set=json.dumps(get_oauth_config()), scope_set=json.dumps(scopes.app_scopes(app.config)), + vuln_priority_set=json.dumps(PRIORITY_LEVELS), mixpanel_key=app.config.get('MIXPANEL_KEY', ''), google_analytics_key=app.config.get('GOOGLE_ANALYTICS_KEY', ''), sentry_public_dsn=app.config.get('SENTRY_PUBLIC_DSN', ''), diff --git a/endpoints/notificationevent.py b/endpoints/notificationevent.py index ebd7e10b6..365b815d3 100644 --- a/endpoints/notificationevent.py +++ b/endpoints/notificationevent.py @@ -1,9 +1,11 @@ import logging import time +import json from datetime import datetime from notificationhelper import build_event_data from util.jinjautil import get_template_env +from util.secscan.api import PRIORITY_LEVELS, get_priority_for_index template_env = get_template_env("events") logger = logging.getLogger(__name__) @@ -37,13 +39,18 @@ class NotificationEvent(object): '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 - repository. + Returns sample data for testing the raising of this notification, with an example notification. """ 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 def event_name(cls): """ @@ -71,8 +78,8 @@ class RepoPushEvent(NotificationEvent): def get_summary(self, event_data, notification_data): return 'Repository %s updated' % (event_data['repository']) - def get_sample_data(self, repository): - return build_event_data(repository, { + def get_sample_data(self, notification): + return build_event_data(notification.repository, { 'updated_tags': {'latest': 'someimageid', 'foo': 'anotherimage'}, 'pruned_image_count': 3 }) @@ -99,18 +106,27 @@ class VulnerabilityFoundEvent(NotificationEvent): return 'info' - def get_sample_data(self, repository): - return build_event_data(repository, { + def get_sample_data(self, notification): + event_config = json.loads(notification.event_config_json) + + return build_event_data(notification.repository, { 'tags': ['latest', 'prod'], 'image': 'some-image-id', 'vulnerability': { 'id': 'CVE-FAKE-CVE', 'description': 'A futurist vulnerability', '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): msg = '%s vulnerability detected in repository %s in tags %s' return msg % (event_data['vulnerability']['priority'], @@ -126,10 +142,10 @@ class BuildQueueEvent(NotificationEvent): def get_level(self, event_data, notification_data): return 'info' - def get_sample_data(self, repository): + def get_sample_data(self, notification): build_uuid = 'fake-build-id' - return build_event_data(repository, { + return build_event_data(notification.repository, { 'is_manual': False, 'build_id': build_uuid, 'build_name': 'some-fake-build', @@ -165,10 +181,10 @@ class BuildStartEvent(NotificationEvent): def get_level(self, event_data, notification_data): return 'info' - def get_sample_data(self, repository): + def get_sample_data(self, notification): build_uuid = 'fake-build-id' - return build_event_data(repository, { + return build_event_data(notification.repository, { 'build_id': build_uuid, 'build_name': 'some-fake-build', 'docker_tags': ['latest', 'foo', 'bar'], @@ -193,10 +209,10 @@ class BuildSuccessEvent(NotificationEvent): def get_level(self, event_data, notification_data): return 'success' - def get_sample_data(self, repository): + def get_sample_data(self, notification): build_uuid = 'fake-build-id' - return build_event_data(repository, { + return build_event_data(notification.repository, { 'build_id': build_uuid, 'build_name': 'some-fake-build', 'docker_tags': ['latest', 'foo', 'bar'], @@ -222,10 +238,10 @@ class BuildFailureEvent(NotificationEvent): def get_level(self, event_data, notification_data): return 'error' - def get_sample_data(self, repository): + def get_sample_data(self, notification): build_uuid = 'fake-build-id' - return build_event_data(repository, { + return build_event_data(notification.repository, { 'build_id': build_uuid, 'build_name': 'some-fake-build', 'docker_tags': ['latest', 'foo', 'bar'], diff --git a/static/css/directives/ui/repository-events-table.css b/static/css/directives/ui/repository-events-table.css index 82909d022..f471a997c 100644 --- a/static/css/directives/ui/repository-events-table.css +++ b/static/css/directives/ui/repository-events-table.css @@ -1,3 +1,13 @@ .repository-events-table-element .notification-row i.fa { 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; } \ No newline at end of file diff --git a/static/directives/repository-events-table.html b/static/directives/repository-events-table.html index 3a463f54c..a83a45931 100644 --- a/static/directives/repository-events-table.html +++ b/static/directives/repository-events-table.html @@ -44,6 +44,17 @@ {{ getEventInfo(notification).title }} + + diff --git a/static/js/directives/ui/repository-events-table.js b/static/js/directives/ui/repository-events-table.js index 05b3e3226..8f7346e1c 100644 --- a/static/js/directives/ui/repository-events-table.js +++ b/static/js/directives/ui/repository-events-table.js @@ -43,6 +43,18 @@ angular.module('quay').directive('repositoryEventsTable', function () { $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) { return ExternalNotificationData.getEventInfo(notification.event); }; diff --git a/static/js/services/notification-service.js b/static/js/services/notification-service.js index bd4c8b70c..31ff5af2c 100644 --- a/static/js/services/notification-service.js +++ b/static/js/services/notification-service.js @@ -129,7 +129,8 @@ function($rootScope, $interval, UserService, ApiService, StringBuilderService, P 'message': 'A {vulnerability.priority} vulnerability was detected in repository {repository}', 'page': function(metadata) { return '/repository/' + metadata.repository + '?tab=tags'; - } + }, + 'dismissable': true } }; diff --git a/static/js/services/vulnerability-service.js b/static/js/services/vulnerability-service.js index 752861adb..12c1a172f 100644 --- a/static/js/services/vulnerability-service.js +++ b/static/js/services/vulnerability-service.js @@ -3,89 +3,7 @@ */ angular.module('quay').factory('VulnerabilityService', ['Config', function(Config) { var vulnService = {}; - - // 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.LEVELS = window.__vuln_priority; vulnService.getLevels = function() { return Object.keys(vulnService.LEVELS).map(function(key) { diff --git a/templates/base.html b/templates/base.html index c3f09c8ce..47eaa66ea 100644 --- a/templates/base.html +++ b/templates/base.html @@ -38,6 +38,7 @@ window.__config = {{ config_set|safe }}; window.__oauth = {{ oauth_set|safe }}; window.__auth_scopes = {{ scope_set|safe }}; + window.__vuln_priority = {{ vuln_priority_set|safe }} window.__token = '{{ csrf_token() }}'; diff --git a/util/sec/__init__.py b/util/sec/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/util/sec/secendpoint.py b/util/sec/secendpoint.py deleted file mode 100644 index 9e9e57413..000000000 --- a/util/sec/secendpoint.py +++ /dev/null @@ -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) - diff --git a/util/secscan/api.py b/util/secscan/api.py index e03a19369..b0138eeef 100644 --- a/util/secscan/api.py +++ b/util/secscan/api.py @@ -2,12 +2,103 @@ import features import logging import requests -from app import app -from database import CloseForLongOperation +from data.database import CloseForLongOperation from urlparse import urljoin 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): """ Helper class for talking to the Security Scan service (Clair). """ def __init__(self, app, config_provider): @@ -76,7 +167,7 @@ class SecurityScannerAPI(object): timeout = security_config.get('API_TIMEOUT_SECONDS', 1) logger.debug('Looking up sec information: %s', url) - with CloseForLongOperation(app.config): + with CloseForLongOperation(self.app.config): if body is not None: return client.post(url, json=body, params=kwargs, timeout=timeout, cert=self.keys, verify=self.certificate) diff --git a/workers/notificationworker.py b/workers/notificationworker.py index 3d3e2f80f..9c747303f 100644 --- a/workers/notificationworker.py +++ b/workers/notificationworker.py @@ -34,7 +34,8 @@ class NotificationWorker(QueueWorker): logger.exception('Cannot find notification event: %s', ex.message) raise JobException('Cannot find notification event: %s' % ex.message) - method_handler.perform(notification, event_handler, job_details) + if event_handler.should_perform(job_details['event_data'], notification): + method_handler.perform(notification, event_handler, job_details) if __name__ == "__main__":