Implement new vulnerabilities and packages tabs.

Fixes https://github.com/coreos-inc/design/issues/268
This commit is contained in:
Joseph Schorr 2016-02-22 18:39:04 -05:00
parent c7904db30d
commit ae9140caae
16 changed files with 1547 additions and 171 deletions

View file

@ -1048,12 +1048,18 @@ a:focus {
padding-left: 6px;
padding-right: 0px;
color: #aaa;
text-align: center;
max-width: 20px;
}
.co-table td.caret-col i.fa {
cursor: pointer;
}
.co-table td.caret-col i.fa.fa-caret-down {
color: black;
}
.co-table .add-row-spacer td {
padding: 5px;
}
@ -1177,6 +1183,28 @@ a:focus {
background: #F6FCFF;
}
.co-filter-box {
position: relative;;
}
.co-filter-box .filter-message {
display: inline-block;
position: absolute;
left: -220px;
top: 7px;
color: #ccc;
}
.co-filter-box .filter-options {
margin-top: 4px;
font-size: 14px;
text-align: right;
}
.co-filter-box .filter-options input {
margin-right: 6px;
}
.co-check-bar {
margin-bottom: 10px;
}

View file

@ -0,0 +1,109 @@
.image-feature-view-element .donut-icon {
position: absolute;
top: 60px;
left: 95px;
font-size: 80px;
text-align: center;
color: #EAEAEA;
margin-left: -6px;
}
.image-feature-view-element .no-vulns {
color: #2FC98E;
}
.image-feature-view-element .no-vulns i.fa {
margin-right: 6px;
}
.image-feature-view-element .security-header {
margin-top: -4px;
margin-bottom: 30px;
padding-bottom: 30px;
border-bottom: 1px solid #eee;
}
.image-feature-view-element .donut-col {
padding-top: 20px;
text-align: center;
}
.image-feature-view-element #featureDonutChart {
display: inline-block;
}
.image-feature-view-element .summary-col {
font-size: 20px;
padding-top: 40px;
}
.image-feature-view-element .summary-col .title-item {
font-size: 24px;
margin-bottom: 40px;
}
.image-feature-view-element .summary-list {
text-align: left;
list-style: none;
}
.image-feature-view-element .summary-list i.fa {
margin-right: 10px;
}
.image-feature-view-element .summary-list strong {
text-align: right;
width: 40px;
display: inline-block;
margin-right: 6px;
}
.image-feature-view-element .co-table .empty {
color: #ddd;
}
.image-feature-view-element .co-table .single-col {
width: 12.5%;
}
.image-feature-view-element .co-table .double-col {
width: 25%;
}
.image-feature-view-element .co-table .impact-col {
text-align: center;
width: 130px;
}
.image-feature-view-element .co-table .image-col {
white-space: nowrap;
}
.image-feature-view-element .co-table .image-col .fa {
margin-left: 6px;
opacity: 0.5;
}
@media (max-width: 767px) {
.image-feature-view-element .co-table .single-col {
width: auto !important;
}
}
.image-feature-view-element .dockerfile-command {
cursor: default;
}
.image-feature-view-element .dockerfile-command .command-title {
font-size: 12px;
max-width: 297px;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
white-space: nowrap;
vertical-align: middle;
}
.image-feature-view-element .vuln-summary i.fa {
margin-right: 6px;
}

View file

@ -0,0 +1,169 @@
.image-vulnerability-view-element .donut-icon {
position: absolute;
top: 70px;
left: 95px;
font-size: 80px;
text-align: center;
color: #EAEAEA;
margin-left: -6px;
}
.image-vulnerability-view-element .security-header {
margin-top: -4px;
margin-bottom: 30px;
padding-bottom: 30px;
border-bottom: 1px solid #eee;
}
.image-vulnerability-view-element .donut-col {
padding-top: 20px;
text-align: center;
}
.image-vulnerability-view-element #vulnDonutChart {
display: inline-block;
}
.image-vulnerability-view-element .summary-col {
font-size: 20px;
padding-top: 40px;
}
.image-vulnerability-view-element .summary-col .title-item {
font-size: 24px;
margin-bottom: 40px;
}
.image-vulnerability-view-element .summary-list {
text-align: left;
list-style: none;
}
.image-vulnerability-view-element .summary-list i.fa {
margin-right: 10px;
}
.image-vulnerability-view-element .summary-list strong {
text-align: right;
width: 40px;
display: inline-block;
margin-right: 6px;
}
.image-vulnerability-view-element .dockerfile-command {
cursor: default;
}
.image-vulnerability-view-element .dockerfile-command .command-title {
font-size: 12px;
max-width: 297px;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
white-space: nowrap;
vertical-align: middle;
}
.image-vulnerability-view-element .co-table .empty {
color: #ddd;
}
.image-vulnerability-view-element .co-table .single-col {
width: 15%;
}
.image-vulnerability-view-element .co-table .double-col {
width: 30%;
}
.image-vulnerability-view-element .co-table .impact-col {
text-align: center;
width: 130px;
}
.image-vulnerability-view-element .co-table .nowrap-col {
white-space: nowrap;
}
.image-vulnerability-view-element .co-table .image-col {
white-space: nowrap;
}
.image-vulnerability-view-element .co-table .image-col .fa {
margin-left: 6px;
opacity: 0.5;
}
@media (max-width: 767px) {
.image-vulnerability-view-element .co-table .single-col {
width: auto !important;
}
}
.image-vulnerability-view-element .fixed-in-version:before {
font-family: FontAwesome;
content: '\f0a9';
margin-right: 6px;
}
.image-vulnerability-view-element .fixed-in-version {
color: rgb(47, 201, 142);
}
.image-vulnerability-view-element .cvss-text {
display: inline-block;
width: 20px;
text-align: right;
}
.image-vulnerability-view-element .vulnerability-priority-view {
margin-left: 10px;
}
.image-vulnerability-view-element .cvss {
display: inline-block;
width: 100px;
height: 10px;
vertical-align: middle;
margin-left: 8px;
background: #eee;
position: relative;;
}
.image-vulnerability-view-element .cvss span {
display: inline-block;
position: absolute;
top: 0px;
left: 0px;
bottom: 0px;
}
.image-vulnerability-view-element .expansion-col {
padding-top: 20px;
padding-bottom: 20px;
}
.image-vulnerability-view-element .subtitle {
color: #999;
font-size: 90%;
text-transform: uppercase;
font-weight: 300;
padding-top: 0!important;
text-align: left;
margin-bottom: 6px;
}
.image-vulnerability-view-element .expand-link {
color: black !important;
}
.image-vulnerability-view-element .external-link {
margin-left: 10px;
font-size: 12px;
}
.image-vulnerability-view-element .description {
display: inline-block;
max-width: 1000px;
}

