From 3b3f101ea668fd6aa5cfb18515fe91e611668c2f Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 12 Nov 2015 15:42:45 -0500 Subject: [PATCH] Vulnerability UI part 2 Fixes #860 Fixes #855 --- endpoints/api/secscan.py | 22 +++- .../directives/repo-view/repo-panel-info.css | 50 +++++++- .../directives/repo-view/repo-panel-tags.css | 4 +- static/css/directives/ui/filter-box.css | 2 +- .../ui/repository-events-summary.css | 21 ++++ .../ui/vulnerability-priority-view.css | 2 +- static/css/pages/image-view.css | 27 ++++- .../directives/repo-view/repo-panel-info.html | 20 +++- .../directives/repo-view/repo-panel-tags.html | 19 +++- .../directives/repository-events-summary.html | 22 ++++ .../directives/repository-events-table.html | 1 + static/img/lock.svg | 17 +++ static/img/scan.svg | 14 +++ .../directives/repo-view/repo-panel-info.js | 3 +- .../directives/repo-view/repo-panel-tags.js | 4 +- .../ui/create-external-notification-dialog.js | 17 ++- .../ui/repository-events-summary.js | 77 +++++++++++++ .../directives/ui/repository-events-table.js | 29 ++++- static/js/pages/image-view.js | 19 +++- static/js/pages/repo-view.js | 5 + .../js/services/external-notification-data.js | 4 +- static/partials/image-view.html | 107 +++++++++++------- static/partials/repo-view.html | 6 +- 23 files changed, 419 insertions(+), 73 deletions(-) create mode 100644 static/css/directives/ui/repository-events-summary.css create mode 100644 static/directives/repository-events-summary.html create mode 100644 static/img/lock.svg create mode 100644 static/img/scan.svg create mode 100644 static/js/directives/ui/repository-events-summary.js diff --git a/endpoints/api/secscan.py b/endpoints/api/secscan.py index 9a1fee133..c05e40fef 100644 --- a/endpoints/api/secscan.py +++ b/endpoints/api/secscan.py @@ -15,6 +15,13 @@ from endpoints.api import (require_repo_read, NotFound, DownstreamIssue, path_pa logger = logging.getLogger(__name__) +class SCAN_STATUS(object): + """ Security scan status enum """ + SCANNED = 'scanned' + FAILED = 'failed' + QUEUED = 'queued' + + def _call_security_api(relative_url, *args, **kwargs): """ Issues an HTTP call to the sec API at the given relative URL. """ try: @@ -39,6 +46,13 @@ def _call_security_api(relative_url, *args, **kwargs): return response_data +def _get_status(repo_image): + if repo_image.security_indexed_engine: + return SCAN_STATUS.SCANNED if repo_image.security_indexed else SCAN_STATUS.FAILED + + return SCAN_STATUS.QUEUED + + @show_if(features.SECURITY_SCANNER) @resource('/v1/repository//image//vulnerabilities') @path_param('repository', 'The full path of the repository. e.g. namespace/name') @@ -61,7 +75,7 @@ class RepositoryImageVulnerabilities(RepositoryParamResource): logger.debug('Image %s under repository %s/%s not security indexed', repo_image.docker_image_id, namespace, repository) return { - 'security_indexed': False + 'status': _get_status(repo_image), } layer_id = '%s.%s' % (repo_image.docker_image_id, repo_image.storage.uuid) @@ -69,7 +83,7 @@ class RepositoryImageVulnerabilities(RepositoryParamResource): minimumPriority=args.minimumPriority) return { - 'security_indexed': True, + 'status': _get_status(repo_image), 'data': data, } @@ -91,14 +105,14 @@ class RepositoryImagePackages(RepositoryParamResource): if not repo_image.security_indexed: return { - 'security_indexed': False + 'status': _get_status(repo_image), } layer_id = '%s.%s' % (repo_image.docker_image_id, repo_image.storage.uuid) data = _call_security_api('layers/%s/packages', layer_id) return { - 'security_indexed': True, + 'status': _get_status(repo_image), 'data': data, } diff --git a/static/css/directives/repo-view/repo-panel-info.css b/static/css/directives/repo-view/repo-panel-info.css index 1b39b3a6b..032fd7d4b 100644 --- a/static/css/directives/repo-view/repo-panel-info.css +++ b/static/css/directives/repo-view/repo-panel-info.css @@ -3,10 +3,56 @@ float: right; } +.repo-panel-info-element .right-sec-controls { + border: 1px solid #ddd; + padding: 20px; + border-radius: 4px; + max-width: 400px; +} + +.repo-panel-info-element .right-sec-controls { + color: #333; + font-weight: 300; + padding-left: 70px; + position: relative; +} + +.repo-panel-info-element .right-sec-controls .sec-logo { + position: absolute; + top: 17px; + left: 15px; +} + +.repo-panel-info-element .right-sec-controls .sec-logo .lock { + position: absolute; + top: 5px; + right: 10px; +} + +.repo-panel-info-element .right-sec-controls b { + color: #333; + font-weight: normal; + margin-bottom: 20px; + display: block; +} + +.repo-panel-info-element .right-sec-controls .configure-alerts { + margin-top: 20px; + font-weight: normal; +} + +.repo-panel-info-element .right-sec-controls .configure-alerts .fa { + margin-right: 6px; +} + +.repo-panel-info-element .right-sec-controls .repository-events-summary { + margin-top: 20px; +} + .repo-panel-info-element .right-controls .copy-box { width: 400px; - display: inline-block; - margin-left: 10px; + margin-top: 10px; + margin-bottom: 20px; } .repo-panel-info-element .stat-col { diff --git a/static/css/directives/repo-view/repo-panel-tags.css b/static/css/directives/repo-view/repo-panel-tags.css index f2818187b..c0a1073c4 100644 --- a/static/css/directives/repo-view/repo-panel-tags.css +++ b/static/css/directives/repo-view/repo-panel-tags.css @@ -93,7 +93,9 @@ margin-right: 4px; } -.repo-panel-tags-element .security-scan-col .scanning { +.repo-panel-tags-element .security-scan-col .scanning, +.repo-panel-tags-element .security-scan-col .failed-scan, +.repo-panel-tags-element .security-scan-col .vuln-load-error { color: #9B9B9B; font-size: 12px; } diff --git a/static/css/directives/ui/filter-box.css b/static/css/directives/ui/filter-box.css index 836a72b11..604930a0d 100644 --- a/static/css/directives/ui/filter-box.css +++ b/static/css/directives/ui/filter-box.css @@ -20,7 +20,7 @@ .filter-box.floating { float: right; min-width: 300px; - margin-top: 0px; + margin-top: 15px; position: relative; } diff --git a/static/css/directives/ui/repository-events-summary.css b/static/css/directives/ui/repository-events-summary.css new file mode 100644 index 000000000..1ff4d60b2 --- /dev/null +++ b/static/css/directives/ui/repository-events-summary.css @@ -0,0 +1,21 @@ +.repository-events-summary-element .summary-list { + padding: 0px; + margin: 0px; + list-style: none; +} + +.repository-events-summary-element .summary-list li { + margin-bottom: 6px; +} + +.repository-events-summary-element .summary-list li i.fa { + margin-right: 4px; +} + +.repository-events-summary-element .notification-event-fields { + display: inline-block; + padding: 0px; + margin: 0px; + list-style: none; + padding-left: 24px; +} \ No newline at end of file diff --git a/static/css/directives/ui/vulnerability-priority-view.css b/static/css/directives/ui/vulnerability-priority-view.css index c3487d6cb..88dd4d27c 100644 --- a/static/css/directives/ui/vulnerability-priority-view.css +++ b/static/css/directives/ui/vulnerability-priority-view.css @@ -4,7 +4,7 @@ .vulnerability-priority-view-element.Unknown, .vulnerability-priority-view-element.Low, -.vulnerability-priority-view-element.Negligable { +.vulnerability-priority-view-element.Negligible { color: #9B9B9B; } diff --git a/static/css/pages/image-view.css b/static/css/pages/image-view.css index 6b5cfa555..a1cb55bab 100644 --- a/static/css/pages/image-view.css +++ b/static/css/pages/image-view.css @@ -21,7 +21,7 @@ } .image-view .co-tab-content h3 { - margin-bottom: 20px; + margin-bottom: 30px; } .image-view .fa-bug { @@ -42,4 +42,29 @@ .image-view .co-filter-box input { display: inline-block; +} + +.image-view .level-col h4 { + margin-top: 0px; + margin-bottom: 20px; +} + +.image-view .levels { + list-style: none; + padding: 0px; + margin: 0px; +} + +.image-view .levels li { + margin-bottom: 20px; +} + +.image-view .levels li .description { + margin-top: 6px; + font-size: 14px; + color: #999; +} + +.image-view .level-col { + padding: 20px; } \ No newline at end of file diff --git a/static/directives/repo-view/repo-panel-info.html b/static/directives/repo-view/repo-panel-info.html index 5fdab5a4c..7faf2b417 100644 --- a/static/directives/repo-view/repo-panel-info.html +++ b/static/directives/repo-view/repo-panel-info.html @@ -65,7 +65,25 @@

