Merge pull request #858 from coreos-inc/vulnerability-tool-updateui

New Quay Sec UI and fix some small bugs
This commit is contained in:
josephschorr 2015-11-11 18:17:58 -05:00
commit 6970b0685e
13 changed files with 307 additions and 115 deletions

View file

@ -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/<repopath:repository>/tag/<tag>/vulnerabilities')
@resource('/v1/repository/<repopath:repository>/image/<imageid>/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,

View file

@ -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')

View file

@ -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) {

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -81,17 +81,17 @@
style="min-width: 120px;">
<a href="javascript:void(0)" ng-click="orderBy('last_modified_datetime')">Last Modified</a>
</td>
<td class="hidden-xs"
ng-class="tablePredicateClass('security_scanned', options.predicate, options.reverse)"
style="min-width: 120px;"
quay-require="['SECURITY_SCANNER']">
Security Scan
</td>
<td class="hidden-xs"
ng-class="tablePredicateClass('size', options.predicate, options.reverse)"
style="min-width: 62px;">
<a href="javascript:void(0)" ng-click="orderBy('size')">Size</a>
</td>
<td ng-class="tablePredicateClass('vuln_level', options.predicate, options.reverse)"
style="width: 60px;">
<a href="javascript:void(0)" ng-click="orderBy('vuln_level')">
<i class="fa fa-flag"></i>
</a>
</td>
<td class="hidden-xs"
ng-class="tablePredicateClass('image_id', options.predicate, options.reverse)"
colspan="{{ imageTracks.length + 1 }}"
@ -113,43 +113,51 @@
<span am-time-ago="tag.last_modified" bo-if="tag.last_modified"></span>
<span bo-if="!tag.last_modified">Unknown</span>
</td>
<td class="hidden-xs" bo-text="tag.size | bytes"></td>
<td>
<td quay-require="['SECURITY_SCANNER']" class="security-scan-col">
<span class="cor-loader-inline" ng-if="getTagVulnerabilities(tag).loading"></span>
<span ng-if="!getTagVulnerabilities(tag).loading">
<i class="fa fa-flag-o" ng-if="!getTagVulnerabilities(tag).indexed"
data-title="Image is currently being checked for vulnerabilities"
bs-tooltip>
</i>
<i class="fa fa-flag None"
ng-if="getTagVulnerabilities(tag).indexed && !getTagVulnerabilities(tag).hasVulnerabilities"
data-title="Image has no vulnerabilities"
bs-tooltip>
</i>
<div class="dropdown vuln-dropdown" style="text-align: left;"
ng-if="getTagVulnerabilities(tag).indexed && getTagVulnerabilities(tag).hasVulnerabilities">
<i class="fa fa-flag"
data-title="Image has vulnerabilities"
data-toggle="dropdown"
ng-class="getTagVulnerabilities(tag).highestVulnerability.Priority"
bs-tooltip>
</i>
<ul class="dropdown-menu pull-right">
<li ng-repeat="vuln in getTagVulnerabilities(tag).vulnerabilities">
<a href="{{ vuln.Link }}" target="_new">
<div class="vuln-name">
<i class="fa fa-flag" bo-class="vuln.Priority"></i>
{{ vuln.ID }}
</div>
<div class="vuln-description">
{{ vuln.Description }}
</div>
</a>
</li>
</ul>
</div>
<!-- Scanning -->
<span class="scanning" ng-if="!getTagVulnerabilities(tag).security_indexed"
data-title="The image for this tag is queued to be scanned for vulnerabilities"
bs-tooltip>Queued for scan</span>
<!-- No Vulns -->
<span class="no-vulns"
ng-if="getTagVulnerabilities(tag).security_indexed && !getTagVulnerabilities(tag).hasVulnerabilities"
data-title="The image for this tag has no vulnerabilities as found in our database"
bs-tooltip
bindonce>
<a bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ tag.image_id }}?tab=security">
<i class="fa fa-check-circle"></i>
Passed
</a>
</span>
<!-- Vulns -->
<span ng-if="getTagVulnerabilities(tag).security_indexed && getTagVulnerabilities(tag).hasVulnerabilities"
ng-class="getTagVulnerabilities(tag).highestVulnerability.Priority"
class="has-vulns" bindonce>
<a class="vuln-link" bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ tag.image_id }}?tab=security"
data-title="The image for this tag has {{ getTagVulnerabilities(tag).highestVulnerability.Count }} {{ getTagVulnerabilities(tag).highestVulnerability.Priority }} level vulnerabilities"
bs-tooltip>
<span class="highest-vuln">
<span class="vulnerability-priority-view" priority="getTagVulnerabilities(tag).highestVulnerability.Priority">
{{ getTagVulnerabilities(tag).highestVulnerability.Count }}
</span>
</span>
<span ng-if="getTagVulnerabilities(tag).vulnerabilities.length - getTagVulnerabilities(tag).highestVulnerability.Count > 0"
class="other-vulns">
+ {{ getTagVulnerabilities(tag).vulnerabilities.length - getTagVulnerabilities(tag).highestVulnerability.Count }} others
</span>
</a>
<a bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ tag.image_id }}?tab=security" style="display: inline-block; margin-left: 6px;">
More Info
</a>
</span>
</span>
</td>
<td class="hidden-xs" bo-text="tag.size | bytes"></td>
<td class="hidden-xs image-id-col">
<span class="image-link" repository="repository" image-id="tag.image_id"></span>
</td>