View file

@ -0,0 +1,50 @@
.nvd-vectors-display-element dl {
display: inline-block;
vertical-align: top;
max-width: 200px;
padding: 10px;
}
.nvd-vectors-display-element dt {
margin-bottom: 10px;
cursor: pointer;
}
.nvd-vectors-display-element dd {
cursor: pointer;
}
.nvd-vectors-display-element dd:before {
display: inline-block;
margin-right: 4px;
font-family: FontAwesome;
content: '\f111';
}
.nvd-vectors-display-element dd.not-current-vector {
color: #ddd;
}
.nvd-vectors-display-element dd.current-vector.high:before {
content: '\f071';
}
.nvd-vectors-display-element dd.current-vector.medium:before {
content: '\f06a';
}
.nvd-vectors-display-element dd.current-vector.low:before {
content: '\f056';
}
.nvd-vectors-display-element .current-vector.high {
color: rgb(214, 68, 86);
}
.nvd-vectors-display-element .current-vector.medium {
color: rgb(252, 166, 87);
}
.nvd-vectors-display-element .current-vector.low {
color: rgb(47, 201, 142);
}

View file

@ -0,0 +1,148 @@
<div class="image-feature-view-element">
<!-- Not scanned -->
<div class="empty" ng-if="securityStatus == '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>
<!-- Unable to scan -->
<div class="empty" ng-if="securityStatus == '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>
<!-- Scanned -->
<div ng-if="securityStatus == 'scanned'">
<!-- Header -->
<div class="security-header row">
<div class="donut-col col-md-3">
<div id="featureDonutChart" style="position: relative;">
<svg style="height:250px; width:250px"></svg>
<span class="donut-icon">
<i class="fa ci-package"></i>
</span>
</div>
</div>
<div class="summary-col col-md-9">
<ul class="summary-list">
<li class="title-item">Quay Security Scanner has resolved <strong>{{ securityFeatures.length }}</strong> packages.</li>
<li ng-repeat="priority in featureBreakdown">
<span ng-if="priority.label != 'None'">
<i class="fa ci-package" ng-style="{'color': priority.color}"></i> <strong>{{ priority.value }}</strong> packages with {{ priority.label }}-level vulnerabilities.
</span>
<span ng-if="priority.label == 'None'" style="margin-top: 20px; display: inline-block;">
<i class="fa ci-package" ng-style="{'color': priority.color}"></i> <strong>{{ priority.value }}</strong> packages with no vulnerabilities.
</span>
</li>
</ul>
</div>
</div>
<!-- Filter -->
<span class="co-filter-box">
<span class="filter-message" ng-if="options.featureFilter">
Showing {{ orderedFeatures.entries.length }} of {{ securityFeatures.length }} packages
</span>
<input class="form-control" type="text" ng-model="options.featureFilter" placeholder="Filter Packages...">
</span>
<h3>Image Packages</h3>
<!-- Table -->
<table class="co-table">
<thead>
<td ng-class="tablePredicateClass('name', options.predicate, options.reverse)">
<a href="javascript:void(0)" ng-click="orderBy('name')">Package Name</a>
</td>
<td class="hidden-xs">
Package Version
</td>
<td ng-class="tablePredicateClass('score', options.predicate, options.reverse)">
<a href="javascript:void(0)" ng-click="orderBy('score')">Vulnerabilities</a>
</td>
<td class="hidden-xs hidden-sm hidden-md"
ng-class="tablePredicateClass('leftoverScore', options.predicate, options.reverse)"
data-title="Identified vulnerabilities remaining after the package is upgraded to the latest version"
data-container="body" bs-tooltip>
<a href="javascript:void(0)" ng-click="orderBy('leftoverScore')">Remaining after upgrade</a>
</td>
<td ng-class="tablePredicateClass('fixableScore', options.predicate, options.reverse)">
<a href="javascript:void(0)" ng-click="orderBy('fixableScore')"
data-title="Delta of the severity of vulnerabilities in the package before->after upgrading" data-container="body" bs-tooltip>Upgrade impact</a>
</td>
<td class="hidden-xs hidden-sm hidden-md">
Introduced In Image
</td>
<td class="hidden-xs options-col"></td>
</thead>
<tbody ng-repeat="feature in orderedFeatures.visibleEntries" bindonce>
<tr>
<td class="single-col">
<span bo-text="feature.name"></span>
</td>
<td class="single-col hidden-xs">
<span bo-text="feature.version"></span>
</td>
<td>
<span class="no-vulns" bo-if="feature.vulnCount == 0">
<i class="fa fa-check-circle"></i>None Detected
</span>
<span class="vuln-summary" bo-if="feature.vulnCount != 0">
<span ng-style="{'color': feature.primarySeverity.color}">
<i class="fa fa fa-exclamation-triangle"></i>
{{ feature.primarySeverity.count }}
{{ feature.primarySeverity.title }}
</span>
<span bo-if="feature.vulnCount - feature.primarySeverity.count > 0">
+ {{ feature.vulnCount - feature.primarySeverity.count }} additional
</span>
</span>
</td>
<td class="hidden-xs hidden-sm hidden-md">
<span class="empty" bo-if="feature.vulnCount == 0">
(N/A)
</span>
<span class="no-vulns" bo-if="feature.vulnCount != 0 && feature.leftoverBreakdown.length == 0">
<i class="fa fa-arrow-circle-right"></i>
All identified vulnerabilities fixed
</span>
<span class="vuln-summary" bo-if="feature.vulnCount != 0 && feature.leftoverBreakdown.length != 0">
<span ng-style="{'color': feature.primaryLeftover.color}">
<i class="fa fa-arrow-circle-right"></i>
{{ feature.primaryLeftover.count }}
{{ feature.primaryLeftover.title }}
</span>
<span bo-if="feature.leftoverCount - feature.primaryLeftover.count > 0">
+ {{ feature.leftoverCount - feature.primaryLeftover.count }} additional
</span>
</span>
</td>
<td class="impact-col">
<span class="empty" bo-if="feature.vulnCount == 0">
(N/A)
</span>
<span bo-if="feature.vulnCount > 0">
<span class="strength-indicator" value="feature.fixableScore" maximum="highestFixableScore"
log-base="2"></span>
</span>
</td>
<td class="double-col image-col hidden-xs hidden-sm hidden-md">
<span data-title="{{ feature.imageCommand }}" bs-tooltip>
<span class="dockerfile-command" command="feature.imageCommand"></span>
</span>
<a href="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ feature.imageId }}"><i class="fa fa-archive"></i></a>
</td>
<td></td>
</tr>
</tbody>
</table>
<div class="empty" ng-if="securityFeatures.length && !orderedFeatures.entries.length"
style="margin-top: 20px;">
<div class="empty-primary-msg">No matching packages found.</div>
<div class="empty-secondary-msg">Try expanding your filtering terms.</div>
</div>
</div>
</div>

