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 }}
+
+
+ -
+ {{ field.title }}:
+
+
+ {{ findEnumValue(field.values, notification.event_config[field.name]).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__":
|