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__)
|
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):
|
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:
|
||||||
|
@ -39,6 +46,13 @@ def _call_security_api(relative_url, *args, **kwargs):
|
||||||
return response_data
|
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)
|
@show_if(features.SECURITY_SCANNER)
|
||||||
@resource('/v1/repository/<repopath:repository>/image/<imageid>/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')
|
||||||
|
@ -61,7 +75,7 @@ class RepositoryImageVulnerabilities(RepositoryParamResource):
|
||||||
logger.debug('Image %s under repository %s/%s not security indexed',
|
logger.debug('Image %s under repository %s/%s not security indexed',
|
||||||
repo_image.docker_image_id, namespace, repository)
|
repo_image.docker_image_id, namespace, repository)
|
||||||
return {
|
return {
|
||||||
'security_indexed': False
|
'status': _get_status(repo_image),
|
||||||
}
|
}
|
||||||
|
|
||||||
layer_id = '%s.%s' % (repo_image.docker_image_id, repo_image.storage.uuid)
|
layer_id = '%s.%s' % (repo_image.docker_image_id, repo_image.storage.uuid)
|
||||||
|
@ -69,7 +83,7 @@ class RepositoryImageVulnerabilities(RepositoryParamResource):
|
||||||
minimumPriority=args.minimumPriority)
|
minimumPriority=args.minimumPriority)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'security_indexed': True,
|
'status': _get_status(repo_image),
|
||||||
'data': data,
|
'data': data,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,14 +105,14 @@ class RepositoryImagePackages(RepositoryParamResource):
|
||||||
|
|
||||||
if not repo_image.security_indexed:
|
if not repo_image.security_indexed:
|
||||||
return {
|
return {
|
||||||
'security_indexed': False
|
'status': _get_status(repo_image),
|
||||||
}
|
}
|
||||||
|
|
||||||
layer_id = '%s.%s' % (repo_image.docker_image_id, repo_image.storage.uuid)
|
layer_id = '%s.%s' % (repo_image.docker_image_id, repo_image.storage.uuid)
|
||||||
data = _call_security_api('layers/%s/packages', layer_id)
|
data = _call_security_api('layers/%s/packages', layer_id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'security_indexed': True,
|
'status': _get_status(repo_image),
|
||||||
'data': data,
|
'data': data,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,56 @@
|
||||||
float: right;
|
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 {
|
.repo-panel-info-element .right-controls .copy-box {
|
||||||
width: 400px;
|
width: 400px;
|
||||||
display: inline-block;
|
margin-top: 10px;
|
||||||
margin-left: 10px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-panel-info-element .stat-col {
|
.repo-panel-info-element .stat-col {
|
||||||
|
|
|
@ -93,7 +93,9 @@
|
||||||
margin-right: 4px;
|
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;
|
color: #9B9B9B;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
.filter-box.floating {
|
.filter-box.floating {
|
||||||
float: right;
|
float: right;
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
margin-top: 0px;
|
margin-top: 15px;
|
||||||
position: relative;
|
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.Unknown,
|
||||||
.vulnerability-priority-view-element.Low,
|
.vulnerability-priority-view-element.Low,
|
||||||
.vulnerability-priority-view-element.Negligable {
|
.vulnerability-priority-view-element.Negligible {
|
||||||
color: #9B9B9B;
|
color: #9B9B9B;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-view .co-tab-content h3 {
|
.image-view .co-tab-content h3 {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-view .fa-bug {
|
.image-view .fa-bug {
|
||||||
|
@ -42,4 +42,29 @@
|
||||||
|
|
||||||
.image-view .co-filter-box input {
|
.image-view .co-filter-box input {
|
||||||
display: inline-block;
|
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 -->
|
<!-- Pull Controls -->
|
||||||
<div class="right-controls hidden-sm hidden-xs">
|
<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>
|
</div>
|
||||||
|
|
||||||
<h4 style="font-size:20px;">Description</h4>
|
<h4 style="font-size:20px;">Description</h4>
|
||||||
|
|
|
@ -115,15 +115,26 @@
|
||||||
</td>
|
</td>
|
||||||
<td quay-require="['SECURITY_SCANNER']" class="security-scan-col">
|
<td quay-require="['SECURITY_SCANNER']" class="security-scan-col">
|
||||||
<span class="cor-loader-inline" ng-if="getTagVulnerabilities(tag).loading"></span>
|
<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">
|
<span ng-if="!getTagVulnerabilities(tag).loading">
|
||||||
<!-- Scanning -->
|
<!-- Queued -->
|
||||||
<span class="scanning" ng-if="!getTagVulnerabilities(tag).security_indexed"
|
<span class="scanning" ng-if="getTagVulnerabilities(tag).status == 'queued'"
|
||||||
data-title="The image for this tag is queued to be scanned for vulnerabilities"
|
data-title="The image for this tag is queued to be scanned for vulnerabilities"
|
||||||
bs-tooltip>Queued for scan</span>
|
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 -->
|
<!-- No Vulns -->
|
||||||
<span class="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"
|
data-title="The image for this tag has no vulnerabilities as found in our database"
|
||||||
bs-tooltip
|
bs-tooltip
|
||||||
bindonce>
|
bindonce>
|
||||||
|
@ -134,7 +145,7 @@
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Vulns -->
|
<!-- 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"
|
ng-class="getTagVulnerabilities(tag).highestVulnerability.Priority"
|
||||||
class="has-vulns" bindonce>
|
class="has-vulns" bindonce>
|
||||||
<a class="vuln-link" bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ tag.image_id }}?tab=security"
|
<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"
|
<div class="create-external-notification-dialog"
|
||||||
repository="repository"
|
repository="repository"
|
||||||
counter="showNewNotificationCounter"
|
counter="showNewNotificationCounter"
|
||||||
|
default-data="newNotificationData"
|
||||||
notification-created="handleNotificationCreated(notification)"></div>
|
notification-created="handleNotificationCreated(notification)"></div>
|
||||||
</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',
|
restrict: 'C',
|
||||||
scope: {
|
scope: {
|
||||||
'repository': '=repository',
|
'repository': '=repository',
|
||||||
'builds': '=builds'
|
'builds': '=builds',
|
||||||
|
'isEnabled': '=isEnabled'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, ApiService, Config) {
|
controller: function($scope, $element, ApiService, Config) {
|
||||||
$scope.$watch('repository', function(repository) {
|
$scope.$watch('repository', function(repository) {
|
||||||
|
|
|
@ -157,10 +157,10 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
ApiService.getRepoImageVulnerabilities(null, params).then(function(resp) {
|
ApiService.getRepoImageVulnerabilities(null, params).then(function(resp) {
|
||||||
imageData.security_indexed = resp.security_indexed;
|
|
||||||
imageData.loading = false;
|
imageData.loading = false;
|
||||||
|
imageData.status = resp['status'];
|
||||||
|
|
||||||
if (imageData.security_indexed) {
|
if (imageData.status == 'scanned') {
|
||||||
var vulnerabilities = resp.data.Vulnerabilities;
|
var vulnerabilities = resp.data.Vulnerabilities;
|
||||||
|
|
||||||
imageData.hasVulnerabilities = !!vulnerabilities.length;
|
imageData.hasVulnerabilities = !!vulnerabilities.length;
|
||||||
|
|
|
@ -11,7 +11,8 @@ angular.module('quay').directive('createExternalNotificationDialog', function ()
|
||||||
scope: {
|
scope: {
|
||||||
'repository': '=repository',
|
'repository': '=repository',
|
||||||
'counter': '=counter',
|
'counter': '=counter',
|
||||||
'notificationCreated': '¬ificationCreated'
|
'notificationCreated': '¬ificationCreated',
|
||||||
|
'defaultData': '=defaultData'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, ExternalNotificationData, ApiService, $timeout, StringBuilderService) {
|
controller: function($scope, $element, ExternalNotificationData, ApiService, $timeout, StringBuilderService) {
|
||||||
$scope.currentEvent = null;
|
$scope.currentEvent = null;
|
||||||
|
@ -98,6 +99,13 @@ angular.module('quay').directive('createExternalNotificationDialog', function ()
|
||||||
ApiService.createRepoNotification(data, params).then(function(resp) {
|
ApiService.createRepoNotification(data, params).then(function(resp) {
|
||||||
$scope.status = '';
|
$scope.status = '';
|
||||||
$scope.notificationCreated({'notification': resp});
|
$scope.notificationCreated({'notification': resp});
|
||||||
|
|
||||||
|
// Used by repository-events-summary.
|
||||||
|
if (!$scope.repository._notificationCounter) {
|
||||||
|
$scope.repository._notificationCounter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.repository._notificationCounter++;
|
||||||
$('#createNotificationModal').modal('hide');
|
$('#createNotificationModal').modal('hide');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -154,6 +162,13 @@ angular.module('quay').directive('createExternalNotificationDialog', function ()
|
||||||
$scope.currentEvent = null;
|
$scope.currentEvent = null;
|
||||||
$scope.currentMethod = null;
|
$scope.currentMethod = null;
|
||||||
$scope.unauthorizedEmail = false;
|
$scope.unauthorizedEmail = false;
|
||||||
|
|
||||||
|
$timeout(function() {
|
||||||
|
if ($scope.defaultData && $scope.defaultData['currentEvent']) {
|
||||||
|
$scope.setEvent($scope.defaultData['currentEvent']);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
$('#createNotificationModal').modal({});
|
$('#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',
|
'repository': '=repository',
|
||||||
'isEnabled': '=isEnabled'
|
'isEnabled': '=isEnabled'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, ApiService, Restangular, UtilService, ExternalNotificationData) {
|
controller: function($scope, $element, $timeout, ApiService, Restangular, UtilService, ExternalNotificationData, $location) {
|
||||||
$scope.showNewNotificationCounter = 0;
|
$scope.showNewNotificationCounter = 0;
|
||||||
|
$scope.newNotificationData = {};
|
||||||
|
|
||||||
var loadNotifications = function() {
|
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 = {
|
var params = {
|
||||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name
|
'repository': $scope.repository.namespace + '/' + $scope.repository.name
|
||||||
|
@ -73,6 +91,13 @@ angular.module('quay').directive('repositoryEventsTable', function () {
|
||||||
var index = $.inArray(notification, $scope.notifications);
|
var index = $.inArray(notification, $scope.notifications);
|
||||||
if (index < 0) { return; }
|
if (index < 0) { return; }
|
||||||
$scope.notifications.splice(index, 1);
|
$scope.notifications.splice(index, 1);
|
||||||
|
|
||||||
|
if (!$scope.repository._notificationCounter) {
|
||||||
|
$scope.repository._notificationCounter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.repository._notificationCounter++;
|
||||||
|
|
||||||
}, ApiService.errorDisplay('Cannot delete notification'));
|
}, ApiService.errorDisplay('Cannot delete notification'));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -55,26 +55,33 @@
|
||||||
|
|
||||||
$scope.packagesResource = ApiService.getRepoImagePackagesAsResource(params).get(function(packages) {
|
$scope.packagesResource = ApiService.getRepoImagePackagesAsResource(params).get(function(packages) {
|
||||||
$scope.packages = packages;
|
$scope.packages = packages;
|
||||||
|
return packages;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.loadImageVulnerabilities = function() {
|
$scope.loadImageVulnerabilities = function() {
|
||||||
if (!Features.SECURITY_SCANNER || $scope.vulnerabilitiesResource) { return; }
|
if (!Features.SECURITY_SCANNER || $scope.vulnerabilitiesResource) { return; }
|
||||||
|
|
||||||
|
$scope.VulnerabilityLevels = VulnerabilityService.getLevels();
|
||||||
|
|
||||||
var params = {
|
var params = {
|
||||||
'repository': namespace + '/' + name,
|
'repository': namespace + '/' + name,
|
||||||
'imageid': imageid
|
'imageid': imageid
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.vulnerabilitiesResource = ApiService.getRepoImageVulnerabilitiesAsResource(params).get(function(resp) {
|
$scope.vulnerabilitiesResource = ApiService.getRepoImageVulnerabilitiesAsResource(params).get(function(resp) {
|
||||||
$scope.vulerabilityInfo = resp;
|
$scope.vulnerabilityInfo = resp;
|
||||||
$scope.vulnerabilities = [];
|
$scope.vulnerabilities = [];
|
||||||
|
|
||||||
resp.data.Vulnerabilities.forEach(function(vuln) {
|
if (resp.data && resp.data.Vulnerabilities) {
|
||||||
vuln_copy = jQuery.extend({}, vuln);
|
resp.data.Vulnerabilities.forEach(function(vuln) {
|
||||||
vuln_copy['index'] = VulnerabilityService.LEVELS[vuln['Priority']]['index'];
|
vuln_copy = jQuery.extend({}, vuln);
|
||||||
$scope.vulnerabilities.push(vuln_copy);
|
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);
|
var imageLoader = ImageLoaderService.getLoader($scope.namespace, $scope.name);
|
||||||
|
|
||||||
// Tab-enabled counters.
|
// Tab-enabled counters.
|
||||||
|
$scope.infoShown = 0;
|
||||||
$scope.tagsShown = 0;
|
$scope.tagsShown = 0;
|
||||||
$scope.logsShown = 0;
|
$scope.logsShown = 0;
|
||||||
$scope.buildsShown = 0;
|
$scope.buildsShown = 0;
|
||||||
|
@ -119,6 +120,10 @@
|
||||||
$scope.viewScope.selectedTags = $.unique(tagNames.split(','));
|
$scope.viewScope.selectedTags = $.unique(tagNames.split(','));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.showInfo = function() {
|
||||||
|
$scope.infoShown++;
|
||||||
|
};
|
||||||
|
|
||||||
$scope.showBuilds = function() {
|
$scope.showBuilds = function() {
|
||||||
$scope.buildsShown++;
|
$scope.buildsShown++;
|
||||||
};
|
};
|
||||||
|
|
|
@ -47,12 +47,12 @@ function(Config, Features, VulnerabilityService) {
|
||||||
events.push({
|
events.push({
|
||||||
'id': 'vulnerability_found',
|
'id': 'vulnerability_found',
|
||||||
'title': 'Package Vulnerability Found',
|
'title': 'Package Vulnerability Found',
|
||||||
'icon': 'fa-flag',
|
'icon': 'fa-bug',
|
||||||
'fields': [
|
'fields': [
|
||||||
{
|
{
|
||||||
'name': 'level',
|
'name': 'level',
|
||||||
'type': 'enum',
|
'type': 'enum',
|
||||||
'title': 'Minimum Severity Level',
|
'title': 'Minimum Priority Level',
|
||||||
'values': VulnerabilityService.LEVELS,
|
'values': VulnerabilityService.LEVELS,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -67,45 +67,67 @@
|
||||||
<!-- Security -->
|
<!-- Security -->
|
||||||
<div id="security" class="tab-pane" quay-require="['SECURITY_SCANNER']">
|
<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="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>
|
<h3>Image Security</h3>
|
||||||
<div class="empty" ng-if="!vulerabilityInfo.security_indexed">
|
<div class="empty" ng-if="vulnerabilityInfo.status == 'queued'">
|
||||||
<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>
|
||||||
<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">
|
<div class="empty-secondary-msg">
|
||||||
Please adjust your filter above.
|
Please try again in a few minutes.
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -113,23 +135,24 @@
|
||||||
<!-- Packages -->
|
<!-- Packages -->
|
||||||
<div id="packages" class="tab-pane" quay-require="['SECURITY_SCANNER']">
|
<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>
|
<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>
|
<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-primary-msg">This image has not been indexed yet</div>
|
||||||
<div class="empty-secondary-msg">
|
<div class="empty-secondary-msg">
|
||||||
Please try again in a few minutes.
|
Please try again in a few minutes.
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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>
|
||||||
</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>
|
<thead>
|
||||||
<td>Package Name</td>
|
<td>Package Name</td>
|
||||||
<td>Package Version</td>
|
<td>Package Version</td>
|
||||||
|
@ -146,7 +169,7 @@
|
||||||
<div class="empty" ng-if="(packages.data.Packages | filter:options.packageFilter).length == 0"
|
<div class="empty" ng-if="(packages.data.Packages | filter:options.packageFilter).length == 0"
|
||||||
style="margin-top: 20px;">
|
style="margin-top: 20px;">
|
||||||
<div class="empty-primary-msg">No matching packages found</div>
|
<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.
|
Please adjust your filter above.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,7 +17,8 @@
|
||||||
|
|
||||||
<div class="cor-tab-panel">
|
<div class="cor-tab-panel">
|
||||||
<div class="cor-tabs">
|
<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>
|
<i class="fa fa-info-circle"></i>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
@ -56,7 +57,8 @@
|
||||||
<div id="info" class="tab-pane active">
|
<div id="info" class="tab-pane active">
|
||||||
<div class="repo-panel-info"
|
<div class="repo-panel-info"
|
||||||
repository="viewScope.repository"
|
repository="viewScope.repository"
|
||||||
builds="viewScope.builds"></div>
|
builds="viewScope.builds"
|
||||||
|
is-enabled="infoShown"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tags -->
|
<!-- Tags -->
|
||||||
|
|
Reference in a new issue