View file

@ -0,0 +1,153 @@
<div class="image-vulnerability-view-element">
<!-- Not scanned -->
<div class="empty" ng-if="securityStatus == '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>
<!-- Unable to scan -->
<div class="empty" ng-if="securityStatus == '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>
<!-- Scanned -->
<div ng-if="securityStatus == 'scanned'">
<!-- Header -->
<div class="security-header row">
<div class="donut-col col-md-3">
<div id="vulnDonutChart" style="position: relative;">
<svg style="height:250px; width:250px"></svg>
<span class="donut-icon">
<i class="fa fa-bug"></i>
</span>
</div>
</div>
<div class="summary-col col-md-9">
<ul class="summary-list" ng-if="priorityBreakdown.length">
<li class="title-item">Quay Security Scanner has detected <strong>{{ securityVulnerabilities.length }}</strong> vulnerabilities.</li>
<li ng-repeat="priority in priorityBreakdown">
<i class="fa fa-exclamation-triangle" ng-style="{'color': priority.color}"></i> <strong>{{ priority.value }}</strong> {{ priority.label }}-level vulnerabilities.
</li>
</ul>
<div ng-if="!priorityBreakdown.length">
<li class="title-item">Quay Security Scanner has detected no vulnerabilities in this image.</li>
</div>
</div>
</div>
<!-- Filter -->
<span class="co-filter-box">
<span class="filter-message" ng-if="options.vulnFilter || options.fixableVulns">
Showing {{ orderedVulnerabilities.entries.length }} of {{ securityVulnerabilities.length }} Vulnerabilities
</span>
<input class="form-control" type="text" ng-model="options.vulnFilter" placeholder="Filter Vulnerabilities...">
<div class="filter-options">
<label><input type="checkbox" ng-model="options.fixableVulns">Only display vulnerabilities with fixes</label>
</div>
</span>
<h3>Image Vulnerabilities</h3>
<!-- Table -->
<table class="co-table">
<thead>
<td class="caret-col"></td>
<td ng-class="tablePredicateClass('name', options.predicate, options.reverse)">
<a href="javascript:void(0)" ng-click="orderBy('name')">CVE</a>
</td>
<td ng-class="tablePredicateClass('score', options.predicate, options.reverse)">
<a href="javascript:void(0)" ng-click="orderBy('score')">CVSS / Severity</a>
</td>
<td class="hidden-xs" ng-class="tablePredicateClass('featureName', options.predicate, options.reverse)">
<a href="javascript:void(0)" ng-click="orderBy('featureName')">Package</a>
</td>
<td class="hidden-xs">Current version</td>
<td class="hidden-xs hidden-sm">Fixed in version</td>
</td>
<td class="hidden-xs hidden-sm hidden-md">Introduced in image</td>
<td class="hidden-xs options-col"></td>
</thead>
<tbody ng-repeat="vuln in orderedVulnerabilities.visibleEntries" bindonce>
<tr>
<td class="caret-col">
<span ng-click="toggleDetails(vuln)">
<i class="fa"
ng-class="vuln.expanded ? 'fa-caret-down' : 'fa-caret-right'"
data-title="View Details" bs-tooltip></i>
</span>
</td>
<td class="single-col nowrap-col">
<a href="javascript:void(0)" bo-text="vuln.name" ng-click="toggleDetails(vuln)" class="expand-link"></a>
<a href="{{ vuln.link }}" class="external-link hidden-xs hidden-sm hidden-md" target="_blank">
<i class="fa fa-link"></i>
</a>
</td>
<td class="single-col nowrap-col">
<span bo-if="vuln.metadata.NVD.CVSSv2.Score">
<span class="cvss-text" bo-text="vuln.metadata.NVD.CVSSv2.Score"></span>
<span class="cvss"><span bo-style="{'width': (vuln.metadata.NVD.CVSSv2.Score * 10) + '%', 'background-color': getCVSSColor(vuln.metadata.NVD.CVSSv2.Score)}"></span>
</span>
</span>
<span bo-if="!vuln.metadata.NVD.CVSSv2.Score">
<span class="vulnerability-priority-view" priority="vuln.severity"></span>
</span>
</td>
<td class="single-col hidden-xs"><span bo-text="vuln.featureName"></span></td>
<td class="single-col hidden-xs hidden-sm"><span bo-text="vuln.introducedInVersion"></span></td>
<td class="single-col hidden-xs">
<span class="empty" bo-if="!vuln.fixedInVersion">(None)</span>
<span class="fixed-in-version" bo-if="vuln.fixedInVersion" bo-text="vuln.fixedInVersion"></span>
</td>
<td class="double-col image-col hidden-xs hidden-sm hidden-md">
<span data-title="{{ vuln.imageCommand }}" bs-tooltip>
<span class="dockerfile-command" command="vuln.imageCommand"></span>
</span>
<a href="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ vuln.imageId }}"><i class="fa fa-archive"></i></a>
</td>
<td></td>
</tr>
<tr ng-if="vuln.expanded">
<td class="expansion-col" colspan="8">
<div class="visible-xs" style="margin-bottom: 20px">
<div class="subtitle">Summary</div>
<table>
<tr><td>Package:</td><td><span bo-text="vuln.featureName"></span></td></tr>
<tr><td>Introduced in version:</td><td><span bo-text="vuln.introducedInVersion"></span></td></tr>
<tr>
<td>Fixed in version:</td>
<td>
<span class="empty" bo-if="!vuln.fixedInVersion">(None)</span>
<span class="fixed-in-version" bo-if="vuln.fixedInVersion" bo-text="vuln.fixedInVersion"></span>
</td>
</tr>
<tr>
<td>Introduced in Image:</td>
<td><span class="image-link" repository="repository" image-id="vuln.imageId"></span></td>
</tr>
</table>
</div>
<div class="vectors" bo-if="vuln.metadata.NVD.CVSSv2.Vectors">
<div class="subtitle">Vectors</div>
<div class="nvd-vectors-display" vectors="{{ vuln.metadata.NVD.CVSSv2.Vectors }}"></div>
</div>
<div class="subtitle">Description</div>
<span class="description" bo-text="vuln.description"></span>
</td>
</tr>
</tbody>
</table>
<div class="empty" ng-if="securityVulnerabilities.length && !orderedVulnerabilities.entries.length"
style="margin-top: 20px;">
<div class="empty-primary-msg">No matching vulnerabilities found.</div>
<div class="empty-secondary-msg">Try expanding your filtering terms.</div>
</div>
</div>
</div>

View file

