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

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