diff --git a/endpoints/api/secscan.py b/endpoints/api/secscan.py index 56fcea95d..9a1fee133 100644 --- a/endpoints/api/secscan.py +++ b/endpoints/api/secscan.py @@ -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_api.call(relative_url, body=None, *args, **kwargs) + response = secscan_api.call(relative_url, None, *args, **kwargs) except requests.exceptions.Timeout: raise DownstreamIssue(payload=dict(message='API call timed out')) except requests.exceptions.ConnectionError: @@ -40,32 +40,32 @@ def _call_security_api(relative_url, *args, **kwargs): @show_if(features.SECURITY_SCANNER) -@resource('/v1/repository//tag//vulnerabilities') +@resource('/v1/repository//image//vulnerabilities') @path_param('repository', 'The full path of the repository. e.g. namespace/name') -@path_param('tag', 'The name of the tag') -class RepositoryTagVulnerabilities(RepositoryParamResource): - """ Operations for managing the vulnerabilities in a repository tag. """ +@path_param('imageid', 'The image ID') +class RepositoryImageVulnerabilities(RepositoryParamResource): + """ Operations for managing the vulnerabilities in a repository image. """ @require_repo_read - @nickname('getRepoTagVulnerabilities') + @nickname('getRepoImageVulnerabilities') @parse_args @query_param('minimumPriority', 'Minimum vulnerability priority', type=str, default='Low') - def get(self, args, namespace, repository, tag): + def get(self, args, namespace, repository, imageid): """ Fetches the vulnerabilities (if any) for a repository tag. """ - try: - tag_image = model.tag.get_tag_image(namespace, repository, tag) - except model.DataModelException: + repo_image = model.image.get_repo_image(namespace, repository, imageid) + if repo_image is None: raise NotFound() - if not tag_image.security_indexed: - logger.debug('Image %s for tag %s under repository %s/%s not security indexed', - tag_image.docker_image_id, tag, namespace, repository) + if not repo_image.security_indexed: + logger.debug('Image %s under repository %s/%s not security indexed', + repo_image.docker_image_id, namespace, repository) return { 'security_indexed': False } - data = _call_security_api('layers/%s/vulnerabilities', tag_image.docker_image_id, + layer_id = '%s.%s' % (repo_image.docker_image_id, repo_image.storage.uuid) + data = _call_security_api('layers/%s/vulnerabilities', layer_id, minimumPriority=args.minimumPriority) return { @@ -94,7 +94,8 @@ class RepositoryImagePackages(RepositoryParamResource): 'security_indexed': False } - data = _call_security_api('layers/%s/packages/diff', repo_image.docker_image_id) + 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, diff --git a/endpoints/secscan.py b/endpoints/secscan.py index 7576318e8..4326ea621 100644 --- a/endpoints/secscan.py +++ b/endpoints/secscan.py @@ -21,5 +21,5 @@ def secscan_notification(): if not layer_ids: return make_response('Okay') - secscan_notification_queue.put(data['Name'], json.dumps(data)) + secscan_notification_queue.put(['notification', data['Name']], json.dumps(data)) return make_response('Okay') diff --git a/static/css/directives/repo-view/repo-panel-tags.css b/static/css/directives/repo-view/repo-panel-tags.css index 6bc6975e1..f2818187b 100644 --- a/static/css/directives/repo-view/repo-panel-tags.css +++ b/static/css/directives/repo-view/repo-panel-tags.css @@ -85,46 +85,34 @@ margin-right: 2px; } -.repo-panel-tags-element .fa-flag { +.repo-panel-tags-element .security-scan-col span { cursor: pointer; } -.repo-panel-tags-element .vuln-name { - +.repo-panel-tags-element .security-scan-col i.fa { + margin-right: 4px; } -.repo-panel-tags-element .vuln-description { - color: #aaa; - font-size: 10px; - white-space: normal; +.repo-panel-tags-element .security-scan-col .scanning { + color: #9B9B9B; + font-size: 12px; } -.repo-panel-tags-element .fa-flag.None { - color: #00CA00; +.repo-panel-tags-element .security-scan-col .no-vulns a { + color: #2FC98E; } -.repo-panel-tags-element .fa-flag.Medium { - color: orange; +.repo-panel-tags-element .security-scan-col .vuln-link, +.repo-panel-tags-element .security-scan-col .vuln-link span { + text-decoration: none !important } -.repo-panel-tags-element .fa-flag.High { - color: red; +.repo-panel-tags-element .security-scan-col .has-vulns.Critical .highest-vuln, +.repo-panel-tags-element .security-scan-col .has-vulns.Defcon1 .highest-vuln { } -.repo-panel-tags-element .vuln-dropdown ul { - min-width: 400px; -} - -@keyframes flickerAnimation { /* flame pulses */ - 0% { opacity:1; } - 50% { opacity:0; } - 100% { opacity:1; } -} - -.repo-panel-tags-element .fa-flag.Critical { - color: red; - opacity:1; - animation: flickerAnimation 1s infinite; +.repo-panel-tags-element .other-vulns { + color: black; } @media (max-width: 767px) { diff --git a/static/css/directives/ui/filter-box.css b/static/css/directives/ui/filter-box.css index 82e43c9c6..836a72b11 100644 --- a/static/css/directives/ui/filter-box.css +++ b/static/css/directives/ui/filter-box.css @@ -15,4 +15,35 @@ margin-right: 10px; margin-bottom: 10px; color: #ccc; +} + +.filter-box.floating { + float: right; + min-width: 300px; + margin-top: 0px; + position: relative; +} + +.filter-box.floating .filter-message { + position: absolute; + left: -200px; + top: 7px; +} + + +@media (max-width: 767px) { + .filter-box.floating { + float: none; + width: 100%; + display: block; + margin-top: 10px; + } + + .filter-box.floating .form-control { + max-width: 100%; + } + + .filter-box.floating .filter-message { + display: none; + } } \ 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 new file mode 100644 index 000000000..c3487d6cb --- /dev/null +++ b/static/css/directives/ui/vulnerability-priority-view.css @@ -0,0 +1,19 @@ +.vulnerability-priority-view-element i.fa { + margin-right: 4px; +} + +.vulnerability-priority-view-element.Unknown, +.vulnerability-priority-view-element.Low, +.vulnerability-priority-view-element.Negligable { + color: #9B9B9B; +} + +.vulnerability-priority-view-element.Medium { + color: #FCA657; +} + +.vulnerability-priority-view-element.High, +.vulnerability-priority-view-element.Critical, +.vulnerability-priority-view-element.Defcon1 { + color: #D64456; +} diff --git a/static/css/pages/image-view.css b/static/css/pages/image-view.css index f91ceacc9..6b5cfa555 100644 --- a/static/css/pages/image-view.css +++ b/static/css/pages/image-view.css @@ -23,3 +23,23 @@ .image-view .co-tab-content h3 { margin-bottom: 20px; } + +.image-view .fa-bug { + margin-right: 4px; +} + +.image-view .co-filter-box { + float: right; + min-width: 300px; + margin-bottom: 10px; +} + +.image-view .co-filter-box .current-filtered { + display: inline-block; + margin-right: 10px; + color: #999; +} + +.image-view .co-filter-box input { + display: inline-block; +} \ No newline at end of file diff --git a/static/directives/repo-view/repo-panel-tags.html b/static/directives/repo-view/repo-panel-tags.html index 7d369f3f9..0f1125d8e 100644 --- a/static/directives/repo-view/repo-panel-tags.html +++ b/static/directives/repo-view/repo-panel-tags.html @@ -81,17 +81,17 @@ style="min-width: 120px;"> Last Modified + + Security Scan + Size - - - - - Unknown - - + - - - - - + + Queued for scan + + + + + + Passed + + + + + + + + + {{ getTagVulnerabilities(tag).highestVulnerability.Count }} + + + + + + {{ getTagVulnerabilities(tag).vulnerabilities.length - getTagVulnerabilities(tag).highestVulnerability.Count }} others + + + + More Info + + + diff --git a/static/directives/vulnerability-priority-view.html b/static/directives/vulnerability-priority-view.html new file mode 100644 index 000000000..23bcce345 --- /dev/null +++ b/static/directives/vulnerability-priority-view.html @@ -0,0 +1,5 @@ + + + + {{ priority }} + \ No newline at end of file diff --git a/static/js/directives/repo-view/repo-panel-tags.js b/static/js/directives/repo-view/repo-panel-tags.js index fcc5d950e..6f80d193f 100644 --- a/static/js/directives/repo-view/repo-panel-tags.js +++ b/static/js/directives/repo-view/repo-panel-tags.js @@ -34,8 +34,8 @@ angular.module('quay').directive('repoPanelTags', function () { $scope.tagHistory = {}; $scope.tagActionHandler = null; $scope.showingHistory = false; - $scope.tagsPerPage = 50; - $scope.tagVulnerabilities = {}; + $scope.tagsPerPage = 25; + $scope.imageVulnerabilities = {}; var setTagState = function() { if (!$scope.repository || !$scope.selectedTags) { return; } @@ -57,7 +57,7 @@ angular.module('quay').directive('repoPanelTags', function () { allTags.push(tagInfo); - if (!$scope.options.tagFilter || tag.indexOf($scope.options.tagFilter) >= 0 || + if (!$scope.options.tagFilter || tagfOf($scope.options.tagFilter) >= 0 || tagInfo.image_id.indexOf($scope.options.tagFilter) >= 0) { tags.push(tagInfo); } @@ -150,51 +150,66 @@ angular.module('quay').directive('repoPanelTags', function () { setTagState(); }); - $scope.loadTagVulnerabilities = function(tag, tagData) { + $scope.loadImageVulnerabilities = function(image_id, imageData) { var params = { - 'tag': tag.name, + 'imageid': image_id, 'repository': $scope.repository.namespace + '/' + $scope.repository.name, }; - ApiService.getRepoTagVulnerabilities(null, params).then(function(resp) { - tagData.indexed = resp.security_indexed; - tagData.loading = false; + ApiService.getRepoImageVulnerabilities(null, params).then(function(resp) { + imageData.security_indexed = resp.security_indexed; + imageData.loading = false; - if (resp.security_indexed) { - tagData.hasVulnerabilities = !!resp.data.Vulnerabilities.length; - tagData.vulnerabilities = resp.data.Vulnerabilities; + if (imageData.security_indexed) { + var vulnerabilities = resp.data.Vulnerabilities; + + imageData.hasVulnerabilities = !!vulnerabilities.length; + imageData.vulnerabilities = vulnerabilities; + + var highest = { + 'Priority': 'Unknown', + 'Count': 0, + 'index': 100000 + }; - var highest = null; resp.data.Vulnerabilities.forEach(function(v) { - if (highest == null || - VulnerabilityService.LEVELS[v.Priority].index < VulnerabilityService.LEVELS[highest.Priority].index) { - highest = v; + if (VulnerabilityService.LEVELS[v.Priority].index < highest.index) { + highest = { + 'Priority': v.Priority, + 'Count': 1, + 'index': VulnerabilityService.LEVELS[v.Priority].index + } + } else if (VulnerabilityService.LEVELS[v.Priority].index == highest.index) { + highest['Count']++; } }); - tagData.highestVulnerability = highest; + imageData.highestVulnerability = highest; } }, function() { - tagData.loading = false; - tagData.hasError = true; + imageData.loading = false; + imageData.hasError = true; }); }; $scope.getTagVulnerabilities = function(tag) { + return $scope.getImageVulnerabilities(tag.image_id); + }; + + $scope.getImageVulnerabilities = function(image_id) { if (!$scope.repository) { return } - var tagName = tag.name; - if (!$scope.tagVulnerabilities[tagName]) { - $scope.tagVulnerabilities[tagName] = { + if (!$scope.imageVulnerabilities[image_id]) { + $scope.imageVulnerabilities[image_id] = { 'loading': true }; - $scope.loadTagVulnerabilities(tag, $scope.tagVulnerabilities[tagName]); + $scope.loadImageVulnerabilities(image_id, $scope.imageVulnerabilities[image_id]); } - return $scope.tagVulnerabilities[tagName]; + return $scope.imageVulnerabilities[image_id]; }; $scope.clearSelectedTags = function() { diff --git a/static/js/directives/ui/vulnerability-priority-view.js b/static/js/directives/ui/vulnerability-priority-view.js new file mode 100644 index 000000000..a257218f5 --- /dev/null +++ b/static/js/directives/ui/vulnerability-priority-view.js @@ -0,0 +1,18 @@ +/** + * An element which displays a priority triangle for vulnerabilities. + */ +angular.module('quay').directive('vulnerabilityPriorityView', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/vulnerability-priority-view.html', + replace: false, + transclude: true, + restrict: 'C', + scope: { + 'priority': '=priority' + }, + controller: function($scope, $element) { + } + }; + return directiveDefinitionObject; +}); \ No newline at end of file diff --git a/static/js/pages/image-view.js b/static/js/pages/image-view.js index d848440f2..22da844a7 100644 --- a/static/js/pages/image-view.js +++ b/static/js/pages/image-view.js @@ -10,11 +10,16 @@ }) }]); - function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, ImageMetadataService) { + function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, ImageMetadataService, VulnerabilityService, Features) { var namespace = $routeParams.namespace; var name = $routeParams.name; var imageid = $routeParams.image; + $scope.options = { + 'vulnFilter': '', + 'packageFilter': '' + }; + var loadImage = function() { var params = { 'repository': namespace + '/' + name, @@ -41,7 +46,7 @@ loadRepository(); $scope.downloadPackages = function() { - if ($scope.packagesResource) { return; } + if (!Features.SECURITY_SCANNER || $scope.packagesResource) { return; } var params = { 'repository': namespace + '/' + name, @@ -53,6 +58,26 @@ }); }; + $scope.loadImageVulnerabilities = function() { + if (!Features.SECURITY_SCANNER || $scope.vulnerabilitiesResource) { return; } + + var params = { + 'repository': namespace + '/' + name, + 'imageid': imageid + }; + + $scope.vulnerabilitiesResource = ApiService.getRepoImageVulnerabilitiesAsResource(params).get(function(resp) { + $scope.vulerabilityInfo = 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); + }); + }); + }; + $scope.downloadChanges = function() { if ($scope.changesResource) { return; } diff --git a/static/partials/image-view.html b/static/partials/image-view.html index ee306150f..6e4b40767 100644 --- a/static/partials/image-view.html +++ b/static/partials/image-view.html @@ -25,8 +25,14 @@ tab-init="downloadChanges()"> + + + + tab-init="downloadPackages()" + quay-show="Features.SECURITY_SCANNER"> @@ -58,9 +64,57 @@ + +
+
+
+ +

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
+
+ Please adjust your filter above. +
+
+
+
+
+ -
+
+
+

Image Packages

This image has not been indexed yet
@@ -75,19 +129,27 @@
- +
- - - + + + - +
Package NamePackage VersionOSPackage NamePackage VersionOS
{{ package.Name }} {{ package.Version }} {{ package.OS }}
+ +
+
No matching packages found
+
+ Please adjust your filter above. +
+
diff --git a/workers/securityworker.py b/workers/securityworker.py index 26d360754..fd0702d81 100644 --- a/workers/securityworker.py +++ b/workers/securityworker.py @@ -256,7 +256,7 @@ class SecurityWorker(Worker): # callback code, etc. try: logger.debug('Loading vulnerabilities for layer %s', img['image_id']) - response = secscan_api.call('layers/%s/vulnerabilities', request['ID']) + response = secscan_api.call('layers/%s/vulnerabilities', None, request['ID']) except requests.exceptions.Timeout: logger.debug('Timeout when calling Sec') continue