@ -0,0 +1,8 @@
<div class="nvd-vectors-display-element">
<dl ng-repeat="vector in parsedVectors" bindonce>
<dt bo-text="getVectorTitle(vector)" data-title="{{ getVectorDescription(vector) }}" data-container="body" bs-tooltip></dt>
<dd bo-class="getVectorClasses(option, vector)" ng-repeat="option in getVectorOptions(vector)" data-container="body" data-title="{{ option.description }}" bs-tooltip>
{{ option.title }}
</dd>
</dl>
</div>

View file

@ -152,7 +152,7 @@
data-title="The image for this tag has no vulnerabilities as found in our database"
bs-tooltip
bindonce>
<a bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ tag.image_id }}?tab=security">
<a bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ tag.image_id }}?tab=vulnerabilities">
<i class="fa fa-check-circle"></i>
Passed
</a>
@ -162,7 +162,7 @@
<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"
<a class="vuln-link" bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ tag.image_id }}?tab=vulnerabilities"
data-title="The image for this tag has {{ getTagVulnerabilities(tag).highestVulnerability.Count }} {{ getTagVulnerabilities(tag).highestVulnerability.Priority }} level vulnerabilities"
bs-tooltip>
<span class="highest-vuln">
@ -176,7 +176,7 @@
+ {{ getTagVulnerabilities(tag).vulnerabilities.length - getTagVulnerabilities(tag).highestVulnerability.Count }} others
</span>
</a>
<a bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ tag.image_id }}?tab=security" style="display: inline-block; margin-left: 6px;">
<a bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ tag.image_id }}?tab=vulnerabilities" style="display: inline-block; margin-left: 6px;">
More Info
</a>
</span>

View file

@ -0,0 +1,311 @@
/**
* An element which displays the features of an image.
*/
angular.module('quay').directive('imageFeatureView', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/image-feature-view.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'repository': '=repository',
'image': '=image',
'isEnabled': '=isEnabled'
},
controller: function($scope, $element, Config, ApiService, VulnerabilityService, AngularViewArray, ImageMetadataService) {
var imageMap = null;
$scope.securityFeatures = [];
$scope.featureBreakdown = [];
$scope.options = {
'featureFilter': null,
'predicate': 'fixableScore',
'reverse': false,
};
$scope.tablePredicateClass = function(name, predicate, reverse) {
if (name != predicate) {
return '';
}
return 'current ' + (reverse ? 'reversed' : '');
};
$scope.orderBy = function(predicate) {
if (predicate == $scope.options.predicate) {
$scope.options.reverse = !$scope.options.reverse;
return;
}
$scope.options.reverse = false;
$scope.options.predicate = predicate;
};
var buildOrderedFeatures = function() {
var features = $scope.securityFeatures.slice(0);
$scope.orderedFeatures = AngularViewArray.create();
features.forEach(function(v) {
var featureFilter = $scope.options.featureFilter;
if (featureFilter) {
if ((v['name'].indexOf(featureFilter) < 0) &&
(v['version'].indexOf(featureFilter) < 0) &&
(v['imageId'].indexOf(featureFilter) < 0)) {
return;
}
}
$scope.orderedFeatures.push(v);
});
$scope.orderedFeatures.entries.sort(function(a, b) {
var left = a[$scope.options['predicate']];
var right = b[$scope.options['predicate']];
if ($scope.options['predicate'] == 'score' ||
$scope.options['predicate'] == 'fixableScore' ||
$scope.options['predicate'] == 'leftoverScore') {
left = left * 1;
right = right * 1;
}
if (left == null) {
left = '0.00';
}
if (right == null) {
right = '0.00';
}
if (left == right) {
return 0;
}
return left > right ? -1 : 1;
});
if ($scope.options['reverse']) {
$scope.orderedFeatures.entries.reverse();
}
$scope.orderedFeatures.setVisible(true);
};
var buildChart = function() {
var chartData = $scope.featureBreakdown;
var colors = [];
for (var i = 0; i < chartData.length; ++i) {
colors.push(chartData[i].color);
}
nv.addGraph(function() {
var chart = nv.models.pieChart()
.x(function(d) { return d.label })
.y(function(d) { return d.value })
.margin({left: -10, right: -10, top: -10, bottom: -10})
.showLegend(false)
.showLabels(true)
.labelThreshold(.05)
.labelType("percent")
.donut(true)
.color(colors)
.donutRatio(0.5);
d3.select("#featureDonutChart svg")
.datum(chartData)
.transition()
.duration(350)
.call(chart);
return chart;
});
};
var buildFeatures = function(data) {
$scope.securityFeatures = [];
$scope.featureBreakdown = [];
$scope.highestFixableScore = -10000;
var severityMap = {};
var levels = VulnerabilityService.getLevels();
if (data && data.Layer && data.Layer.Features) {
data.Layer.Features.forEach(function(feature) {
var imageId = feature.AddedBy.split('.')[0];
feature_obj = {
'name': feature.Name,
'namespace': feature.Namespace,
'version': feature.Version,
'addedBy': feature.AddedBy,
'imageId': imageId,
'imageCommand': ImageMetadataService.getImageCommand($scope.image, imageId),
'vulnCount': 0,
'severityBreakdown': [],
'fixableBreakdown': [],
'score': 0,
'fixableCount': 0,
'leftoverCount': 0,
'fixableScore': 0,
'leftoverScore': 0,
'unfixableCount': 0
}
if (feature.Vulnerabilities) {
var highestSeverity = null;
var localSeverityMap = {};
var localLeftoverMap = {};
feature.Vulnerabilities.forEach(function(vuln) {
var severity = VulnerabilityService.LEVELS[vuln['Severity']];
var score = severity.score;
if (vuln.Metadata && vuln.Metadata.NVD && vuln.Metadata.NVD.CVSSv2 && vuln.Metadata.NVD.CVSSv2.Score) {
score = vuln.Metadata.NVD.CVSSv2.Score;
severity = VulnerabilityService.getSeverityForCVSS(score);
}
var logScore = (Math.pow(2, score) + 0.1);
feature_obj['score'] += logScore;
if (vuln.FixedBy) {
feature_obj['fixableScore'] += logScore;
feature_obj['fixableCount']++;
} else {
feature_obj['leftoverCount']++;
feature_obj['leftoverScore'] += logScore;
}
if (highestSeverity == null) {
highestSeverity = severity;
} else {
var index = severity['index'];
if (index < highestSeverity) {
highestSeverity = severity;
}
}
if (!localSeverityMap[severity['index']]) {
localSeverityMap[severity['index']] = 0;
}
if (!localLeftoverMap[severity['index']]) {
localLeftoverMap[severity['index']] = 0;
}
localSeverityMap[severity['index']]++;
if (!vuln.FixedBy) {
localLeftoverMap[severity['index']]++;
}
});
if (!severityMap[highestSeverity['index']]) {
severityMap[highestSeverity['index']] = 0;
}
severityMap[highestSeverity['index']]++;
var localSeverityBreakdown = [];
var localLeftoverBreakdown = [];
for (var i = 0; i < levels.length; ++i) {
var level = levels[i];
if (localSeverityMap[level['index']]) {
localSeverityBreakdown.push({
'title': level['title'],
'color': level['color'],
'count': localSeverityMap[level['index']]
})
}
if (localLeftoverMap[level['index']]) {
localLeftoverBreakdown.push({
'title': level['title'],
'color': level['color'],
'count': localLeftoverMap[level['index']]
})
}
}
feature_obj['vulnCount'] = feature.Vulnerabilities.length;
feature_obj['severityBreakdown'] = localSeverityBreakdown;
feature_obj['leftoverBreakdown'] = localLeftoverBreakdown;
if (localSeverityBreakdown) {
feature_obj['primarySeverity'] = localSeverityBreakdown[0];
}
if (localLeftoverBreakdown) {
feature_obj['primaryLeftover'] = localLeftoverBreakdown[0];
}
if (feature.Vulnerabilities.length > 0) {
feature_obj['score'] = feature_obj['score'] / feature.Vulnerabilities.length;
}
if (feature_obj['fixableScore'] > $scope.highestFixableScore) {
$scope.highestFixableScore = feature_obj['fixableScore'];
}
} else {
feature_obj['fixableScore'] = -1;
}
$scope.securityFeatures.push(feature_obj);
});
}
var greenCount = $scope.securityFeatures.length;
for (var i = 0; i < levels.length; ++i) {
var level = levels[i];
if (!severityMap[level['index']]) {
continue
}
greenCount -= severityMap[level['index']];
$scope.featureBreakdown.push({
'label': levels[i].title,
'value': severityMap[level['index']],
'color': levels[i].color,
});
}
$scope.featureBreakdown.push({
'label': 'None',
'value': greenCount,
'color': '#2FC98E'
});
buildOrderedFeatures();
};
var loadImageVulnerabilities = function() {
if ($scope.securityResource) {
return;
}
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'imageid': $scope.image.id,
'vulnerabilities': true,
};
$scope.securityResource = ApiService.getRepoImageSecurityAsResource(params).get(function(resp) {
$scope.securityStatus = resp.status;
buildFeatures(resp.data);
buildChart();
return resp;
});
};
$scope.$watch('options.predicate', buildOrderedFeatures);
$scope.$watch('options.reverse', buildOrderedFeatures);
$scope.$watch('options.featureFilter', buildOrderedFeatures);
$scope.$watch('isEnabled', function(isEnabled) {
if ($scope.isEnabled && $scope.repository && $scope.image) {
loadImageVulnerabilities();
}
});
}
};
return directiveDefinitionObject;
});

