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

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