Description

diff --git a/static/directives/repo-view/repo-panel-tags.html b/static/directives/repo-view/repo-panel-tags.html index 0f1125d8e..6c182bfe3 100644 --- a/static/directives/repo-view/repo-panel-tags.html +++ b/static/directives/repo-view/repo-panel-tags.html @@ -115,15 +115,26 @@ + + Could not load security information + - - + Queued for scan + + + + Failed to scan + + @@ -134,7 +145,7 @@ - +
+
    +
  • + + {{ getMethodInfo(notification).title }} for + +
      +
    • + {{ field.title }} of + + + {{ findEnumValue(field.values, notification.event_config[field.name]).title }} + + +
    • +
    +
  • +
+
+ \ No newline at end of file diff --git a/static/directives/repository-events-table.html b/static/directives/repository-events-table.html index a83a45931..fd40d9789 100644 --- a/static/directives/repository-events-table.html +++ b/static/directives/repository-events-table.html @@ -100,5 +100,6 @@
diff --git a/static/img/lock.svg b/static/img/lock.svg new file mode 100644 index 000000000..748961f52 --- /dev/null +++ b/static/img/lock.svg @@ -0,0 +1,17 @@ + + + + Artboard 1 + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file diff --git a/static/img/scan.svg b/static/img/scan.svg new file mode 100644 index 000000000..91ea19e43 --- /dev/null +++ b/static/img/scan.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/static/js/directives/repo-view/repo-panel-info.js b/static/js/directives/repo-view/repo-panel-info.js index 2902699ec..f11894732 100644 --- a/static/js/directives/repo-view/repo-panel-info.js +++ b/static/js/directives/repo-view/repo-panel-info.js @@ -10,7 +10,8 @@ angular.module('quay').directive('repoPanelInfo', function () { restrict: 'C', scope: { 'repository': '=repository', - 'builds': '=builds' + 'builds': '=builds', + 'isEnabled': '=isEnabled' }, controller: function($scope, $element, ApiService, Config) { $scope.$watch('repository', function(repository) { diff --git a/static/js/directives/repo-view/repo-panel-tags.js b/static/js/directives/repo-view/repo-panel-tags.js index 6f80d193f..1ea285f45 100644 --- a/static/js/directives/repo-view/repo-panel-tags.js +++ b/static/js/directives/repo-view/repo-panel-tags.js @@ -157,10 +157,10 @@ angular.module('quay').directive('repoPanelTags', function () { }; ApiService.getRepoImageVulnerabilities(null, params).then(function(resp) { - imageData.security_indexed = resp.security_indexed; imageData.loading = false; + imageData.status = resp['status']; - if (imageData.security_indexed) { + if (imageData.status == 'scanned') { var vulnerabilities = resp.data.Vulnerabilities; imageData.hasVulnerabilities = !!vulnerabilities.length; diff --git a/static/js/directives/ui/create-external-notification-dialog.js b/static/js/directives/ui/create-external-notification-dialog.js index cbf8696e3..874769216 100644 --- a/static/js/directives/ui/create-external-notification-dialog.js +++ b/static/js/directives/ui/create-external-notification-dialog.js @@ -11,7 +11,8 @@ angular.module('quay').directive('createExternalNotificationDialog', function () scope: { 'repository': '=repository', 'counter': '=counter', - 'notificationCreated': '¬ificationCreated' + 'notificationCreated': '¬ificationCreated', + 'defaultData': '=defaultData' }, controller: function($scope, $element, ExternalNotificationData, ApiService, $timeout, StringBuilderService) { $scope.currentEvent = null; @@ -98,6 +99,13 @@ angular.module('quay').directive('createExternalNotificationDialog', function () ApiService.createRepoNotification(data, params).then(function(resp) { $scope.status = ''; $scope.notificationCreated({'notification': resp}); + + // Used by repository-events-summary. + if (!$scope.repository._notificationCounter) { + $scope.repository._notificationCounter = 0; + } + + $scope.repository._notificationCounter++; $('#createNotificationModal').modal('hide'); }); }; @@ -154,6 +162,13 @@ angular.module('quay').directive('createExternalNotificationDialog', function () $scope.currentEvent = null; $scope.currentMethod = null; $scope.unauthorizedEmail = false; + + $timeout(function() { + if ($scope.defaultData && $scope.defaultData['currentEvent']) { + $scope.setEvent($scope.defaultData['currentEvent']); + } + }, 100); + $('#createNotificationModal').modal({}); } }); diff --git a/static/js/directives/ui/repository-events-summary.js b/static/js/directives/ui/repository-events-summary.js new file mode 100644 index 000000000..50d667334 --- /dev/null +++ b/static/js/directives/ui/repository-events-summary.js @@ -0,0 +1,77 @@ +/** + * An element which displays a summary of events on a repository of a particular type. + */ +angular.module('quay').directive('repositoryEventsSummary', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/repository-events-summary.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'repository': '=repository', + 'isEnabled': '=isEnabled', + 'eventFilter': '@eventFilter', + 'hasEvents': '=hasEvents' + }, + controller: function($scope, ApiService, ExternalNotificationData) { + var loadNotifications = function() { + if (!$scope.repository || !$scope.isEnabled || !$scope.eventFilter || $scope.notificationsResource) { + return; + } + + var params = { + 'repository': $scope.repository.namespace + '/' + $scope.repository.name + }; + + $scope.notificationsResource = ApiService.listRepoNotificationsAsResource(params).get( + function(resp) { + var notifications = []; + resp.notifications.forEach(function(notification) { + if (notification.event == $scope.eventFilter) { + notifications.push(notification); + } + }); + + $scope.notifications = notifications; + $scope.hasEvents = !!notifications.length; + return $scope.notifications; + }); + }; + + $scope.$watch('repository', loadNotifications); + $scope.$watch('isEnabled', loadNotifications); + $scope.$watch('eventFilter', loadNotifications); + + // Watch _notificationCounter, which is set by create-external-notification-dialog. We use this + // to invalidate and reload. + $scope.$watch('repository._notificationCounter', function() { + $scope.notificationsResource = null; + loadNotifications(); + }); + + loadNotifications(); + + $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); + }; + + $scope.getMethodInfo = function(notification) { + return ExternalNotificationData.getMethodInfo(notification.method); + }; + } + }; + return directiveDefinitionObject; +}); \ No newline at end of file diff --git a/static/js/directives/ui/repository-events-table.js b/static/js/directives/ui/repository-events-table.js index 8f7346e1c..8aca804b6 100644 --- a/static/js/directives/ui/repository-events-table.js +++ b/static/js/directives/ui/repository-events-table.js @@ -13,11 +13,29 @@ angular.module('quay').directive('repositoryEventsTable', function () { 'repository': '=repository', 'isEnabled': '=isEnabled' }, - controller: function($scope, $element, ApiService, Restangular, UtilService, ExternalNotificationData) { + controller: function($scope, $element, $timeout, ApiService, Restangular, UtilService, ExternalNotificationData, $location) { $scope.showNewNotificationCounter = 0; + $scope.newNotificationData = {}; var loadNotifications = function() { - if (!$scope.repository || $scope.notificationsResource || !$scope.isEnabled) { return; } + if (!$scope.repository || !$scope.isEnabled) { return; } + + var add_event = $location.search()['add_event']; + if (add_event) { + $timeout(function() { + $scope.newNotificationData = { + 'currentEvent': ExternalNotificationData.getEventInfo(add_event) + }; + + $scope.askCreateNotification(); + }, 100); + + $location.search('add_event', null); + } + + if ($scope.notificationsResource) { + return; + } var params = { 'repository': $scope.repository.namespace + '/' + $scope.repository.name @@ -73,6 +91,13 @@ angular.module('quay').directive('repositoryEventsTable', function () { var index = $.inArray(notification, $scope.notifications); if (index < 0) { return; } $scope.notifications.splice(index, 1); + + if (!$scope.repository._notificationCounter) { + $scope.repository._notificationCounter = 0; + } + + $scope.repository._notificationCounter++; + }, ApiService.errorDisplay('Cannot delete notification')); }; diff --git a/static/js/pages/image-view.js b/static/js/pages/image-view.js index 22da844a7..2783c49a6 100644 --- a/static/js/pages/image-view.js +++ b/static/js/pages/image-view.js @@ -55,26 +55,33 @@ $scope.packagesResource = ApiService.getRepoImagePackagesAsResource(params).get(function(packages) { $scope.packages = packages; + return packages; }); }; $scope.loadImageVulnerabilities = function() { if (!Features.SECURITY_SCANNER || $scope.vulnerabilitiesResource) { return; } + $scope.VulnerabilityLevels = VulnerabilityService.getLevels(); + var params = { 'repository': namespace + '/' + name, 'imageid': imageid }; $scope.vulnerabilitiesResource = ApiService.getRepoImageVulnerabilitiesAsResource(params).get(function(resp) { - $scope.vulerabilityInfo = resp; + $scope.vulnerabilityInfo = resp; $scope.vulnerabilities = []; - resp.data.Vulnerabilities.forEach(function(vuln) { - vuln_copy = jQuery.extend({}, vuln); - vuln_copy['index'] = VulnerabilityService.LEVELS[vuln['Priority']]['index']; - $scope.vulnerabilities.push(vuln_copy); - }); + if (resp.data && resp.data.Vulnerabilities) { + resp.data.Vulnerabilities.forEach(function(vuln) { + vuln_copy = jQuery.extend({}, vuln); + vuln_copy['index'] = VulnerabilityService.LEVELS[vuln['Priority']]['index']; + $scope.vulnerabilities.push(vuln_copy); + }); + } + + return resp; }); }; diff --git a/static/js/pages/repo-view.js b/static/js/pages/repo-view.js index c5ae7f093..e7088d399 100644 --- a/static/js/pages/repo-view.js +++ b/static/js/pages/repo-view.js @@ -17,6 +17,7 @@ var imageLoader = ImageLoaderService.getLoader($scope.namespace, $scope.name); // Tab-enabled counters. + $scope.infoShown = 0; $scope.tagsShown = 0; $scope.logsShown = 0; $scope.buildsShown = 0; @@ -119,6 +120,10 @@ $scope.viewScope.selectedTags = $.unique(tagNames.split(',')); }; + $scope.showInfo = function() { + $scope.infoShown++; + }; + $scope.showBuilds = function() { $scope.buildsShown++; }; diff --git a/static/js/services/external-notification-data.js b/static/js/services/external-notification-data.js index 14de1d24e..b9356f671 100644 --- a/static/js/services/external-notification-data.js +++ b/static/js/services/external-notification-data.js @@ -47,12 +47,12 @@ function(Config, Features, VulnerabilityService) { events.push({ 'id': 'vulnerability_found', 'title': 'Package Vulnerability Found', - 'icon': 'fa-flag', + 'icon': 'fa-bug', 'fields': [ { 'name': 'level', 'type': 'enum', - 'title': 'Minimum Severity Level', + 'title': 'Minimum Priority Level', 'values': VulnerabilityService.LEVELS, } ] diff --git a/static/partials/image-view.html b/static/partials/image-view.html index 6e4b40767..0693097cb 100644 --- a/static/partials/image-view.html +++ b/static/partials/image-view.html @@ -67,45 +67,67 @@
-
+
+
-

Image Security

-
-
This image has not been indexed yet
-
- Please try again in a few minutes. -
-
-
-
This image contains no recognized security vulnerabilities
-
- Quay currently indexes Debian, Red Hat and Ubuntu packages. -
-
- -
- - - - - - - - - - - -
VulnerabilityPriorityDescription
{{ vulnerability.ID }} - - {{ vulnerability.Description }}
- -
-
No matching vulnerabilities found
+

Image Security

+
+
This image has not been indexed yet
- Please adjust your filter above. + Please try again in a few minutes.
+ +
+
This image could not be indexed
+
+ Our security scanner was unable to index this image. +
+
+ +
+
This image contains no recognized security vulnerabilities
+
+ Quay currently indexes Debian, Red Hat and Ubuntu packages. +
+
+ +
+ + + + + + + + + + + +
VulnerabilityPriorityDescription
{{ vulnerability.ID }} + + {{ vulnerability.Description }}
+ +
+
No matching vulnerabilities found
+
+ Please adjust your filter above. +
+
+
+
+ +
@@ -113,23 +135,24 @@
-
+

Image Packages

-
+
This image has not been indexed yet
Please try again in a few minutes.
-
-
This image contains no recognized packages
+ +
+
This image could not be indexed
- Quay currently indexes Debian, Red Hat and Ubuntu packages. + Our security scanner was unable to index this image.
- +
@@ -146,7 +169,7 @@
No matching packages found
-
+
Please adjust your filter above.
diff --git a/static/partials/repo-view.html b/static/partials/repo-view.html index cd3cbca2b..e16ba85ed 100644 --- a/static/partials/repo-view.html +++ b/static/partials/repo-view.html @@ -17,7 +17,8 @@
- + @@ -56,7 +57,8 @@
+ builds="viewScope.builds" + is-enabled="infoShown">
Package Name Package Version