/** * An element which displays the features of an image. */ angular.module('quay').directive('imageFeatureView', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/image-feature-view.html', replace: false, transclude: true, restrict: 'C', scope: { 'repository': '=repository', 'image': '=image', 'isEnabled': '=isEnabled' }, controller: function($scope, $element, Config, ApiService, VulnerabilityService, AngularViewArray, ImageMetadataService) { var imageMap = null; $scope.securityFeatures = []; $scope.featureBreakdown = []; $scope.options = { 'featureFilter': null, 'predicate': 'fixableScore', 'reverse': false, }; $scope.tablePredicateClass = function(name, predicate, reverse) { if (name != predicate) { return ''; } return 'current ' + (reverse ? 'reversed' : ''); }; $scope.orderBy = function(predicate) { if (predicate == $scope.options.predicate) { $scope.options.reverse = !$scope.options.reverse; return; } $scope.options.reverse = false; $scope.options.predicate = predicate; }; var buildOrderedFeatures = function() { var features = $scope.securityFeatures.slice(0); $scope.orderedFeatures = AngularViewArray.create(); features.forEach(function(v) { var featureFilter = $scope.options.featureFilter; if (featureFilter) { if ((v['name'].indexOf(featureFilter) < 0) && (v['version'].indexOf(featureFilter) < 0) && (v['imageId'].indexOf(featureFilter) < 0)) { return; } } $scope.orderedFeatures.push(v); }); $scope.orderedFeatures.entries.sort(function(a, b) { var left = a[$scope.options['predicate']]; var right = b[$scope.options['predicate']]; if ($scope.options['predicate'] == 'score' || $scope.options['predicate'] == 'fixableScore' || $scope.options['predicate'] == 'leftoverScore') { left = left * 1; right = right * 1; } if (left == null) { left = '0.00'; } if (right == null) { right = '0.00'; } if (left == right) { return 0; } return left > right ? -1 : 1; }); if ($scope.options['reverse']) { $scope.orderedFeatures.entries.reverse(); } $scope.orderedFeatures.setVisible(true); }; var buildChart = function() { var chartData = $scope.featureBreakdown; var colors = []; for (var i = 0; i < chartData.length; ++i) { colors.push(chartData[i].color); } nv.addGraph(function() { var chart = nv.models.pieChart() .x(function(d) { return d.label }) .y(function(d) { return d.value }) .margin({left: -10, right: -10, top: -10, bottom: -10}) .showLegend(false) .showLabels(true) .labelThreshold(.05) .labelType("percent") .donut(true) .color(colors) .donutRatio(0.5); d3.select("#featureDonutChart svg") .datum(chartData) .transition() .duration(350) .call(chart); return chart; }); }; var buildFeatures = function(data) { $scope.securityFeatures = []; $scope.featureBreakdown = []; $scope.highestFixableScore = -10000; var severityMap = {}; var levels = VulnerabilityService.getLevels(); if (data && data.Layer && data.Layer.Features) { data.Layer.Features.forEach(function(feature) { var imageId = 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.securityStatus = resp.status; buildFeatures(resp.data); buildChart(); return resp; }); }; $scope.$watch('options.predicate', buildOrderedFeatures); $scope.$watch('options.reverse', buildOrderedFeatures); $scope.$watch('options.featureFilter', buildOrderedFeatures); $scope.$watch('isEnabled', function(isEnabled) { if ($scope.isEnabled && $scope.repository && $scope.image) { loadImageVulnerabilities(); } }); } }; return directiveDefinitionObject; });