View file

@ -0,0 +1,252 @@
/**
* An element which displays the vulnerabilities in an image.
*/
angular.module('quay').directive('imageVulnerabilityView', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/image-vulnerability-view.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'repository': '=repository',
'image': '=image',
'isEnabled': '=isEnabled'
},
controller: function($scope, $element, Config, ApiService, VulnerabilityService, AngularViewArray, ImageMetadataService) {
var imageMap = null;
$scope.securityVulnerabilities = [];
$scope.options = {
'vulnFilter': null,
'fixableVulns': false,
'predicate': 'score',
'reverse': false,
};
$scope.tablePredicateClass = function(name, predicate, reverse) {
if (name != predicate) {
return '';
}
return 'current ' + (reverse ? 'reversed' : '');
};
$scope.orderBy = function(predicate) {
if (predicate == $scope.options.predicate) {
$scope.options.reverse = !$scope.options.reverse;
return;
}
$scope.options.reverse = false;
$scope.options.predicate = predicate;
};
$scope.getCVSSColor = function(score) {
return VulnerabilityService.getCVSSColor(score);
};
$scope.toggleDetails = function(vuln) {
vuln.expanded = !vuln.expanded;
};
var buildOrderedVulnerabilities = function() {
var vulnerabilities = $scope.securityVulnerabilities.slice(0);
$scope.orderedVulnerabilities = AngularViewArray.create();
vulnerabilities.forEach(function(v) {
var vulnFilter = $scope.options.vulnFilter;
if (vulnFilter) {
if ((v['name'].indexOf(vulnFilter) < 0) &&
(v['featureName'].indexOf(vulnFilter) < 0) &&
(v['imageCommand'].indexOf(vulnFilter) < 0)) {
return;
}
}
if ($scope.options.fixableVulns && !v['fixedInVersion']) {
return;
}
$scope.orderedVulnerabilities.push(v);
});
$scope.orderedVulnerabilities.entries.sort(function(a, b) {
var left = a[$scope.options['predicate']];
var right = b[$scope.options['predicate']];
if ($scope.options['predicate'] == 'score') {
left = left * 1;
right = right * 1;
}
if (left == null) {
left = '0.00';
}
if (right == null) {
right = '0.00';
}
if (left == right) {
return 0;
}
return left > right ? -1 : 1;
});
if ($scope.options['reverse']) {
$scope.orderedVulnerabilities.entries.reverse();
}
$scope.orderedVulnerabilities.setVisible(true);
};
var buildChart = function() {
var chartData = $scope.priorityBreakdown;
if ($scope.priorityBreakdown.length == 0) {
chartData = [{
'label': 'None',
'value': 1,
'color': '#2FC98E'
}];
}
var colors = [];
for (var i = 0; i < chartData.length; ++i) {
colors.push(chartData[i].color);
}
nv.addGraph(function() {
var chart = nv.models.pieChart()
.x(function(d) { return d.label })
.y(function(d) { return d.value })
.margin({left: -10, right: -10, top: -10, bottom: -10})
.showLegend(false)
.showLabels(true)
.labelThreshold(.05)
.labelType("percent")
.donut(true)
.color(colors)
.donutRatio(0.5);
d3.select("#vulnDonutChart svg")
.datum(chartData)
.transition()
.duration(350)
.call(chart);
return chart;
});
};
var buildFeaturesAndVulns = function(data) {
$scope.securityFeatures = [];
$scope.securityVulnerabilities = [];
$scope.priorityBreakdown = [];
var severityMap = {};
if (data && data.Layer && data.Layer.Features) {
data.Layer.Features.forEach(function(feature) {
feature_obj = {
'name': feature.Name,
'namespace': feature.Namespace,
'version': feature.Version,
'addedBy': feature.AddedBy
}
feature_vulnerabilities = []
if (feature.Vulnerabilities) {
feature.Vulnerabilities.forEach(function(vuln) {
var score = VulnerabilityService.LEVELS[vuln['Severity']]['score'];
if (vuln.Metadata && vuln.Metadata.NVD && vuln.Metadata.NVD.CVSSv2 && vuln.Metadata.NVD.CVSSv2.Score) {
score = vuln.Metadata.NVD.CVSSv2.Score;
}
var imageId = feature.AddedBy.split('.')[0];
vuln_obj = {
'name': vuln.Name,
'namespace': vuln.Namespace,
'description': vuln.Description,
'link': vuln.Link,
'severity': vuln.Severity,
'metadata': vuln.Metadata,
'feature': jQuery.extend({}, feature_obj),
'featureName': feature.Name,
'fixedInVersion': vuln.FixedBy,
'introducedInVersion': feature.Version,
'imageId': imageId,
'imageCommand': ImageMetadataService.getImageCommand($scope.image, imageId),
'score': score,
'expanded': false,
}
feature_vulnerabilities.push(vuln_obj)
$scope.securityVulnerabilities.push(vuln_obj);
if (severityMap[vuln.Severity] == undefined) {
severityMap[vuln.Severity] = 0;
}
severityMap[vuln.Severity]++;
});
}
feature_obj['vulnerabilities'] = feature_vulnerabilities
$scope.securityFeatures.push(feature_obj);
});
var levels = VulnerabilityService.getLevels();
for (var i = 0; i < levels.length; ++i) {
if (severityMap[levels[i].title]) {
$scope.priorityBreakdown.push({
'label': levels[i].title,
'value': severityMap[levels[i].title],
'color': levels[i].color,
})
}
}
}
buildOrderedVulnerabilities();
};
var loadImageVulnerabilities = function() {
if ($scope.securityResource) {
return;
}
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'imageid': $scope.image.id,
'vulnerabilities': true,
};
$scope.securityResource = ApiService.getRepoImageSecurityAsResource(params).get(function(resp) {
$scope.securityStatus = resp.status;
buildFeaturesAndVulns(resp.data);
buildChart();
return resp;
});
};
$scope.$watch('options.predicate', buildOrderedVulnerabilities);
$scope.$watch('options.reverse', buildOrderedVulnerabilities);
$scope.$watch('options.vulnFilter', buildOrderedVulnerabilities);
$scope.$watch('options.fixableVulns', buildOrderedVulnerabilities);
$scope.$watch('isEnabled', function(isEnabled) {
if ($scope.isEnabled && $scope.repository && $scope.image) {
loadImageVulnerabilities();
}
});
}
};
return directiveDefinitionObject;
});

