Merge pull request #865 from coreos-inc/vulnerability-tool-uipt2

Vulnerability UI part 2
This commit is contained in:
josephschorr 2015-11-12 17:00:24 -05:00
commit 7860122028
23 changed files with 419 additions and 73 deletions

View file

@ -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/<repopath:repository>/image/<imageid>/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,
}

View file

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

View file

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

View file

@ -20,7 +20,7 @@
.filter-box.floating {
float: right;
min-width: 300px;
margin-top: 0px;
margin-top: 15px;
position: relative;
}

View file

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

View file

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

View file

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

View file

@ -65,7 +65,25 @@
<!-- Pull Controls -->
<div class="right-controls hidden-sm hidden-xs">
Pull Full Repository: <div class="copy-box" hovering-message="true" value="pullCommand"></div>
<div class="right-pull-controls">
<div>Pull this container with the following Docker command:</div>
<div class="copy-box" hovering-message="true" value="pullCommand"></div>
</div>
<div class="right-sec-controls" quay-show="repository.can_admin && Features.SECURITY_SCANNER">
<span class="sec-logo">
<img class="lock" src="/static/img/lock.svg">
<img class="scan" src="/static/img/scan.svg">
</span>
<b>Automated Security Scanning (Preview)</b>
<div>Continually scanning this repository for 17K+ known vulnerabilities. <a href="http://blog.quay.io/security-scanning-beta" target="_blank">Read more about this feature</a>.</div>
<div class="configure-alerts" ng-if="!hasEvents">
<a href="/repository/{{ repository.namespace }}/{{ repository.name }}?tab=settings&add_event=vulnerability_found"><i class="fa fa-bell-o"></i>Configure Vulnerability Alerts</a>
</div>
<div class="repository-events-summary" is-enabled="repository.can_admin" repository="repository"
event-filter="vulnerability_found" has-events="hasEvents"></div>
</div>
</div>
<h4 style="font-size:20px;">Description</h4>

View file

@ -115,15 +115,26 @@
</td>
<td quay-require="['SECURITY_SCANNER']" class="security-scan-col">
<span class="cor-loader-inline" ng-if="getTagVulnerabilities(tag).loading"></span>
<span class="vuln-load-error" ng-if="getTagVulnerabilities(tag).hasError">
Could not load security information
</span>
<span ng-if="!getTagVulnerabilities(tag).loading">
<!-- Scanning -->
<span class="scanning" ng-if="!getTagVulnerabilities(tag).security_indexed"
<!-- Queued -->
<span class="scanning" ng-if="getTagVulnerabilities(tag).status == 'queued'"
data-title="The image for this tag is queued to be scanned for vulnerabilities"
bs-tooltip>Queued for scan</span>
<!-- Scan Failed -->
<span class="failed-scan" ng-if="getTagVulnerabilities(tag).status == 'failed'"
data-title="The image for this tag could not be scanned for vulnerabilities"
bs-tooltip>
<i class="fa fa-times-circle"></i>
Failed to scan
</span>
<!-- No Vulns -->
<span class="no-vulns"
ng-if="getTagVulnerabilities(tag).security_indexed && !getTagVulnerabilities(tag).hasVulnerabilities"
ng-if="getTagVulnerabilities(tag).status == 'scanned' && !getTagVulnerabilities(tag).hasVulnerabilities"
data-title="The image for this tag has no vulnerabilities as found in our database"
bs-tooltip
bindonce>
@ -134,7 +145,7 @@
</span>
<!-- Vulns -->
<span ng-if="getTagVulnerabilities(tag).security_indexed && getTagVulnerabilities(tag).hasVulnerabilities"
<span ng-if="getTagVulnerabilities(tag).status == 'scanned' && 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"

View file

@ -0,0 +1,22 @@
<div class="repository-events-summary-element">
<div class="resource-view" resource="notificationsResource"
error-message="'Could not load repository events'">
<ul class="summary-list">
<li ng-repeat="notification in notifications">
<i class="fa fa-lg" ng-class="getMethodInfo(notification).icon"></i>
{{ getMethodInfo(notification).title }} for
<ul class="notification-event-fields" ng-if="getEventInfo(notification).fields.length">
<li ng-repeat="field in getEventInfo(notification).fields">
{{ field.title }} of
<span ng-switch on="field.type">
<span ng-switch-when="enum">
{{ findEnumValue(field.values, notification.event_config[field.name]).title }}
</span>
</span>
</li>
</ul>
</li>
</ul>
</div>
</div>

View file

@ -100,5 +100,6 @@
<div class="create-external-notification-dialog"
repository="repository"
counter="showNewNotificationCounter"
default-data="newNotificationData"
notification-created="handleNotificationCreated(notification)"></div>
</div>

