parent
c75fcfbd5e
commit
821b09daaf
20 changed files with 656 additions and 564 deletions
|
@ -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;
|
||||
|
|
89
static/js/services/table-service.js
Normal file
89
static/js/services/table-service.js
Normal 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;
|
||||
}]);
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
Reference in a new issue