View file

@ -0,0 +1,5 @@
<span class="vulnerability-priority-view-element" ng-class="priority">
<i class="fa fa-exclamation-triangle"></i>
<span ng-transclude/>
{{ priority }}
</span>

View file

@ -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() {

View file

@ -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;
});

View file

@ -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; }

View file

@ -25,8 +25,14 @@
tab-init="downloadChanges()">
<i class="fa fa-code-fork"></i>
</span>
<span class="cor-tab" tab-title="Security Scan" tab-target="#security"
tab-init="loadImageVulnerabilities()"
quay-show="Features.SECURITY_SCANNER">
<i class="fa fa-bug"></i>
</span>
<span class="cor-tab" tab-title="Packages" tab-target="#packages"
tab-init="downloadPackages()">
tab-init="downloadPackages()"
quay-show="Features.SECURITY_SCANNER">
<i class="fa ci-package"></i>
</span>
</div> <!-- /cor-tabs -->
@ -58,9 +64,57 @@
</div>
</div>
<!-- Security -->
<div id="security" class="tab-pane" quay-require="['SECURITY_SCANNER']">
<div class="resource-view" resource="vulnerabilitiesResource" error-message="'Could not load security information for image'">
<div class="filter-box floating" collection="vulnerabilities" filter-model="options.vulnFilter" filter-name="Vulnerabilities" ng-if="vulerabilityInfo.security_indexed && vulnerabilities.length"></div>
<h3>Image Security</h3>
<div class="empty" ng-if="!vulerabilityInfo.security_indexed">
<div class="empty-primary-msg">This image has not been indexed yet</div>
<div class="empty-secondary-msg">
Please try again in a few minutes.
</div>
</div>
<div class="empty" ng-if="vulerabilityInfo.security_indexed && !vulnerabilities.length">
<div class="empty-primary-msg">This image contains no recognized security vulnerabilities</div>
<div class="empty-secondary-msg">
Quay currently indexes Debian, Red Hat and Ubuntu packages.
</div>
</div>
<div ng-if="vulerabilityInfo.security_indexed && vulnerabilities.length">
<table class="co-table">
<thead>
<td style="width: 200px;">Vulnerability</td>
<td style="width: 200px;">Priority</td>
<td>Description</td>
</thead>
<tr ng-repeat="vulnerability in vulnerabilities | filter:options.vulnFilter | orderBy:'index'">
<td><a href="{{ vulnerability.Link }}" target="_blank">{{ vulnerability.ID }}</a></td>
<td>
<span class="vulnerability-priority-view" priority="vulnerability.Priority"></span>
<td>{{ vulnerability.Description }}</td>
</tr>
</table>
<div class="empty" ng-if="(vulnerabilities | filter:options.vulnFilter).length == 0"
style="margin-top: 20px;">
<div class="empty-primary-msg">No matching vulnerabilities found</div>
<div class="empty-secondary-msg">
Please adjust your filter above.
</div>
</div>
</div>
</div>
</div>
<!-- Packages -->
<div id="packages" class="tab-pane">
<div id="packages" class="tab-pane" quay-require="['SECURITY_SCANNER']">
<div class="resource-view" resource="packagesResource" error-message="'Could not load image packages'">
<div class="filter-box floating" collection="packages.data.Packages" filter-model="options.packageFilter" filter-name="Packages" ng-if="packages.security_indexed && packages.data.Packages.length"></div>
<h3>Image Packages</h3>
<div class="empty" ng-if="!packages.security_indexed">
<div class="empty-primary-msg">This image has not been indexed yet</div>
@ -75,19 +129,27 @@
</div>
</div>
<table class="table" ng-if="packages.security_indexed && packages.data.Packages.length">
<table class="co-table" ng-if="packages.security_indexed && packages.data.Packages.length">
<thead>
<th>Package Name</th>
<th>Package Version</th>
<th>OS</th>
<td>Package Name</td>
<td>Package Version</td>
<td>OS</td>
</thead>
<tr ng-repeat="package in packages.data.Packages | orderBy:'Name'">
<tr ng-repeat="package in packages.data.Packages | filter:options.packageFilter | orderBy:'Name'">
<td>{{ package.Name }}</td>
<td>{{ package.Version }}</td>
<td>{{ package.OS }}</td>
</tr>
</table>
<div class="empty" ng-if="(packages.data.Packages | filter:options.packageFilter).length == 0"
style="margin-top: 20px;">
<div class="empty-primary-msg">No matching packages found</div>
<div class="empty-secondary-msg">
Please adjust your filter above.
</div>
</div>
</div>
</div>
</div>

View file

@ -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