17
static/img/lock.svg Normal file
View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="13px" height="19px" viewBox="0 0 13 19" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.4.1 (15681) - http://www.bohemiancoding.com/sketch -->
<title>Artboard 1</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="Artboard-1" sketch:type="MSArtboardGroup" stroke-width="2" stroke="#1D3447">
<g id="Oval-121-+-Rectangle-443" sketch:type="MSLayerGroup" transform="translate(0.000000, 1.000000)">
<g sketch:type="MSShapeGroup">
<path d="M9.75,3.41251359 C9.75,1.5540428 8.29492544,0.0474545455 6.5,0.0474545455 C4.70507456,0.0474545455 3.25,1.5540428 3.25,3.41251359 L3.25,7.41109091 L9.75,7.41109091 L9.75,3.41251359 Z" id="Oval-121"></path>
<rect id="Rectangle-443" fill="#2FC98E" x="0" y="5.77472727" width="13" height="11.4545455" rx="2"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

14
static/img/scan.svg Normal file
View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="33.223px" height="31px" viewBox="0 0 33.223 31" enable-background="new 0 0 33.223 31" xml:space="preserve">
<polygon fill-rule="evenodd" clip-rule="evenodd" fill="#1E3547" points="31.311,16.034 33.223,9.604 27.315,10.646 "/>
<g>
<path fill="#1E3547" d="M3.552,11.75c1.638-5.762,6.937-10,13.217-10s11.579,4.238,13.217,10h1.805C30.107,5.013,24.021,0,16.769,0
S3.43,5.013,1.747,11.75H3.552z"/>
<path fill="#1E3547" d="M30.06,18.96c-1.54,5.909-6.907,10.29-13.292,10.29S5.018,24.87,3.477,18.96H1.672
C3.25,25.845,9.413,31,16.769,31s13.519-5.155,15.097-12.04H30.06z"/>
</g>
<polygon fill-rule="evenodd" clip-rule="evenodd" fill="#1E3547" points="1.913,14.434 0,20.864 5.909,19.822 "/>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

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

View file

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

View file

@ -11,7 +11,8 @@ angular.module('quay').directive('createExternalNotificationDialog', function ()
scope: {
'repository': '=repository',
'counter': '=counter',
'notificationCreated': '&notificationCreated'
'notificationCreated': '&notificationCreated',
'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({});
}
});

View file

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

View file

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

View file

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

View file

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

View file

@ -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,
}
]

View file

@ -67,45 +67,67 @@
<!-- 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>
<div class="col-md-9">
<div class="filter-box floating" collection="vulnerabilities" filter-model="options.vulnFilter" filter-name="Vulnerabilities" ng-if="vulnerabilityInfo.status == 'scanned' && 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>
<h3>Image Security</h3>
<div class="empty" ng-if="vulnerabilityInfo.status == 'queued'">
<div class="empty-primary-msg">This image has not been indexed yet</div>
<div class="empty-secondary-msg">
Please adjust your filter above.
Please try again in a few minutes.
</div>
</div>
<div class="empty" ng-if="vulnerabilityInfo.status == 'failed'">
<div class="empty-primary-msg">This image could not be indexed</div>
<div class="empty-secondary-msg">
Our security scanner was unable to index this image.
</div>
</div>
<div class="empty" ng-if="vulnerabilityInfo.status == 'scanned' && !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="vulnerabilityInfo.status == 'scanned' && 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 class="level-col col-md-3 hidden-sm hidden-xs">
<h4>Priority Guide</h4>
<ul class="levels">
<li ng-repeat="level in VulnerabilityLevels | orderBy: 'index'">
<div class="vulnerability-priority-view" priority="level.title"></div>
<div class="description">
{{ level.description }}
</div>
</li>
</ul>
</div>
</div>
</div>
@ -113,23 +135,24 @@
<!-- Packages -->
<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>
<div class="filter-box floating" collection="packages.data.Packages" filter-model="options.packageFilter" filter-name="Packages" ng-if="packages.status == 'scanned' && packages.data.Packages.length"></div>
<h3>Image Packages</h3>
<div class="empty" ng-if="!packages.security_indexed">
<div class="empty" ng-if="packages.status == 'queued'">
<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="packages.security_indexed && !packages.data.Packages.length">
<div class="empty-primary-msg">This image contains no recognized packages</div>
<div class="empty" ng-if="packages.status == 'failed'">
<div class="empty-primary-msg">This image could not be indexed</div>
<div class="empty-secondary-msg">
Quay currently indexes Debian, Red Hat and Ubuntu packages.
Our security scanner was unable to index this image.
</div>
</div>
<table class="co-table" ng-if="packages.security_indexed && packages.data.Packages.length">
<table class="co-table" ng-if="packages.status == 'scanned'">
<thead>
<td>Package Name</td>
<td>Package Version</td>
@ -146,7 +169,7 @@
<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">
<div class="empty-secondary-msg" ng-if="options.packageFilter">
Please adjust your filter above.
</div>
</div>

View file

@ -17,7 +17,8 @@
<div class="cor-tab-panel">
<div class="cor-tabs">
<span class="cor-tab" tab-active="true" tab-title="Information" tab-target="#info">
<span class="cor-tab" tab-active="true" tab-title="Information" tab-target="#info"
tab-init="showInfo()">
<i class="fa fa-info-circle"></i>
</span>
@ -56,7 +57,8 @@
<div id="info" class="tab-pane active">
<div class="repo-panel-info"
repository="viewScope.repository"
builds="viewScope.builds"></div>
builds="viewScope.builds"
is-enabled="infoShown"></div>
</div>
<!-- Tags -->