Merge pull request #858 from coreos-inc/vulnerability-tool-updateui
New Quay Sec UI and fix some small bugs
This commit is contained in:
commit
6970b0685e
13 changed files with 307 additions and 115 deletions
|
@ -18,7 +18,7 @@ logger = logging.getLogger(__name__)
|
||||||
def _call_security_api(relative_url, *args, **kwargs):
|
def _call_security_api(relative_url, *args, **kwargs):
|
||||||
""" Issues an HTTP call to the sec API at the given relative URL. """
|
""" Issues an HTTP call to the sec API at the given relative URL. """
|
||||||
try:
|
try:
|
||||||
response = secscan_api.call(relative_url, body=None, *args, **kwargs)
|
response = secscan_api.call(relative_url, None, *args, **kwargs)
|
||||||
except requests.exceptions.Timeout:
|
except requests.exceptions.Timeout:
|
||||||
raise DownstreamIssue(payload=dict(message='API call timed out'))
|
raise DownstreamIssue(payload=dict(message='API call timed out'))
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError:
|
||||||
|
@ -40,32 +40,32 @@ def _call_security_api(relative_url, *args, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
@show_if(features.SECURITY_SCANNER)
|
@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('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
@path_param('tag', 'The name of the tag')
|
@path_param('imageid', 'The image ID')
|
||||||
class RepositoryTagVulnerabilities(RepositoryParamResource):
|
class RepositoryImageVulnerabilities(RepositoryParamResource):
|
||||||
""" Operations for managing the vulnerabilities in a repository tag. """
|
""" Operations for managing the vulnerabilities in a repository image. """
|
||||||
|
|
||||||
@require_repo_read
|
@require_repo_read
|
||||||
@nickname('getRepoTagVulnerabilities')
|
@nickname('getRepoImageVulnerabilities')
|
||||||
@parse_args
|
@parse_args
|
||||||
@query_param('minimumPriority', 'Minimum vulnerability priority', type=str,
|
@query_param('minimumPriority', 'Minimum vulnerability priority', type=str,
|
||||||
default='Low')
|
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. """
|
""" Fetches the vulnerabilities (if any) for a repository tag. """
|
||||||
try:
|
repo_image = model.image.get_repo_image(namespace, repository, imageid)
|
||||||
tag_image = model.tag.get_tag_image(namespace, repository, tag)
|
if repo_image is None:
|
||||||
except model.DataModelException:
|
|
||||||
raise NotFound()
|
raise NotFound()
|
||||||
|
|
||||||
if not tag_image.security_indexed:
|
if not repo_image.security_indexed:
|
||||||
logger.debug('Image %s for tag %s under repository %s/%s not security indexed',
|
logger.debug('Image %s under repository %s/%s not security indexed',
|
||||||
tag_image.docker_image_id, tag, namespace, repository)
|
repo_image.docker_image_id, namespace, repository)
|
||||||
return {
|
return {
|
||||||
'security_indexed': False
|
'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)
|
minimumPriority=args.minimumPriority)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -94,7 +94,8 @@ class RepositoryImagePackages(RepositoryParamResource):
|
||||||
'security_indexed': False
|
'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 {
|
return {
|
||||||
'security_indexed': True,
|
'security_indexed': True,
|
||||||
|
|
|
@ -21,5 +21,5 @@ def secscan_notification():
|
||||||
if not layer_ids:
|
if not layer_ids:
|
||||||
return make_response('Okay')
|
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')
|
return make_response('Okay')
|
||||||
|
|
|
@ -85,46 +85,34 @@
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-panel-tags-element .fa-flag {
|
.repo-panel-tags-element .security-scan-col span {
|
||||||
cursor: pointer;
|
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 {
|
.repo-panel-tags-element .security-scan-col .scanning {
|
||||||
color: #aaa;
|
color: #9B9B9B;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
white-space: normal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-panel-tags-element .fa-flag.None {
|
.repo-panel-tags-element .security-scan-col .no-vulns a {
|
||||||
color: #00CA00;
|
color: #2FC98E;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-panel-tags-element .fa-flag.Medium {
|
.repo-panel-tags-element .security-scan-col .vuln-link,
|
||||||
color: orange;
|
.repo-panel-tags-element .security-scan-col .vuln-link span {
|
||||||
|
text-decoration: none !important
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-panel-tags-element .fa-flag.High {
|
.repo-panel-tags-element .security-scan-col .has-vulns.Critical .highest-vuln,
|
||||||
color: red;
|
.repo-panel-tags-element .security-scan-col .has-vulns.Defcon1 .highest-vuln {
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-panel-tags-element .vuln-dropdown ul {
|
.repo-panel-tags-element .other-vulns {
|
||||||
min-width: 400px;
|
color: black;
|
||||||
}
|
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
|
|
|
@ -15,4 +15,35 @@
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
color: #ccc;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
19
static/css/directives/ui/vulnerability-priority-view.css
Normal file
19
static/css/directives/ui/vulnerability-priority-view.css
Normal 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;
|
||||||
|
}
|
|
@ -23,3 +23,23 @@
|
||||||
.image-view .co-tab-content h3 {
|
.image-view .co-tab-content h3 {
|
||||||
margin-bottom: 20px;
|
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;
|
||||||
|
}
|
|
@ -81,17 +81,17 @@
|
||||||
style="min-width: 120px;">
|
style="min-width: 120px;">
|
||||||
<a href="javascript:void(0)" ng-click="orderBy('last_modified_datetime')">Last Modified</a>
|
<a href="javascript:void(0)" ng-click="orderBy('last_modified_datetime')">Last Modified</a>
|
||||||
</td>
|
</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"
|
<td class="hidden-xs"
|
||||||
ng-class="tablePredicateClass('size', options.predicate, options.reverse)"
|
ng-class="tablePredicateClass('size', options.predicate, options.reverse)"
|
||||||
style="min-width: 62px;">
|
style="min-width: 62px;">
|
||||||
<a href="javascript:void(0)" ng-click="orderBy('size')">Size</a>
|
<a href="javascript:void(0)" ng-click="orderBy('size')">Size</a>
|
||||||
</td>
|
</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"
|
<td class="hidden-xs"
|
||||||
ng-class="tablePredicateClass('image_id', options.predicate, options.reverse)"
|
ng-class="tablePredicateClass('image_id', options.predicate, options.reverse)"
|
||||||
colspan="{{ imageTracks.length + 1 }}"
|
colspan="{{ imageTracks.length + 1 }}"
|
||||||
|
@ -113,43 +113,51 @@
|
||||||
<span am-time-ago="tag.last_modified" bo-if="tag.last_modified"></span>
|
<span am-time-ago="tag.last_modified" bo-if="tag.last_modified"></span>
|
||||||
<span bo-if="!tag.last_modified">Unknown</span>
|
<span bo-if="!tag.last_modified">Unknown</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="hidden-xs" bo-text="tag.size | bytes"></td>
|
<td quay-require="['SECURITY_SCANNER']" class="security-scan-col">
|
||||||
<td>
|
|
||||||
<span class="cor-loader-inline" ng-if="getTagVulnerabilities(tag).loading"></span>
|
<span class="cor-loader-inline" ng-if="getTagVulnerabilities(tag).loading"></span>
|
||||||
<span ng-if="!getTagVulnerabilities(tag).loading">
|
<span ng-if="!getTagVulnerabilities(tag).loading">
|
||||||
<i class="fa fa-flag-o" ng-if="!getTagVulnerabilities(tag).indexed"
|
<!-- Scanning -->
|
||||||
data-title="Image is currently being checked for vulnerabilities"
|
<span class="scanning" ng-if="!getTagVulnerabilities(tag).security_indexed"
|
||||||
bs-tooltip>
|
data-title="The image for this tag is queued to be scanned for vulnerabilities"
|
||||||
</i>
|
bs-tooltip>Queued for scan</span>
|
||||||
<i class="fa fa-flag None"
|
|
||||||
ng-if="getTagVulnerabilities(tag).indexed && !getTagVulnerabilities(tag).hasVulnerabilities"
|
<!-- No Vulns -->
|
||||||
data-title="Image has no vulnerabilities"
|
<span class="no-vulns"
|
||||||
bs-tooltip>
|
ng-if="getTagVulnerabilities(tag).security_indexed && !getTagVulnerabilities(tag).hasVulnerabilities"
|
||||||
</i>
|
data-title="The image for this tag has no vulnerabilities as found in our database"
|
||||||
<div class="dropdown vuln-dropdown" style="text-align: left;"
|
bs-tooltip
|
||||||
ng-if="getTagVulnerabilities(tag).indexed && getTagVulnerabilities(tag).hasVulnerabilities">
|
bindonce>
|
||||||
<i class="fa fa-flag"
|
<a bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ tag.image_id }}?tab=security">
|
||||||
data-title="Image has vulnerabilities"
|
<i class="fa fa-check-circle"></i>
|
||||||
data-toggle="dropdown"
|
Passed
|
||||||
ng-class="getTagVulnerabilities(tag).highestVulnerability.Priority"
|
</a>
|
||||||
bs-tooltip>
|
</span>
|
||||||
</i>
|
|
||||||
<ul class="dropdown-menu pull-right">
|
<!-- Vulns -->
|
||||||
<li ng-repeat="vuln in getTagVulnerabilities(tag).vulnerabilities">
|
<span ng-if="getTagVulnerabilities(tag).security_indexed && getTagVulnerabilities(tag).hasVulnerabilities"
|
||||||
<a href="{{ vuln.Link }}" target="_new">
|
ng-class="getTagVulnerabilities(tag).highestVulnerability.Priority"
|
||||||
<div class="vuln-name">
|
class="has-vulns" bindonce>
|
||||||
<i class="fa fa-flag" bo-class="vuln.Priority"></i>
|
<a class="vuln-link" bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ tag.image_id }}?tab=security"
|
||||||
{{ vuln.ID }}
|
data-title="The image for this tag has {{ getTagVulnerabilities(tag).highestVulnerability.Count }} {{ getTagVulnerabilities(tag).highestVulnerability.Priority }} level vulnerabilities"
|
||||||
</div>
|
bs-tooltip>
|
||||||
<div class="vuln-description">
|
<span class="highest-vuln">
|
||||||
{{ vuln.Description }}
|
<span class="vulnerability-priority-view" priority="getTagVulnerabilities(tag).highestVulnerability.Priority">
|
||||||
</div>
|
{{ getTagVulnerabilities(tag).highestVulnerability.Count }}
|
||||||
</a>
|
</span>
|
||||||
</li>
|
</span>
|
||||||
</ul>
|
|
||||||
</div>
|
<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>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="hidden-xs" bo-text="tag.size | bytes"></td>
|
||||||
<td class="hidden-xs image-id-col">
|
<td class="hidden-xs image-id-col">
|
||||||
<span class="image-link" repository="repository" image-id="tag.image_id"></span>
|
<span class="image-link" repository="repository" image-id="tag.image_id"></span>
|
||||||
</td>
|
</td>
|
||||||
|
|
5
static/directives/vulnerability-priority-view.html
Normal file
5
static/directives/vulnerability-priority-view.html
Normal 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>
|
|
@ -34,8 +34,8 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
$scope.tagHistory = {};
|
$scope.tagHistory = {};
|
||||||
$scope.tagActionHandler = null;
|
$scope.tagActionHandler = null;
|
||||||
$scope.showingHistory = false;
|
$scope.showingHistory = false;
|
||||||
$scope.tagsPerPage = 50;
|
$scope.tagsPerPage = 25;
|
||||||
$scope.tagVulnerabilities = {};
|
$scope.imageVulnerabilities = {};
|
||||||
|
|
||||||
var setTagState = function() {
|
var setTagState = function() {
|
||||||
if (!$scope.repository || !$scope.selectedTags) { return; }
|
if (!$scope.repository || !$scope.selectedTags) { return; }
|
||||||
|
@ -57,7 +57,7 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
|
|
||||||
allTags.push(tagInfo);
|
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) {
|
tagInfo.image_id.indexOf($scope.options.tagFilter) >= 0) {
|
||||||
tags.push(tagInfo);
|
tags.push(tagInfo);
|
||||||
}
|
}
|
||||||
|
@ -150,51 +150,66 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
setTagState();
|
setTagState();
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.loadTagVulnerabilities = function(tag, tagData) {
|
$scope.loadImageVulnerabilities = function(image_id, imageData) {
|
||||||
var params = {
|
var params = {
|
||||||
'tag': tag.name,
|
'imageid': image_id,
|
||||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
ApiService.getRepoTagVulnerabilities(null, params).then(function(resp) {
|
ApiService.getRepoImageVulnerabilities(null, params).then(function(resp) {
|
||||||
tagData.indexed = resp.security_indexed;
|
imageData.security_indexed = resp.security_indexed;
|
||||||
tagData.loading = false;
|
imageData.loading = false;
|
||||||
|
|
||||||
if (resp.security_indexed) {
|
if (imageData.security_indexed) {
|
||||||
tagData.hasVulnerabilities = !!resp.data.Vulnerabilities.length;
|
var vulnerabilities = resp.data.Vulnerabilities;
|
||||||
tagData.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) {
|
resp.data.Vulnerabilities.forEach(function(v) {
|
||||||
if (highest == null ||
|
if (VulnerabilityService.LEVELS[v.Priority].index < highest.index) {
|
||||||
VulnerabilityService.LEVELS[v.Priority].index < VulnerabilityService.LEVELS[highest.Priority].index) {
|
highest = {
|
||||||
highest = v;
|
'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() {
|
}, function() {
|
||||||
tagData.loading = false;
|
imageData.loading = false;
|
||||||
tagData.hasError = true;
|
imageData.hasError = true;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.getTagVulnerabilities = function(tag) {
|
$scope.getTagVulnerabilities = function(tag) {
|
||||||
|
return $scope.getImageVulnerabilities(tag.image_id);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.getImageVulnerabilities = function(image_id) {
|
||||||
if (!$scope.repository) {
|
if (!$scope.repository) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var tagName = tag.name;
|
if (!$scope.imageVulnerabilities[image_id]) {
|
||||||
if (!$scope.tagVulnerabilities[tagName]) {
|
$scope.imageVulnerabilities[image_id] = {
|
||||||
$scope.tagVulnerabilities[tagName] = {
|
|
||||||
'loading': true
|
'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() {
|
$scope.clearSelectedTags = function() {
|
||||||
|
|
18
static/js/directives/ui/vulnerability-priority-view.js
Normal file
18
static/js/directives/ui/vulnerability-priority-view.js
Normal 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;
|
||||||
|
});
|
|
@ -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 namespace = $routeParams.namespace;
|
||||||
var name = $routeParams.name;
|
var name = $routeParams.name;
|
||||||
var imageid = $routeParams.image;
|
var imageid = $routeParams.image;
|
||||||
|
|
||||||
|
$scope.options = {
|
||||||
|
'vulnFilter': '',
|
||||||
|
'packageFilter': ''
|
||||||
|
};
|
||||||
|
|
||||||
var loadImage = function() {
|
var loadImage = function() {
|
||||||
var params = {
|
var params = {
|
||||||
'repository': namespace + '/' + name,
|
'repository': namespace + '/' + name,
|
||||||
|
@ -41,7 +46,7 @@
|
||||||
loadRepository();
|
loadRepository();
|
||||||
|
|
||||||
$scope.downloadPackages = function() {
|
$scope.downloadPackages = function() {
|
||||||
if ($scope.packagesResource) { return; }
|
if (!Features.SECURITY_SCANNER || $scope.packagesResource) { return; }
|
||||||
|
|
||||||
var params = {
|
var params = {
|
||||||
'repository': namespace + '/' + name,
|
'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() {
|
$scope.downloadChanges = function() {
|
||||||
if ($scope.changesResource) { return; }
|
if ($scope.changesResource) { return; }
|
||||||
|
|
||||||
|
|
|
@ -25,8 +25,14 @@
|
||||||
tab-init="downloadChanges()">
|
tab-init="downloadChanges()">
|
||||||
<i class="fa fa-code-fork"></i>
|
<i class="fa fa-code-fork"></i>
|
||||||
</span>
|
</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"
|
<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>
|
<i class="fa ci-package"></i>
|
||||||
</span>
|
</span>
|
||||||
</div> <!-- /cor-tabs -->
|
</div> <!-- /cor-tabs -->
|
||||||
|
@ -58,9 +64,57 @@
|
||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- 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="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>
|
<h3>Image Packages</h3>
|
||||||
<div class="empty" ng-if="!packages.security_indexed">
|
<div class="empty" ng-if="!packages.security_indexed">
|
||||||
<div class="empty-primary-msg">This image has not been indexed yet</div>
|
<div class="empty-primary-msg">This image has not been indexed yet</div>
|
||||||
|
@ -75,19 +129,27 @@
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<thead>
|
||||||
<th>Package Name</th>
|
<td>Package Name</td>
|
||||||
<th>Package Version</th>
|
<td>Package Version</td>
|
||||||
<th>OS</th>
|
<td>OS</td>
|
||||||
</thead>
|
</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.Name }}</td>
|
||||||
<td>{{ package.Version }}</td>
|
<td>{{ package.Version }}</td>
|
||||||
<td>{{ package.OS }}</td>
|
<td>{{ package.OS }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -256,7 +256,7 @@ class SecurityWorker(Worker):
|
||||||
# callback code, etc.
|
# callback code, etc.
|
||||||
try:
|
try:
|
||||||
logger.debug('Loading vulnerabilities for layer %s', img['image_id'])
|
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:
|
except requests.exceptions.Timeout:
|
||||||
logger.debug('Timeout when calling Sec')
|
logger.debug('Timeout when calling Sec')
|
||||||
continue
|
continue
|
||||||
|
|
Reference in a new issue