View file

@ -0,0 +1,35 @@
/**
* An element which displays NVD vectors is an expanded format.
*/
angular.module('quay').directive('nvdVectorsDisplay', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/nvd-vectors-display.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'vectors': '@vectors',
},
controller: function($scope, $element, VulnerabilityService) {
$scope.parsedVectors = VulnerabilityService.parseVectorsString($scope.vectors);
$scope.getVectorTitle = function(vector) {
return VulnerabilityService.getVectorTitle(vector);
};
$scope.getVectorDescription = function(vector) {
return VulnerabilityService.getVectorDescription(vector);
};
$scope.getVectorOptions = function(vectorString) {
return VulnerabilityService.getVectorOptions(vectorString);
};
$scope.getVectorClasses = function(option, vectorString) {
return VulnerabilityService.getVectorClasses(option, vectorString);
};
}
};
return directiveDefinitionObject;
});

View file

@ -10,14 +10,15 @@
})
}]);
function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, ImageMetadataService, VulnerabilityService, Features) {
function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, ImageMetadataService, Features) {
var namespace = $routeParams.namespace;
var name = $routeParams.name;
var imageid = $routeParams.image;
$scope.imageSecurityCounter = 0;
$scope.imagePackageCounter = 0;
$scope.options = {
'vulnFilter': '',
'packageFilter': ''
'vulnFilter': ''
};
var loadImage = function() {
@ -46,57 +47,13 @@
loadRepository();
$scope.loadImageSecurity = function() {
if (!Features.SECURITY_SCANNER || $scope.securityResource) { return; }
if (!Features.SECURITY_SCANNER) { return; }
$scope.imageSecurityCounter++;
};
$scope.VulnerabilityLevels = VulnerabilityService.getLevels();
var params = {
'repository': namespace + '/' + name,
'imageid': imageid,
'vulnerabilities': true,
};
$scope.securityResource = ApiService.getRepoImageSecurityAsResource(params).get(function(resp) {
$scope.securityStatus = resp.status;
$scope.securityFeatures = [];
$scope.securityVulnerabilities = [];
if (resp.data && resp.data.Layer && resp.data.Layer.Features) {
resp.data.Layer.Features.forEach(function(feature) {
feature_obj = {
'name': feature.Name,
'namespace': feature.Namespace,
'version': feature.Version,
'addedBy': feature.AddedBy,
}
feature_vulnerabilities = []
if (feature.Vulnerabilities) {
feature.Vulnerabilities.forEach(function(vuln) {
vuln_obj = {
'name': vuln.Name,
'namespace': vuln.Namespace,
'description': vuln.Description,
'link': vuln.Link,
'severity': vuln.Severity,
'metadata': vuln.Metadata,
'feature': jQuery.extend({}, feature_obj),
'fixedBy': vuln.FixedBy,
'index': VulnerabilityService.LEVELS[vuln['Severity']]['index'],
}
feature_vulnerabilities.push(vuln_obj)
$scope.securityVulnerabilities.push(vuln_obj);
});
}
feature_obj['vulnerabilities'] = feature_vulnerabilities
$scope.securityFeatures.push(feature_obj);
});
}
return resp;
});
$scope.loadImagePackages = function() {
if (!Features.SECURITY_SCANNER) { return; }
$scope.imagePackageCounter++;
};
$scope.initializeTree = function() {

View file

@ -24,5 +24,34 @@ angular.module('quay').factory('ImageMetadataService', ['UtilService', function(
return UtilService.textToSafeHtml(metadataService.getFormattedCommand(image));
};
metadataService.getImageCommand = function(image, imageId) {
if (!image.__imageMap) {
image.__imageMap = {};
for (var i = 0; i < image.history.length; ++i) {
var cimage = image.history[i];
image.__imageMap[cimage.id] = cimage;
}
}
return getDockerfileCommand(image.__imageMap[imageId].command);
};
var getDockerfileCommand = function(command) {
if (!command) { return ''; }
// ["/bin/sh", "-c", "#(nop) RUN foo"]
var commandPrefix = '#(nop)'
if (command.length != 3) { return ''; }
if (command[0] != '/bin/sh' || command[1] != '-c') { return ''; }
var cmd = command[2];
if (cmd.substring(0, commandPrefix.length) != commandPrefix) {
return 'RUN ' + cmd;
}
return command[2].substr(commandPrefix.length + 1);
};
return metadataService;
}]);

View file

@ -5,10 +5,234 @@ angular.module('quay').factory('VulnerabilityService', ['Config', function(Confi
var vulnService = {};
vulnService.LEVELS = window.__vuln_priority;
var cvssSeverityMap = {};
vulnService.getSeverityForCVSS = function(score) {
if (cvssSeverityMap[score]) {
return cvssSeverityMap[score];
}
var levels = vulnService.getLevels();
for (var i = 0; i < levels.length; ++i) {
if (score >= levels[i].score) {
cvssSeverityMap[score] = levels[i];
return levels[i];
}
}
return vulnService.LEVELS['Unknown'];
};
vulnService.getCVSSColor = function(score) {
return vulnService.getSeverityForCVSS(score).color;
};
vulnService.getLevels = function() {
return Object.keys(vulnService.LEVELS).map(function(key) {
var levels = Object.keys(vulnService.LEVELS).map(function(key) {
return vulnService.LEVELS[key];
});
return levels.sort(function(a, b) {
return a.index - b.index;
});
};
vulnService.parseVectorsString = function(vectorsString) {
return vectorsString.split('/');
};
vulnService.getVectorTitle = function(vectorString) {
var parts = vectorString.split(':');
var vector = vulnService.NVD_VECTORS[parts[0]];
if (!vector) {
return '';
}
return vector.title;
};
vulnService.getVectorDescription = function(vectorString) {
var parts = vectorString.split(':');
var vector = vulnService.NVD_VECTORS[parts[0]];
if (!vector) {
return '';
}
return vector.description;
};
vulnService.getVectorClasses = function(option, vectorString) {
var parts = vectorString.split(':');
var vector = vulnService.NVD_VECTORS[parts[0]];
if (!vector) {
return '';
}
var classes = '';
if (option.id == parts[1]) {
classes += 'current-vector ';
} else {
classes += 'not-current-vector ';
}
classes += option.severity;
return classes
};
vulnService.getVectorOptions = function(vectorString) {
var parts = vectorString.split(':');
return vulnService.NVD_VECTORS[parts[0]].values;
};
vulnService.NVD_VECTORS = {
'AV': {
'title': 'Access Vector',
'description': 'This metric reflects how the vulnerability is exploited. The more remote an attacker can be to attack a host, the greater the vulnerability score.',
'values': [
{
'id': 'N',
'title': 'Network',
'description': 'A vulnerability exploitable with network access means the vulnerable software is bound to the network stack and the attacker does not require local network access or local access. Such a vulnerability is often termed "remotely exploitable". An example of a network attack is an RPC buffer overflow.',
'severity': 'high'
},
{
'id': 'A',
'title': 'Adjacent Network',
'description': 'A vulnerability exploitable with adjacent network access requires the attacker to have access to either the broadcast or collision domain of the vulnerable software. Examples of local networks include local IP subnet, Bluetooth, IEEE 802.11, and local Ethernet segment.',
'severity': 'medium'
},
{
'id': 'L',
'title': 'Local',
'description': 'A vulnerability exploitable with only local access requires the attacker to have either physical access to the vulnerable system or a local (shell) account. Examples of locally exploitable vulnerabilities are peripheral attacks such as Firewire/USB DMA attacks, and local privilege escalations (e.g., sudo).',
'severity': 'low'
}
]
},
'AC': {
'title': 'Access Complexity',
'description': 'This metric measures the complexity of the attack required to exploit the vulnerability once an attacker has gained access to the target system. For example, consider a buffer overflow in an Internet service: once the target system is located, the attacker can launch an exploit at will.',
'values': [
{
'id': 'L',
'title': 'Low',
'description': 'Specialized access conditions or extenuating circumstances do not exist making this easy to exploit',
'severity': 'high'
},
{
'id': 'M',
'title': 'Medium',
'description': 'The access conditions are somewhat specialized making this somewhat difficult to exploit',
'severity': 'medium'
},
{
'id': 'H',
'title': 'High',
'description': 'Specialized access conditions exist making this harder to exploit',
'severity': 'low'
}
]
},
'Au': {
'title': 'Authentication',
'description': 'This metric measures the number of times an attacker must authenticate to a target in order to exploit a vulnerability. This metric does not gauge the strength or complexity of the authentication process, only that an attacker is required to provide credentials before an exploit may occur.  The fewer authentication instances that are required, the higher the vulnerability score.',
'values': [
{
'id': 'N',
'title': 'None',
'description': 'Authentication is not required to exploit the vulnerability.',
'severity': 'high'
},
{
'id': 'S',
'title': 'Single',
'description': 'The vulnerability requires an attacker to be logged into the system (such as at a command line or via a desktop session or web interface).',
'severity': 'medium'
},
{
'id': 'M',
'title': 'Multiple',
'description': 'Exploiting the vulnerability requires that the attacker authenticate two or more times, even if the same credentials are used each time. An example is an attacker authenticating to an operating system in addition to providing credentials to access an application hosted on that system.',
'severity': 'low'
}
]
},
'C': {
'title': 'Confidentiality Impact',
'description': 'This metric measures the impact on confidentiality of a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones. Increased confidentiality impact increases the vulnerability score.',
'values': [
{
'id': 'C',
'title': 'Complete',
'description': 'There is total information disclosure, resulting in all system files being revealed. The attacker is able to read all of the system\'s data (memory, files, etc.)',
'severity': 'high'
},
{
'id': 'P',
'title': 'Partial',
'description': 'There is considerable informational disclosure. Access to some system files is possible, but the attacker does not have control over what is obtained, or the scope of the loss is constrained. An example is a vulnerability that divulges only certain tables in a database.',
'severity': 'medium'
},
{
'id': 'N',
'title': 'None',
'description': 'There is no impact to the confidentiality of the system.',
'severity': 'low'
}
]
},
'I': {
'title': 'Integrity Impact',
'description': 'This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and guaranteed veracity of information. Increased integrity impact increases the vulnerability score.',
'values': [
{
'id': 'C',
'title': 'Complete',
'description': 'There is a total compromise of system integrity. There is a complete loss of system protection, resulting in the entire system being compromised. The attacker is able to modify any files on the target system',
'severity': 'high'
},
{
'id': 'P',
'title': 'Partial',
'description': 'Modification of some system files or information is possible, but the attacker does not have control over what can be modified, or the scope of what the attacker can affect is limited. For example, system or application files may be overwritten or modified, but either the attacker has no control over which files are affected or the attacker can modify files within only a limited context or scope.',
'severity': 'medium'
},
{
'id': 'N',
'title': 'None',
'description': 'There is no impact to the integrity of the system.',
'severity': 'low'
}
]
},
'A': {
'title': 'Availability Impact',
'description': 'This metric measures the impact to availability of a successfully exploited vulnerability. Availability refers to the accessibility of information resources. Attacks that consume network bandwidth, processor cycles, or disk space all impact the availability of a system. Increased availability impact increases the vulnerability score.',
'values': [
{
'id': 'C',
'title': 'Complete',
'description': 'There is a total shutdown of the affected resource. The attacker can render the resource completely unavailable.',
'severity': 'high'
},
{
'id': 'P',
'title': 'Partial',
'description': 'There is reduced performance or interruptions in resource availability. An example is a network-based flood attack that permits a limited number of successful connections to an Internet service.',
'severity': 'medium'
},
{
'id': 'N',
'title': 'None',
'description': 'There is no impact to the availability of the system.',
'severity': 'low'
}
]
}
};
return vulnService;

View file

@ -27,7 +27,7 @@
<i class="fa fa-bug"></i>
</span>
<span class="cor-tab" tab-title="Packages" tab-target="#packages"
tab-init="loadImageSecurity()"
tab-init="loadImagePackages()"
quay-show="Features.SECURITY_SCANNER">
<i class="fa ci-package"></i>
</span>
@ -44,123 +44,12 @@
<!-- Vulnerabilities -->
<div id="vulnerabilities" class="tab-pane" quay-require="['SECURITY_SCANNER']">
<div class="resource-view" resource="securityResource" error-message="'Could not load security information for image'">
<div class="col-md-9">
<div class="filter-box floating" collection="securityVulnerabilities" filter-model="options.vulnFilter" filter-name="Vulnerabilities" ng-if="securityStatus == 'scanned' && securityVulnerabilities.length"></div>
<h3>Image Security</h3>
<div class="empty" ng-if="securityStatus == '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="securityStatus == '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="securityStatus == 'scanned' && !securityVulnerabilities.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 based images.
</div>
</div>
<div ng-if="securityStatus == 'scanned' && securityVulnerabilities.length">
<table class="co-table">
<thead>
<td style="width: 200px;">Vulnerability</td>
<td style="width: 200px;">Priority</td>
<td style="width: 200px;">Introduced by</td>
<td style="width: 200px;">Fixed by</td>
<td>Description</td>
</thead>
<tr ng-repeat="vulnerability in securityVulnerabilities | filter:options.vulnFilter | orderBy:'index'">
<td><a href="{{ vulnerability.link }}" target="_blank">{{ vulnerability.name }}</a></td>
<td>
<span class="vulnerability-priority-view" priority="vulnerability.severity"></span>
</td>
<td>{{ vulnerability.feature.name }} {{ vulnerability.feature.version }}</td>
<td>
<span ng-if="vulnerability.fixedBy">{{ vulnerability.feature.name }} {{ vulnerability.fixedBy }}</span>
</td>
<td>{{ vulnerability.description }}</td>
</tr>
</table>
<div class="empty" ng-if="(securityVulnerabilities | 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 class="image-vulnerability-view" repository="repository" image="image" is-enabled="imageSecurityCounter"></div>
</div>
<!-- Features -->
<div id="packages" class="tab-pane" quay-require="['SECURITY_SCANNER']">
<div class="resource-view" resource="securityResource" error-message="'Could not load image packages'">
<div class="filter-box floating" collection="securityFeatures" filter-model="options.packageFilter" filter-name="Features" ng-if="securityStatus == 'scanned' && securityFeatures.length"></div>
<h3>Image Packages</h3>
<div class="empty" ng-if="securityStatus == '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="securityStatus == '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>
<table class="co-table" ng-if="securityStatus == 'scanned'">
<thead>
<td>Package Name</td>
<td>Package Version</td>
<td>Package OS</td>
<td>Number of vulnerabilities</td>
</thead>
<tr ng-repeat="feature in securityFeatures | filter:options.packageFilter | orderBy:'Name'">
<td>{{ feature.name }}</td>
<td>{{ feature.version }}</td>
<td>{{ feature.namespace }}</td>
<td>{{ feature.vulnerabilities.length }}</td>
</tr>
</table>
<div class="empty" ng-if="(securityFeatures | filter:options.packageFilter).length == 0"
style="margin-top: 20px;">
<div class="empty-primary-msg">No matching packages found</div>
<div class="empty-secondary-msg" ng-if="options.packageFilter">
Please adjust your filter above.
</div>
</div>
</div>
<div class="image-feature-view" repository="repository" image="image" is-enabled="imagePackageCounter"></div>
</div>
</div>
</div>

View file

@ -5,6 +5,8 @@ PRIORITY_LEVELS = {
'title': 'Unknown',
'index': 6,
'level': 'info',
'color': '#9B9B9B',
'score': 0,
'description': 'Unknown is either a security problem that has not been assigned ' +
'to a priority yet or a priority that our system did not recognize',
@ -15,6 +17,8 @@ PRIORITY_LEVELS = {
'title': 'Negligible',
'index': 5,
'level': 'info',
'color': '#9B9B9B',
'score': 1,
'description': 'Negligible is technically a security problem, but is only theoretical ' +
'in nature, requires a very special situation, has almost no install base, ' +
@ -26,6 +30,8 @@ PRIORITY_LEVELS = {
'title': 'Low',
'index': 4,
'level': 'warning',
'color': 'rgb(204, 201, 46)',
'score': 3,
'description': 'Low is a security problem, but is hard to exploit due to environment, ' +
'requires a user-assisted attack, a small install base, or does very ' +
@ -38,6 +44,8 @@ PRIORITY_LEVELS = {
'value': 'Medium',
'index': 3,
'level': 'warning',
'color': 'rgb(252, 166, 87)',
'score': 6,
'description': 'Medium is a real security problem, and is exploitable for many people. ' +
'Includes network daemon denial of service attacks, cross-site scripting, ' +
@ -50,6 +58,8 @@ PRIORITY_LEVELS = {
'value': 'High',
'index': 2,
'level': 'warning',
'color': '#D64456',
'score': 9,
'description': 'High is a real problem, exploitable for many people in a default installation. ' +
'Includes serious remote denial of services, local root privilege escalations, ' +
@ -62,6 +72,8 @@ PRIORITY_LEVELS = {
'value': 'Critical',
'index': 1,
'level': 'error',
'color': '#D64456',
'score': 10,
'description': 'Critical is a world-burning problem, exploitable for nearly all people in ' +
'a installation of the package. Includes remote root privilege escalations, ' +
@ -74,6 +86,8 @@ PRIORITY_LEVELS = {
'value': 'Defcon1',
'index': 0,
'level': 'error',
'color': 'black',
'score': 11,
'description': 'Defcon1 is a Critical problem which has been manually highlighted ' +
'by the Quay team. It requires immediate attention.',