Merge pull request #865 from coreos-inc/vulnerability-tool-uipt2
Vulnerability UI part 2
This commit is contained in:
commit
7860122028
23 changed files with 419 additions and 73 deletions
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
.filter-box.floating {
|
||||
float: right;
|
||||
min-width: 300px;
|
||||
margin-top: 0px;
|
||||
margin-top: 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
|
21
static/css/directives/ui/repository-events-summary.css
Normal file
21
static/css/directives/ui/repository-events-summary.css
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
22
static/directives/repository-events-summary.html
Normal file
22
static/directives/repository-events-summary.html
Normal 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>
|
|
@ -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
17
static/img/lock.svg
Normal 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
14
static/img/scan.svg
Normal 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 |
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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({});
|
||||
}
|
||||
});
|
||||
|
|
77
static/js/directives/ui/repository-events-summary.js
Normal file
77
static/js/directives/ui/repository-events-summary.js
Normal 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;
|
||||
});
|
|
@ -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'));
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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++;
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 -->
|
||||
|
|
Reference in a new issue