Update Quay Sec UI as per feedback from design team

Fixes #1281
This commit is contained in:
Joseph Schorr 2016-03-09 15:28:21 -05:00
parent c75fcfbd5e
commit 821b09daaf
20 changed files with 656 additions and 564 deletions

View file

@ -18,7 +18,7 @@ EXTERNAL_JS = [
]
EXTERNAL_CSS = [
'netdna.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.css',
'netdna.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.css',
'netdna.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css',
'fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,700',
's3.amazonaws.com/cdn.core-os.net/icons/core-icons.css'

View file

@ -1199,10 +1199,18 @@ a:focus {
margin-top: 4px;
font-size: 14px;
text-align: right;
display: inline-block;
}
.co-filter-box .filter-options input {
margin-right: 6px;
.co-filter-box .filter-options label input {
margin-right: 4px;
}
.co-filter-box.with-options > input {
display: inline-block;
width: 200px;
margin-right: 4px;
}
.co-check-bar {

View file

@ -98,6 +98,7 @@
.repo-panel-tags-element .security-scan-col .vuln-load-error {
color: #9B9B9B;
font-size: 12px;
cursor: default;
}
.repo-panel-tags-element .security-scan-col .no-vulns a {

View file

@ -26,20 +26,26 @@
.image-feature-view-element .donut-col {
padding-top: 20px;
text-align: center;
max-width: 250px;
display: inline-block;
vertical-align: top;
}
.image-feature-view-element #featureDonutChart {
display: inline-block;
}
.image-feature-view-element .summary-col {
font-size: 20px;
padding-top: 40px;
font-size: 18px;
display: inline-block;
vertical-align: top;
padding-top: 30px;
}
.image-feature-view-element .summary-col .title-item {
font-size: 24px;
margin-bottom: 40px;
margin-bottom: 30px;
}
.image-feature-view-element .summary-list {
@ -51,7 +57,7 @@
margin-right: 10px;
}
.image-feature-view-element .summary-list strong {
.image-feature-view-element .summary-list .package-item strong {
text-align: right;
width: 40px;
display: inline-block;

View file

@ -18,6 +18,9 @@
.image-vulnerability-view-element .donut-col {
padding-top: 20px;
text-align: center;
max-width: 250px;
display: inline-block;
vertical-align: top;
}
.image-vulnerability-view-element #vulnDonutChart {
@ -25,13 +28,20 @@
}
.image-vulnerability-view-element .summary-col {
font-size: 20px;
padding-top: 40px;
font-size: 18px;
display: inline-block;
vertical-align: top;
padding-top: 30px;
}
.image-vulnerability-view-element .summary-col .title-item {
font-size: 24px;
margin-bottom: 40px;
margin-bottom: 6px;
}
.image-vulnerability-view-element .summary-col .subtitle-item {
font-size: 22px;
margin-bottom: 6px;
}
.image-vulnerability-view-element .summary-list {
@ -43,7 +53,7 @@
margin-right: 10px;
}
.image-vulnerability-view-element .summary-list strong {
.image-vulnerability-view-element .summary-list li.severity-item strong {
text-align: right;
width: 40px;
display: inline-block;
@ -167,3 +177,19 @@
display: inline-block;
max-width: 1000px;
}
.image-vulnerability-view-element .asterisk {
vertical-align: super;
font-size: 9px;
margin-left: 2px;
}
.image-vulnerability-view-element .severity-note {
margin-bottom: 10px;
}
.image-vulnerability-view-element .severity-note .vulnerability-priority-view {
margin: 0px;
margin-left: 2px;
margin-right: 2px;
}

View file

@ -2,7 +2,7 @@
display: inline-block;
vertical-align: top;
max-width: 200px;
padding: 10px;
padding-right: 20px;
}
.nvd-vectors-display-element dt {

View file

@ -1,23 +1,7 @@
.vulnerability-priority-view-element {
cursor: default;
}
.vulnerability-priority-view-element i.fa {
margin-right: 4px;
}
.vulnerability-priority-view-element.Unknown,
.vulnerability-priority-view-element.Low,
.vulnerability-priority-view-element.Negligible {
color: #9B9B9B;
}
.vulnerability-priority-view-element.Medium {
color: #FCA657;
}
.vulnerability-priority-view-element.High,
.vulnerability-priority-view-element.Critical {
color: #D64456;
}
.vulnerability-priority-view-element.Defcon1 {
color: black;
font-weight: bold;
}
}

View file

@ -16,7 +16,7 @@
</div>
<!-- Scanned and has no features -->
<div ng-if="securityStatus == 'scanned' && !securityFeatures.length">
<div ng-if="securityStatus == 'scanned' && !featuresInfo.features.length">
<div class="empty" style="margin-top: 20px;">
<div class="empty-icon">
<i class="fa ci-package"></i>
@ -29,7 +29,7 @@
</div>
<!-- Scanned -->
<div ng-if="securityStatus == 'scanned' && securityFeatures.length">
<div ng-if="securityStatus == 'scanned' && featuresInfo.features.length">
<!-- Header -->
<div class="security-header row">
<div class="donut-col col-md-3">
@ -42,13 +42,13 @@
</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.
<li class="title-item">Quay Security Scanner has recognized <strong>{{ featuresInfo.features.length }}</strong> packages.</li>
<li ng-repeat="severity in featuresInfo.severityBreakdown">
<span class="package-item" ng-if="severity.label != 'None'">
<i class="fa ci-package" ng-style="{'color': severity.color}"></i> <strong>{{ severity.value }}</strong> packages with {{ severity.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 class="package-item" ng-if="severity.label == 'None'" style="margin-top: 20px; display: inline-block;">
<i class="fa ci-package" ng-style="{'color': severity.color}"></i> <strong>{{ severity.value }}</strong> packages with no vulnerabilities.
</span>
</li>
</ul>
@ -57,33 +57,33 @@
<!-- Filter -->
<span class="co-filter-box">
<span class="filter-message" ng-if="options.featureFilter">
Showing {{ orderedFeatures.entries.length }} of {{ securityFeatures.length }} packages
<span class="filter-message" ng-if="options.filter">
Showing {{ orderedFeatures.entries.length }} of {{ featuresInfo.features.length }} packages
</span>
<input class="form-control" type="text" ng-model="options.featureFilter" placeholder="Filter Packages...">
<input class="form-control" type="text" ng-model="options.filter" 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 ng-class="TableService.tablePredicateClass('name', options.predicate, options.reverse)">
<a href="javascript:void(0)" ng-click="TableService.orderBy('name', options)">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 ng-class="TableService.tablePredicateClass('score', options.predicate, options.reverse)">
<a href="javascript:void(0)" ng-click="TableService.orderBy('score', options)">Vulnerabilities</a>
</td>
<td class="hidden-xs hidden-sm hidden-md"
ng-class="tablePredicateClass('leftoverScore', options.predicate, options.reverse)"
ng-class="TableService.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>
<a href="javascript:void(0)" ng-click="TableService.orderBy('leftoverScore', options)">Remaining after upgrade</a>
</td>
<td ng-class="tablePredicateClass('fixableScore', options.predicate, options.reverse)">
<a href="javascript:void(0)" ng-click="orderBy('fixableScore')"
<td ng-class="TableService.tablePredicateClass('fixableScore', options.predicate, options.reverse)">
<a href="javascript:void(0)" ng-click="TableService.orderBy('fixableScore', options)"
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">
@ -104,13 +104,13 @@
<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}">
<span ng-style="{'color': feature.severityBreakdown[0].color}">
<i class="fa fa fa-exclamation-triangle"></i>
{{ feature.primarySeverity.count }}
{{ feature.primarySeverity.title }}
{{ feature.primarySeverity.value }}
{{ feature.primarySeverity.label }}
</span>
<span bo-if="feature.vulnCount - feature.primarySeverity.count > 0">
+ {{ feature.vulnCount - feature.primarySeverity.count }} additional
<span bo-if="feature.vulnCount - feature.primarySeverity.value > 0">
+ {{ feature.vulnCount - feature.primarySeverity.value }} additional
</span>
</span>
</td>
@ -118,18 +118,18 @@
<span class="empty" bo-if="feature.vulnCount == 0">
(N/A)
</span>
<span class="no-vulns" bo-if="feature.vulnCount != 0 && feature.leftoverBreakdown.length == 0">
<span class="no-vulns" bo-if="feature.vulnCount != 0 && feature.leftoverCount == 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 class="vuln-summary" bo-if="feature.vulnCount != 0 && feature.leftoverCount != 0">
<span ng-style="{'color': feature.primaryLeftover.color}">
<i class="fa fa-arrow-circle-right"></i>
{{ feature.primaryLeftover.count }}
{{ feature.primaryLeftover.title }}
{{ feature.primaryLeftover.value }}
{{ feature.primaryLeftover.label }}
</span>
<span bo-if="feature.leftoverCount - feature.primaryLeftover.count > 0">
+ {{ feature.leftoverCount - feature.primaryLeftover.count }} additional
<span bo-if="feature.leftoverCount - feature.primaryLeftover.value > 0">
+ {{ feature.leftoverCount - feature.primaryLeftover.value }} additional
</span>
</span>
</td>
@ -137,8 +137,11 @@
<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"
<span class="empty" bo-if="feature.fixableScore == 0">
(No changes)
</span>
<span bo-if="feature.vulnCount > 0 && feature.fixableScore > 0">
<span class="strength-indicator" value="feature.fixableScore" maximum="featuresInfo.highestFixableScore"
log-base="2"></span>
</span>
</td>
@ -152,7 +155,7 @@
</tr>
</tbody>
</table>
<div class="empty" ng-if="securityFeatures.length && !orderedFeatures.entries.length"
<div class="empty" ng-if="featuresInfo.features.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>

View file

@ -16,7 +16,7 @@
</div>
<!-- Scanned and has no features -->
<div ng-if="securityStatus == 'scanned' && !securityFeatures.length">
<div ng-if="securityStatus == 'scanned' && !vulnerabilitiesInfo.features.length">
<div class="empty" style="margin-top: 20px;">
<div class="empty-icon">
<i class="fa fa-bug"></i>
@ -29,10 +29,10 @@
</div>
<!-- Scanned and has features -->
<div ng-if="securityStatus == 'scanned' && securityFeatures.length">
<div ng-if="securityStatus == 'scanned' && vulnerabilitiesInfo.features.length">
<!-- Header -->
<div class="security-header row">
<div class="donut-col col-md-3">
<div class="security-header">
<div class="donut-col">
<div id="vulnDonutChart" style="position: relative;">
<svg style="height:250px; width:250px"></svg>
<span class="donut-icon">
@ -40,51 +40,56 @@
</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>
<div class="summary-col">
<ul class="summary-list" ng-if="vulnerabilitiesInfo.severityBreakdown.length">
<li class="title-item">Quay Security Scanner has detected <strong>{{ vulnerabilitiesInfo.vulnerabilities.length }}</strong> vulnerabilities.</li>
<li class="subtitle-item" ng-if="vulnerabilitiesInfo.fixable.length">
Patches are available for <strong>{{ vulnerabilitiesInfo.fixable.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 style="margin-bottom: 30px"></li>
<li class="severity-item" ng-repeat="severity in vulnerabilitiesInfo.severityBreakdown">
<i class="fa fa-exclamation-triangle" ng-style="{'color': severity.color}"></i> <strong>{{ severity.value }}</strong> {{ severity.label }}-level vulnerabilities.
</li>
</ul>
<div ng-if="!priorityBreakdown.length">
<div ng-if="!vulnerabilitiesInfo.severityBreakdown.length">
Quay Security Scanner has detected no vulnerabilities in this image.
</div>
</div>
</div>
<!-- Filter -->
<span class="co-filter-box" ng-show="priorityBreakdown.length">
<span class="filter-message" ng-if="options.vulnFilter || options.fixableVulns">
Showing {{ orderedVulnerabilities.entries.length }} of {{ securityVulnerabilities.length }} Vulnerabilities
<span class="co-filter-box with-options" ng-show="vulnerabilitiesInfo.vulnerabilities.length">
<span class="filter-message" ng-if="options.filter || options.fixableVulns">
Showing {{ orderedVulnerabilities.entries.length }} of {{ vulnerabilitiesInfo.vulnerabilities.length }} Vulnerabilities
</span>
<input class="form-control" type="text" ng-model="options.vulnFilter" placeholder="Filter Vulnerabilities...">
<input class="form-control" type="text" ng-model="options.filter" placeholder="Filter Vulnerabilities...">
<div class="filter-options">
<label><input type="checkbox" ng-model="options.fixableVulns">Only display vulnerabilities with fixes</label>
<label><input type="checkbox" ng-model="options.fixableVulns">Only show fixable</label>
</div>
</span>
<h3>Image Vulnerabilities</h3>
<!-- Table -->
<div class="empty" ng-if="!securityVulnerabilities.length"
<div class="empty" ng-if="!vulnerabilitiesInfo.vulnerabilities.length"
style="margin-top: 20px;">
<div class="empty-primary-msg">No vulnerabilities found.</div>
<div class="empty-secondary-msg">Quay Security Scanner has detected no vulnerabilities in this image.</div>
</div>
<table class="co-table" ng-show="priorityBreakdown.length">
<table class="co-table" ng-show="vulnerabilitiesInfo.vulnerabilities.length">
<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 ng-class="TableService.tablePredicateClass('name', options.predicate, options.reverse)">
<a href="javascript:void(0)" ng-click="TableService.orderBy('name', options)">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 ng-class="TableService.tablePredicateClass('score', options.predicate, options.reverse)">
<a href="javascript:void(0)" ng-click="TableService.orderBy('score', options)">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 class="hidden-xs" ng-class="TableService.tablePredicateClass('featureName', options.predicate, options.reverse)">
<a href="javascript:void(0)" ng-click="TableService.orderBy('featureName', options)">Package</a>
</td>
<td class="hidden-xs">Current version</td>
<td class="hidden-xs hidden-sm">Fixed in version</td>
@ -108,17 +113,22 @@
</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 bo-if="vuln.cvssScore && !vuln.scoreDivergence">
<span class="cvss-text" bo-text="vuln.cvssScore"></span>
<span class="cvss"><span bo-style="{'width': (vuln.cvssScore * 10) + '%', 'background-color': vuln.cvssColor}"></span>
</span>
</span>
<span bo-if="!vuln.metadata.NVD.CVSSv2.Score">
<span bo-if="!vuln.cvssScore || vuln.scoreDivergence" data-title="{{ getSeverityTooltip(vuln) }}" data-container="body" bs-tooltip>
<span class="vulnerability-priority-view" priority="vuln.severity"></span>
<span class="asterisk" ng-if="vuln.scoreDivergence == 'adjusted-lower'" ng-style="{'color': vuln.severityInfo.color}">
<i class="fa fa-asterisk"></i>
</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 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>
@ -137,7 +147,12 @@
<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>Introduced in version:</td>
<td>
<span bo-text="vuln.introducedInVersion"></span>
</td>
</tr>
<tr>
<td>Fixed in version:</td>
<td>
@ -152,6 +167,15 @@
</table>
</div>
<div class="severity-note" bo-if="vuln.scoreDivergence">
<div class="subtitle">Severity note</div>
<span class="description">
Note that this vulnerability was originally given a CVSSv2 score of <strong bo-text="vuln.cvssScore"></strong> by NVD but was subsequently reclassified as
<span class="vulnerability-priority-view" priority="vuln.severity"></span>
by <span bo-text="getDistro(vuln)"></span>
</span>
</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>
@ -163,7 +187,7 @@
</tr>
</tbody>
</table>
<div class="empty" ng-if="securityVulnerabilities.length && !orderedVulnerabilities.entries.length"
<div class="empty" ng-if="vulnerabilitiesInfo.vulnerabilities.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>

View file

@ -169,7 +169,7 @@
</span>
<!-- Vulns -->
<span ng-if="getTagVulnerabilities(tag).status == 'scanned' && getTagVulnerabilities(tag).hasVulnerabilities"
<span ng-if="getTagVulnerabilities(tag).status == 'scanned' && getTagVulnerabilities(tag).hasFeatures && 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=vulnerabilities"

View file

@ -1,4 +1,4 @@
<span class="vulnerability-priority-view-element" ng-class="priority">
<span class="vulnerability-priority-view-element" ng-style="{'color': color}">
<i class="fa fa-exclamation-triangle"></i>
<span ng-transclude/>
{{ priority }}

View file

@ -35,6 +35,7 @@ angular.module('quay').directive('repoPanelTags', function () {
$scope.tagActionHandler = null;
$scope.showingHistory = false;
$scope.tagsPerPage = 25;
$scope.imageVulnerabilities = {};
$scope.defcon1 = {};
$scope.hasDefcon1 = false;
@ -153,13 +154,7 @@ angular.module('quay').directive('repoPanelTags', function () {
});
$scope.loadImageVulnerabilities = function(image_id, imageData) {
var params = {
'imageid': image_id,
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'vulnerabilities': true,
};
ApiService.getRepoImageSecurity(null, params).then(function(resp) {
VulnerabilityService.loadImageVulnerabilities($scope.repository, image_id, function(resp) {
imageData.loading = false;
imageData.status = resp['status'];
@ -171,36 +166,27 @@ angular.module('quay').directive('repoPanelTags', function () {
'index': 100000
};
var hasFeatures = false;
if (resp.data && resp.data.Layer && resp.data.Layer.Features) {
resp.data.Layer.Features.forEach(function(feature) {
hasFeatures = true;
VulnerabilityService.forEachVulnerability(resp, function(vuln) {
if (VulnerabilityService.LEVELS[vuln.Severity].index == 0) {
$scope.defcon1[vuln.ID] = v;
$scope.hasDefcon1 = true;
}
if (feature.Vulnerabilities) {
feature.Vulnerabilities.forEach(function(vuln) {
if (VulnerabilityService.LEVELS[vuln.Severity].index == 0) {
$scope.defcon1[vuln.ID] = v;
$scope.hasDefcon1 = true;
}
if (VulnerabilityService.LEVELS[vuln.Severity].index < highest.index) {
highest = {
'Priority': vuln.Severity,
'Count': 1,
'index': VulnerabilityService.LEVELS[vuln.Severity].index
}
} else if (VulnerabilityService.LEVELS[vuln.Severity].index == highest.index) {
highest['Count']++;
}
vulnerabilities.push(vuln);
});
if (VulnerabilityService.LEVELS[vuln.Severity].index < highest.index) {
highest = {
'Priority': vuln.Severity,
'Count': 1,
'index': VulnerabilityService.LEVELS[vuln.Severity].index
}
});
}
} else if (VulnerabilityService.LEVELS[vuln.Severity].index == highest.index) {
highest['Count']++;
}
vulnerabilities.push(vuln);
});
imageData.hasFeatures = VulnerabilityService.hasFeatures(resp);
imageData.hasVulnerabilities = !!vulnerabilities.length;
imageData.hasFeatures = hasFeatures;
imageData.vulnerabilities = vulnerabilities;
imageData.highestVulnerability = highest;
}

View file

@ -13,87 +13,28 @@ angular.module('quay').directive('imageFeatureView', function () {
'image': '=image',
'isEnabled': '=isEnabled'
},
controller: function($scope, $element, Config, ApiService, VulnerabilityService, AngularViewArray, ImageMetadataService) {
var imageMap = null;
$scope.securityFeatures = [];
$scope.featureBreakdown = [];
controller: function($scope, $element, Config, ApiService, VulnerabilityService, AngularViewArray, ImageMetadataService, TableService) {
$scope.options = {
'featureFilter': null,
'filter': null,
'predicate': 'fixableScore',
'reverse': false,
};
$scope.tablePredicateClass = function(name, predicate, reverse) {
if (name != predicate) {
return '';
}
$scope.TableService = TableService;
return 'current ' + (reverse ? 'reversed' : '');
};
$scope.orderBy = function(predicate) {
if (predicate == $scope.options.predicate) {
$scope.options.reverse = !$scope.options.reverse;
var buildOrderedFeatures = function() {
if (!$scope.featuresInfo) {
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 features = $scope.featuresInfo.features;
$scope.orderedFeatures = TableService.buildOrderedItems(features, $scope.options,
['name', 'version', 'imageId'],
['score', 'fixableScore', 'leftoverScore'])
};
var buildChart = function() {
var chartData = $scope.featureBreakdown;
var chartData = $scope.featuresInfo.severityBreakdown;
var colors = [];
for (var i = 0; i < chartData.length; ++i) {
colors.push(chartData[i].color);
@ -122,181 +63,16 @@ angular.module('quay').directive('imageFeatureView', function () {
});
};
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 = null;
if (feature.AddedBy) {
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,
});
}
if (greenCount > 0) {
$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.securityResource = VulnerabilityService.loadImageVulnerabilitiesAsResource($scope.repository, $scope.image.id, function(resp) {
$scope.securityStatus = resp.status;
buildFeatures(resp.data);
$scope.featuresInfo = VulnerabilityService.buildFeaturesInfo($scope.image, resp);
buildOrderedFeatures();
buildChart();
return resp;
});
@ -304,7 +80,7 @@ angular.module('quay').directive('imageFeatureView', function () {
$scope.$watch('options.predicate', buildOrderedFeatures);
$scope.$watch('options.reverse', buildOrderedFeatures);
$scope.$watch('options.featureFilter', buildOrderedFeatures);
$scope.$watch('options.filter', buildOrderedFeatures);
$scope.$watch('isEnabled', function(isEnabled) {
if ($scope.isEnabled && $scope.repository && $scope.image) {

View file

@ -13,23 +13,8 @@ angular.module('quay').directive('imageViewLayer', function () {
'image': '=image',
'images': '=images'
},
controller: function($scope, $element) {
$scope.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);
};
controller: function($scope, $element, ImageMetadataService) {
$scope.getDockerfileCommand = ImageMetadataService.getDockerfileCommand;
$scope.getClass = function() {
var index = $.inArray($scope.image, $scope.images);

View file

@ -13,99 +13,53 @@ angular.module('quay').directive('imageVulnerabilityView', function () {
'image': '=image',
'isEnabled': '=isEnabled'
},
controller: function($scope, $element, Config, ApiService, VulnerabilityService, AngularViewArray, ImageMetadataService) {
var imageMap = null;
$scope.securityVulnerabilities = [];
controller: function($scope, $element, Config, ApiService, VulnerabilityService, AngularViewArray, ImageMetadataService, TableService) {
$scope.options = {
'vulnFilter': null,
'filter': 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.TableService = TableService;
$scope.toggleDetails = function(vuln) {
vuln.expanded = !vuln.expanded;
};
var buildOrderedVulnerabilities = function() {
var vulnerabilities = $scope.securityVulnerabilities.slice(0);
$scope.getDistro = function(vuln) {
return vuln['namespace'].split(':', 1);
};
$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;
}
}
$scope.getSeverityTooltip = function(vuln) {
var distro = vuln['namespace'].split(':', 1);
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();
if (vuln.scoreDivergence != 'adjusted-lower') {
return 'Marked with a ' + vuln['severity'] + ' severity by ' + distro;
}
$scope.orderedVulnerabilities.setVisible(true);
return 'Note: This vulnerability was originally given a CVSSv2 score ' +
'of ' + vuln['cvssScore'] + ' by NVD, but was subsequently reclassifed as a ' +
vuln['severity'] + ' issue by ' + distro;
};
var buildOrderedVulnerabilities = function() {
if (!$scope.vulnerabilitiesInfo) {
return;
}
var vulnerabilities = $scope.vulnerabilitiesInfo.vulnerabilities;
$scope.orderedVulnerabilities = TableService.buildOrderedItems(vulnerabilities, $scope.options,
['name', 'featureName', 'imageCommand'],
['score'],
function(item) {
return !$scope.options.fixableVulns || item['fixedInVersion'];
})
};
var buildChart = function() {
var chartData = $scope.priorityBreakdown;
if ($scope.priorityBreakdown.length == 0) {
var chartData = $scope.vulnerabilitiesInfo.severityBreakdown;
if (chartData.length == 0) {
chartData = [{
'label': 'None',
'value': 1,
@ -141,98 +95,16 @@ angular.module('quay').directive('imageVulnerabilityView', function () {
});
};
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 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 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[severity['index']] == undefined) {
severityMap[severity['index']] = 0;
}
severityMap[severity['index']]++;
});
}
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]['index']]) {
$scope.priorityBreakdown.push({
'label': levels[i].title,
'value': severityMap[levels[i]['index']],
'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.securityResource = VulnerabilityService.loadImageVulnerabilitiesAsResource($scope.repository, $scope.image.id, function(resp) {
$scope.securityStatus = resp.status;
buildFeaturesAndVulns(resp.data);
$scope.vulnerabilitiesInfo = VulnerabilityService.buildVulnerabilitiesInfo($scope.image, resp);
buildOrderedVulnerabilities();
buildChart();
return resp;
});
@ -240,7 +112,7 @@ angular.module('quay').directive('imageVulnerabilityView', function () {
$scope.$watch('options.predicate', buildOrderedVulnerabilities);
$scope.$watch('options.reverse', buildOrderedVulnerabilities);
$scope.$watch('options.vulnFilter', buildOrderedVulnerabilities);
$scope.$watch('options.filter', buildOrderedVulnerabilities);
$scope.$watch('options.fixableVulns', buildOrderedVulnerabilities);
$scope.$watch('isEnabled', function(isEnabled) {

View file

@ -11,7 +11,14 @@ angular.module('quay').directive('vulnerabilityPriorityView', function () {
scope: {
'priority': '=priority'
},
controller: function($scope, $element) {
controller: function($scope, $element, VulnerabilityService) {
$scope.color = '';
$scope.$watch('priority', function(priority) {
if (priority) {
$scope.color = VulnerabilityService.LEVELS[priority]['color'];
}
});
}
};
return directiveDefinitionObject;

View file

@ -42,24 +42,22 @@ angular.module('quay').factory('ImageMetadataService', ['UtilService', function(
return null;
}
return getDockerfileCommand(found.command);
return metadataService.getDockerfileCommand(found.command);
};
var getDockerfileCommand = function(command) {
metadataService.getDockerfileCommand = function(command) {
if (!command) { return ''; }
command = command.join(' ').split(' ')
// ["/bin/sh", "-c", "#(nop) RUN foo"]
var commandPrefix = '#(nop)';
if (command.length != 3) { return ''; }
// ["/bin/sh", "-c", "#(nop)", "RUN", "foo"]
if (command[0] != '/bin/sh' || command[1] != '-c') { return ''; }
var cmd = command[2];
if (cmd.substring(0, commandPrefix.length) != commandPrefix) {
return 'RUN ' + cmd;
var commandPrefix = '#(nop)';
if (command[2] != commandPrefix) {
return 'RUN ' + command.slice(2).join(' ');
}
return command[2].substr(commandPrefix.length + 1);
return command.slice(3).join(' ');
};
return metadataService;

View file

@ -0,0 +1,89 @@
/**
* Service which provides helper methods for constructing and managing tabular data.
*/
angular.module('quay').factory('TableService', ['AngularViewArray', function(AngularViewArray) {
var tableService = {};
tableService.tablePredicateClass = function(name, predicate, reverse) {
if (name != predicate) {
return '';
}
return 'current ' + (reverse ? 'reversed' : '');
};
tableService.orderBy = function(predicate, options) {
if (predicate == options.predicate) {
options.reverse = !options.reverse;
return;
}
options.reverse = false;
options.predicate = predicate;
};
tableService.buildOrderedItems = function(items, options, filterFields, numericFields, opt_extrafilter) {
var orderedItems = AngularViewArray.create();
items.forEach(function(item) {
var filter = options.filter;
if (filter) {
var found = false;
for (var i = 0; i < filterFields.length; ++i) {
var filterField = filterFields[i];
if (item[filterField].indexOf(filter) >= 0) {
found = true;
break;
}
}
if (!found) {
return;
}
}
if (opt_extrafilter && !opt_extrafilter(item)) {
return;
}
orderedItems.push(item);
});
orderedItems.entries.sort(function(a, b) {
var left = a[options['predicate']];
var right = b[options['predicate']];
for (var i = 0; i < numericFields.length; ++i) {
var numericField = numericFields[i];
if (options['predicate'] == numericField) {
left = left * 1;
right = right * 1;
break;
}
}
if (left == null) {
left = '0.00';
}
if (right == null) {
right = '0.00';
}
if (left == right) {
return 0;
}
return left > right ? -1 : 1;
});
if (options['reverse']) {
orderedItems.entries.reverse();
}
orderedItems.setVisible(true);
return orderedItems;
};
return tableService;
}]);

View file

@ -1,10 +1,333 @@
/**
* Service which provides helper methods for working with the vulnerability system.
*/
angular.module('quay').factory('VulnerabilityService', ['Config', function(Config) {
angular.module('quay').factory('VulnerabilityService', ['Config', 'ApiService', 'ImageMetadataService',
function(Config, ApiService, ImageMetadataService) {
var vulnService = {};
vulnService.LEVELS = window.__vuln_priority;
vulnService.getUnadjustedScoreOf = function(vuln) {
var severity = vulnService.LEVELS[vuln['Severity']];
return severity.score;
};
vulnService.getCVSSScoreOf = function(vuln) {
if (vuln.Metadata && vuln.Metadata.NVD && vuln.Metadata.NVD.CVSSv2 && vuln.Metadata.NVD.CVSSv2.Score) {
return vuln.Metadata.NVD.CVSSv2.Score;
}
return null;
}
vulnService.buildVulnerabilitiesInfo = function(image, resp) {
var levels = vulnService.getLevels();
var severityCountMap = {};
levels.forEach(function(level) {
severityCountMap[level['index']] = 0;
});
var fixable = [];
var vulnerabilities = [];
var featuresInfo = vulnService.buildFeaturesInfo(image, resp);
featuresInfo.features.forEach(function(feature) {
if (feature.vulnerabilities) {
vulnerabilities = vulnerabilities.concat(feature.vulnerabilities);
fixable = fixable.concat(feature.fixable);
feature.severityBreakdown.forEach(function(level) {
severityCountMap[level['index']] += level['value'];
});
}
});
var severityBreakdown = [];
levels.forEach(function(level) {
if (severityCountMap[level['index']]) {
severityBreakdown.push({
'index': level['index'],
'label': level['title'],
'value': severityCountMap[level['index']],
'color': level['color']
});
}
});
return {
'vulnerabilities': vulnerabilities,
'fixable': fixable,
'severityBreakdown': severityBreakdown,
'features': featuresInfo.features,
}
};
vulnService.buildVulnerabilitiesInfoForFeature = function(image, feature) {
var levels = vulnService.getLevels();
var vulnerabilities = [];
var fixable = [];
var severityCountMap = {};
var fixableCountMap = {};
levels.forEach(function(level) {
severityCountMap[level['index']] = 0;
fixableCountMap[level['index']] = 0;
});
var score = 0;
var fixableScore = 0;
var highestSeverityIndex = levels.length;
if (feature.Vulnerabilities) {
var addedByImageId = feature.AddedBy ? feature.AddedBy.split('.')[0] : null;
feature.Vulnerabilities.forEach(function(vuln) {
var severity = vulnService.LEVELS[vuln['Severity']];
var cvssScore = vulnService.getCVSSScoreOf(vuln);
var unadjustedScore = vulnService.getUnadjustedScoreOf(vuln);
var currentScore = unadjustedScore;
var scoreDivergence = null;
// If the vulnerability has a CVSS score, ensure it is within 2 levels of the severity
// score from the distro. If it is out of that band, then we have a score divergence
// and use the distro's score directly.
if (cvssScore != null) {
if (cvssScore - unadjustedScore > 2) {
scoreDivergence = 'adjusted-lower';
} else if (unadjustedScore > cvssScore) {
scoreDivergence = 'adjusted-higher';
} else {
currentScore = cvssScore;
}
}
var exponentialScore = Math.pow(2, currentScore) + 0.1;
var vuln_object = {
'score': exponentialScore,
'scoreDivergence': scoreDivergence,
'severityInfo': severity,
'cvssScore': cvssScore,
'cvssColor': vulnService.getCVSSColor(cvssScore),
'name': vuln.Name,
'namespace': vuln.Namespace,
'description': vuln.Description,
'link': vuln.Link,
'severity': vuln.Severity,
'metadata': vuln.Metadata,
'featureName': feature.Name,
'fixedInVersion': vuln.FixedBy,
'introducedInVersion': feature.Version,
'imageId': addedByImageId,
'imageCommand': ImageMetadataService.getImageCommand(image, addedByImageId),
'expanded': false
};
// Save the highest vulnerability severity for this feature.
highestSeverityIndex = Math.min(severity['index'], highestSeverityIndex)
// Add the score and (if necessary) the fixable scores.
score += exponentialScore;
severityCountMap[severity['index']]++
vulnerabilities.push(vuln_object);
if (vuln.FixedBy) {
fixableCountMap[severity['index']]++
fixableScore += exponentialScore;
fixable.push(vuln_object)
}
});
}
// Calculate the breakdown of the vulnerabilities by severity.
var severityBreakdown = [];
var fixableBreakdown = [];
var leftoverBreakdown = [];
levels.forEach(function(level) {
if (severityCountMap[level['index']]) {
severityBreakdown.push({
'index': level['index'],
'label': level['title'],
'value': severityCountMap[level['index']],
'color': level['color']
});
if (fixableCountMap[level['index']]) {
fixableBreakdown.push({
'index': level['index'],
'label': level['title'],
'value': fixableCountMap[level['index']],
'color': level['color']
});
}
var leftoverCount = severityCountMap[level['index']] - fixableCountMap[level['index']];
if (leftoverCount) {
leftoverBreakdown.push({
'index': level['index'],
'label': level['title'],
'value': leftoverCount,
'color': level['color']
});
}
}
});
return {
'vulnerabilities': vulnerabilities,
'fixable': fixable,
'severityBreakdown': severityBreakdown,
'fixableBreakdown': fixableBreakdown,
'leftoverBreakdown': leftoverBreakdown,
'score': score,
'fixableScore': fixableScore,
'highestSeverity': levels[highestSeverityIndex],
};
};
vulnService.buildFeaturesInfo = function(image, resp) {
var features = [];
var severityCountMap = {};
var highestFixableScore = 0;
var levels = vulnService.getLevels();
levels.forEach(function(level) {
severityCountMap[level['index']] = 0;
});
vulnService.forEachFeature(resp, function(feature) {
// Calculate the scores and breakdowns for all the vulnerabilities under feature.
var vulnerabilityInfo = vulnService.buildVulnerabilitiesInfoForFeature(image, feature);
var addedByImageId = feature.AddedBy ? feature.AddedBy.split('.')[0] : null;
var feature_obj = {
'name': feature.Name,
'namespace': feature.Namespace,
'version': feature.Version,
'addedBy': feature.AddedBy,
'imageId': addedByImageId,
'imageCommand': ImageMetadataService.getImageCommand(image, addedByImageId),
'vulnCount': vulnerabilityInfo.vulnerabilities.length,
'severityBreakdown': vulnerabilityInfo.severityBreakdown,
'fixableBreakdown': vulnerabilityInfo.fixableBreakdown,
'leftoverBreakdown': vulnerabilityInfo.leftoverBreakdown,
'score': vulnerabilityInfo.score,
'fixableCount': vulnerabilityInfo.fixable.length,
'leftoverCount': vulnerabilityInfo.vulnerabilities.length - vulnerabilityInfo.fixable.length,
'fixableScore': vulnerabilityInfo.fixableScore,
'leftoverScore': vulnerabilityInfo.score - vulnerabilityInfo.fixableScore,
'primarySeverity': vulnerabilityInfo.severityBreakdown[0],
'primaryLeftover': vulnerabilityInfo.leftoverBreakdown[0],
'vulnerabilities': vulnerabilityInfo.vulnerabilities,
'fixable': vulnerabilityInfo.fixable
};
if (vulnerabilityInfo.highestSeverity) {
severityCountMap[vulnerabilityInfo.highestSeverity['index']]++;
} else {
// Ensures that features with no vulns are always at the bottom of the table in the
// default sort by fixableScore.
feature_obj['fixableScore'] = -1;
feature_obj['leftoverScore'] = -1;
}
highestFixableScore = Math.max(highestFixableScore, vulnerabilityInfo.fixableScore);
features.push(feature_obj);
});
// Calculate the breakdown of each severity level for the features.
var totalCount = features.length;
var severityBreakdown = [];
levels.forEach(function(level) {
if (!severityCountMap[level['index']]) {
return;
}
totalCount -= severityCountMap[level['index']];
severityBreakdown.push({
'index': level['index'],
'label': level['title'],
'value': severityCountMap[level['index']],
'color': level['color']
});
});
if (totalCount > 0) {
severityBreakdown.push({
'index': levels.length,
'label': 'None',
'value': totalCount,
'color': '#2FC98E'
});
}
return {
'features': features,
'severityBreakdown': severityBreakdown,
'highestFixableScore': highestFixableScore
}
};
vulnService.loadImageVulnerabilitiesAsResource = function(repo, image_id, result) {
var params = {
'repository': repo.namespace + '/' + repo.name,
'imageid': image_id,
'vulnerabilities': true,
};
return ApiService.getRepoImageSecurityAsResource(params).get(result);
};
vulnService.loadImageVulnerabilities = function(repo, image_id, result, reject) {
var params = {
'imageid': image_id,
'repository': repo.namespace + '/' + repo.name,
'vulnerabilities': true,
};
ApiService.getRepoImageSecurity(null, params).then(result, reject);
};
vulnService.hasFeatures = function(resp) {
return resp.data && resp.data.Layer && resp.data.Layer.Features && resp.data.Layer.Features.length;
};
vulnService.forEachFeature = function(resp, callback) {
if (!vulnService.hasFeatures(resp)) {
return;
}
resp.data.Layer.Features.forEach(callback);
};
vulnService.forEachVulnerability = function(resp, callback) {
if (!vulnService.hasFeatures(resp)) {
return;
}
vulnService.forEachFeature(resp, function(feature) {
if (feature.Vulnerabilities) {
feature.Vulnerabilities.forEach(callback);
}
});
};
var cvssSeverityMap = {};
vulnService.getSeverityForCVSS = function(score) {
@ -24,6 +347,10 @@ angular.module('quay').factory('VulnerabilityService', ['Config', function(Confi
};
vulnService.getCVSSColor = function(score) {
if (score == null) {
return null;
}
return vulnService.getSeverityForCVSS(score).color;
};

View file

@ -30,7 +30,7 @@ PRIORITY_LEVELS = {
'title': 'Low',
'index': 4,
'level': 'warning',
'color': 'rgb(204, 201, 46)',
'color': '#F8CA1C',
'score': 3,
'description': 'Low is a security problem, but is hard to exploit due to environment, ' +
@ -44,7 +44,7 @@ PRIORITY_LEVELS = {
'value': 'Medium',
'index': 3,
'level': 'warning',
'color': 'rgb(252, 166, 87)',
'color': '#FCA657',
'score': 6,
'description': 'Medium is a real security problem, and is exploitable for many people. ' +
@ -58,7 +58,7 @@ PRIORITY_LEVELS = {
'value': 'High',
'index': 2,
'level': 'warning',
'color': '#D64456',
'color': '#F77454',
'score': 9,
'description': 'High is a real problem, exploitable for many people in a default installation. ' +