Implement UI support for manifest lists
This commit is contained in:
parent
276d0d571d
commit
c46b11bac1
14 changed files with 338 additions and 157 deletions
|
@ -7,8 +7,10 @@ from cachetools import lru_cache
|
|||
|
||||
from data import model
|
||||
from data.registry_model.datatype import datatype, requiresinput, optionalinput
|
||||
from image.docker import ManifestException
|
||||
from image.docker.schemas import parse_manifest_from_bytes
|
||||
from image.docker.schema1 import DOCKER_SCHEMA1_SIGNED_MANIFEST_CONTENT_TYPE
|
||||
from image.docker.schema2 import DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE
|
||||
|
||||
|
||||
class RepositoryReference(datatype('Repository', [])):
|
||||
|
@ -138,6 +140,13 @@ class Tag(datatype('Tag', ['name', 'reversion', 'manifest_digest', 'lifetime_sta
|
|||
""" Returns the manifest for this tag. Will only apply to new-style OCI tags. """
|
||||
return manifest
|
||||
|
||||
@property
|
||||
@optionalinput('manifest')
|
||||
def manifest(self, manifest):
|
||||
""" Returns the manifest for this tag or None if none. Will only apply to new-style OCI tags.
|
||||
"""
|
||||
return Manifest.for_manifest(manifest, self.legacy_image_if_present)
|
||||
|
||||
@property
|
||||
@requiresinput('repository')
|
||||
def repository(self, repository):
|
||||
|
@ -202,6 +211,21 @@ class Manifest(datatype('Manifest', ['digest', 'media_type', 'manifest_bytes']))
|
|||
""" Returns the parsed manifest for this manifest. """
|
||||
return parse_manifest_from_bytes(self.manifest_bytes, self.media_type, validate=validate)
|
||||
|
||||
@property
|
||||
def layers_compressed_size(self):
|
||||
""" Returns the total compressed size of the layers in the manifest or None if this could not
|
||||
be computed.
|
||||
"""
|
||||
try:
|
||||
return self.get_parsed_manifest().layers_compressed_size
|
||||
except ManifestException:
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_manifest_list(self):
|
||||
""" Returns True if this manifest points to a list (instead of an image). """
|
||||
return self.media_type == DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE
|
||||
|
||||
|
||||
class LegacyImage(datatype('LegacyImage', ['docker_image_id', 'created', 'comment', 'command',
|
||||
'image_size', 'aggregate_size', 'uploading',
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
""" Manage the tags of a repository. """
|
||||
import json
|
||||
|
||||
from datetime import datetime
|
||||
from flask import request, abort
|
||||
|
@ -27,14 +28,26 @@ def _tag_dict(tag):
|
|||
if tag.lifetime_end_ts > 0:
|
||||
tag_info['end_ts'] = tag.lifetime_end_ts
|
||||
|
||||
if tag.manifest_digest:
|
||||
tag_info['manifest_digest'] = tag.manifest_digest
|
||||
|
||||
if tag.legacy_image:
|
||||
# TODO(jschorr): Remove this once fully on OCI data model.
|
||||
if tag.legacy_image_if_present:
|
||||
tag_info['docker_image_id'] = tag.legacy_image.docker_image_id
|
||||
tag_info['image_id'] = tag.legacy_image.docker_image_id
|
||||
tag_info['size'] = tag.legacy_image.aggregate_size
|
||||
|
||||
# TODO(jschorr): Remove this check once fully on OCI data model.
|
||||
if tag.manifest_digest:
|
||||
tag_info['manifest_digest'] = tag.manifest_digest
|
||||
if tag.manifest:
|
||||
try:
|
||||
tag_info['manifest'] = json.loads(tag.manifest.manifest_bytes)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
tag_info['is_manifest_list'] = tag.manifest.is_manifest_list
|
||||
|
||||
if 'size' not in tag_info:
|
||||
tag_info['size'] = tag.manifest.layers_compressed_size
|
||||
|
||||
if tag.lifetime_start_ts > 0:
|
||||
last_modified = format_date(datetime.utcfromtimestamp(tag.lifetime_start_ts))
|
||||
tag_info['last_modified'] = last_modified
|
||||
|
|
|
@ -46,6 +46,12 @@ class ManifestInterface(object):
|
|||
"""
|
||||
pass
|
||||
|
||||
@abstractproperty
|
||||
def layers_compressed_size(self):
|
||||
""" Returns the total compressed size of all the layers in this manifest. Returns None if this
|
||||
cannot be computed locally.
|
||||
"""
|
||||
|
||||
@abstractproperty
|
||||
def blob_digests(self):
|
||||
""" Returns an iterator over all the blob digests referenced by this manifest,
|
||||
|
|
|
@ -251,6 +251,10 @@ class DockerSchema1Manifest(ManifestInterface):
|
|||
def manifest_dict(self):
|
||||
return self._parsed
|
||||
|
||||
@property
|
||||
def layers_compressed_size(self):
|
||||
return None
|
||||
|
||||
@property
|
||||
def digest(self):
|
||||
return digest_tools.sha256_digest(self._payload)
|
||||
|
|
|
@ -228,6 +228,10 @@ class DockerSchema2ManifestList(ManifestInterface):
|
|||
def local_blob_digests(self):
|
||||
return self.blob_digests
|
||||
|
||||
@property
|
||||
def layers_compressed_size(self):
|
||||
return None
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def manifests(self, lookup_manifest_fn):
|
||||
""" Returns the manifests in the list. The `lookup_manifest_fn` is a function
|
||||
|
|
|
@ -169,6 +169,10 @@ class DockerSchema2Manifest(ManifestInterface):
|
|||
self._layers = list(self._generate_layers())
|
||||
return self._layers
|
||||
|
||||
@property
|
||||
def layers_compressed_size(self):
|
||||
return sum(layer.compressed_size for layer in self.layers)
|
||||
|
||||
@property
|
||||
def leaf_layer(self):
|
||||
return self.layers[-1]
|
||||
|
|
|
@ -176,6 +176,10 @@
|
|||
padding-top: 0px;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .manifest-list .labels-col {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .signing-delegations-list {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
@ -258,4 +262,35 @@
|
|||
margin-bottom: 4px;
|
||||
text-align: center;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .manifest-list-icons {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .manifest-list-manifest-icon {
|
||||
display: inline-block;
|
||||
margin-right: 4px;
|
||||
background-color: #e8f1f6;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .secscan-manifestlist {
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .manifest-list-view td {
|
||||
border-bottom: 0px;
|
||||
border-top: 1px dotted #eee;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .expanded-view.manifest-list {
|
||||
border-top: 1px solid #eee;
|
||||
}
|
|
@ -122,7 +122,7 @@
|
|||
<td class="hidden-xs hidden-sm"
|
||||
ng-class="tablePredicateClass('image_id', options.predicate, options.reverse)"
|
||||
style="width: 140px;">
|
||||
<a ng-click="orderBy('image_id')">Image</a>
|
||||
<a ng-click="orderBy('image_id')">Manifest</a>
|
||||
</td>
|
||||
<td class="hidden-xs hidden-sm hidden-md image-track" ng-repeat="it in imageTracks"
|
||||
ng-if="imageTracks.length <= maxTrackCount"></td>
|
||||
|
@ -137,7 +137,15 @@
|
|||
bindonce>
|
||||
<tr ng-class="expandedView ? 'expanded-view': ''">
|
||||
<td><span class="cor-checkable-item" controller="checkedTags" item="tag"></span></td>
|
||||
<td class="co-flowing-col"><span class="tag-span"><span bo-text="tag.name"></span></span></td>
|
||||
<td class="co-flowing-col">
|
||||
<span class="tag-span"><span bo-text="tag.name"></span></span>
|
||||
<span class="manifest-list-icons" bo-if="tag.is_manifest_list">
|
||||
<i class="manifest-list-manifest-icon fa fa-{{ manifest.os }}"
|
||||
ng-repeat="manifest in manifestsOf(tag)"
|
||||
data-title="{{ manifest.description }}"
|
||||
bs-tooltip></i>
|
||||
</span>
|
||||
</td>
|
||||
<td class="signing-col hidden-xs"
|
||||
quay-require="['SIGNING']"
|
||||
ng-if="repository.trust_enabled">
|
||||
|
@ -151,89 +159,33 @@
|
|||
|
||||
<!-- Security scanning -->
|
||||
<td quay-require="['SECURITY_SCANNER']" class="security-scan-col hidden-sm hidden-xs">
|
||||
<span class="cor-loader-inline" ng-if="getTagVulnerabilities(tag).loading"></span>
|
||||
<span class="vuln-load-error" ng-if="getTagVulnerabilities(tag).hasError"
|
||||
data-title="The vulnerabilities for this tag could not be retrieved at the present time, try again later"
|
||||
<!-- Manifest List -->
|
||||
<span class="secscan-manifestlist" ng-if="::tag.is_manifest_list"
|
||||
ng-click="setExpanded(true)"
|
||||
data-title="The tag points to a list of manifests. Click 'Expanded' to view."
|
||||
bs-tooltip>
|
||||
<i class="fa fa-times-circle"></i>
|
||||
Could not load security information
|
||||
See Child Manifests
|
||||
</span>
|
||||
|
||||
<span ng-if="!getTagVulnerabilities(tag).loading">
|
||||
<!-- No Digest -->
|
||||
<span class="nodigest" ng-if="getTagVulnerabilities(tag).status == 'nodigest'"
|
||||
data-title="The tag does not have a V2 digest and so is unsupported for scan"
|
||||
bs-tooltip>
|
||||
<span class="donut-chart" width="22" data="[{'index': 0, 'value': 1, 'color': '#eee'}]"></span>
|
||||
Unsupported
|
||||
</span>
|
||||
|
||||
<!-- Queued -->
|
||||
<span class="scanning" ng-if="getTagVulnerabilities(tag).status == 'queued'"
|
||||
data-title="The image for this tag is queued to be scanned for vulnerabilities"
|
||||
bs-tooltip>
|
||||
<i class="fa fa-ellipsis-h"></i>
|
||||
Queued
|
||||
</span>
|
||||
|
||||
<!-- Scan Failed -->
|
||||
<span class="failed-scan" ng-if="getTagVulnerabilities(tag).status == 'failed'"
|
||||
data-title="The image for this tag could not be scanned for vulnerabilities"
|
||||
bs-tooltip>
|
||||
<span class="donut-chart" width="22" data="[{'index': 0, 'value': 1, 'color': '#eee'}]"></span>
|
||||
Unable to scan
|
||||
</span>
|
||||
|
||||
<!-- No Features -->
|
||||
<span class="failed-scan"
|
||||
ng-if="getTagVulnerabilities(tag).status == 'scanned' && !getTagVulnerabilities(tag).hasFeatures"
|
||||
data-title="The image for this tag has an operating system or package manager unsupported by Quay Security Scanner"
|
||||
bs-tooltip
|
||||
bindonce>
|
||||
<span class="donut-chart" width="22" data="[{'index': 0, 'value': 1, 'color': '#eee'}]"></span>
|
||||
Unsupported
|
||||
</span>
|
||||
|
||||
<!-- Features and No Vulns -->
|
||||
<span class="no-vulns"
|
||||
ng-if="getTagVulnerabilities(tag).status == 'scanned' && getTagVulnerabilities(tag).hasFeatures && !getTagVulnerabilities(tag).hasVulnerabilities"
|
||||
data-title="The image for this tag has no vulnerabilities as found in our database"
|
||||
bs-tooltip
|
||||
bindonce>
|
||||
<a bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/manifest/{{ tag.manifest_digest }}?tab=vulnerabilities">
|
||||
<span class="donut-chart" width="22" data="[{'index': 0, 'value': 1, 'color': '#2FC98E'}]"></span>
|
||||
Passed
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<!-- Vulns -->
|
||||
<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 }}/manifest/{{ tag.manifest_digest }}?tab=vulnerabilities"
|
||||
data-title="This tag has {{ getTagVulnerabilities(tag).vulnerabilities.length }} vulnerabilities across {{ getTagVulnerabilities(tag).featuresInfo.brokenFeaturesCount }} packages"
|
||||
bs-tooltip>
|
||||
<!-- Donut -->
|
||||
<span class="donut-chart" min-percent="10" width="22" data="getTagVulnerabilities(tag).vulnerabilitiesInfo.severityBreakdown"></span>
|
||||
|
||||
<!-- Messaging -->
|
||||
<span class="highest-vuln">
|
||||
<span class="vulnerability-priority-view" hide-icon="true" priority="getTagVulnerabilities(tag).highestVulnerability.Priority">
|
||||
{{ getTagVulnerabilities(tag).highestVulnerability.Count }}
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
<span class="dot" ng-if="getTagVulnerabilities(tag).vulnerabilitiesInfo.fixable.length">·</span>
|
||||
<a class="vuln-link" bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/manifest/{{ tag.manifest_digest }}?tab=vulnerabilities&fixable=true" ng-if="getTagVulnerabilities(tag).vulnerabilitiesInfo.fixable.length">
|
||||
{{ getTagVulnerabilities(tag).vulnerabilitiesInfo.fixable.length }} fixable
|
||||
</a>
|
||||
</span>
|
||||
<!-- No Digest -->
|
||||
<span class="nodigest" ng-if="::!tag.manifest_digest"
|
||||
data-title="The tag does not have a V2 digest and so is unsupported for scan"
|
||||
bs-tooltip>
|
||||
<span class="donut-chart" width="22" data="[{'index': 0, 'value': 1, 'color': '#eee'}]"></span>
|
||||
Unsupported
|
||||
</span>
|
||||
|
||||
<!-- Manifest security view -->
|
||||
<manifest-security-view repository="::repository" manifest-digest="::tag.manifest_digest"
|
||||
ng-if="::(tag.manifest_digest && !tag.is_manifest_list)">
|
||||
</manifest-security-view>
|
||||
</td>
|
||||
|
||||
<!-- Size -->
|
||||
<td class="hidden-xs" bo-text="tag.size | bytes"></td>
|
||||
<td class="hidden-xs">
|
||||
<span bo-text="tag.size | bytes" bo-if="!tag.is_manifest_list"></span>
|
||||
<span bo-if="tag.is_manifest_list">N/A</span>
|
||||
</td>
|
||||
|
||||
<!-- Expiration -->
|
||||
<td class="hidden-xs hidden-sm hidden-md">
|
||||
|
@ -307,7 +259,40 @@
|
|||
</td>
|
||||
<td class="options-col hidden-xs hidden-sm"><!-- Whitespace col --></td>
|
||||
</tr>
|
||||
<tr ng-if="expandedView">
|
||||
|
||||
<!-- Manifest List Expanded View -->
|
||||
<tr class="manifest-list-view" ng-repeat="manifest in manifestsOf(tag)"
|
||||
ng-if="expandedView && tag.is_manifest_list">
|
||||
<td class="checkbox-col"></td>
|
||||
<td colspan="2">
|
||||
<i class="manifest-list-manifest-icon fa fa-{{ manifest.os }}"></i>
|
||||
{{ manifest.description }}
|
||||
</td>
|
||||
|
||||
<!-- Security scanning -->
|
||||
<td quay-require="['SECURITY_SCANNER']" class="security-scan-col hidden-sm hidden-xs"
|
||||
colspan="3">
|
||||
<manifest-security-view repository="::repository" manifest-digest="::manifest.digest">
|
||||
</manifest-security-view>
|
||||
</td>
|
||||
|
||||
<td class="hidden-xs hidden-sm hidden-md image-track" ng-repeat="it in imageTracks"
|
||||
ng-if="imageTracks.length <= maxTrackCount" bindonce>
|
||||
<span class="image-track-line"
|
||||
ng-if="::getTrackEntryForIndex(it, $parent.$parent.$index)"
|
||||
ng-class="::trackLineExpandedClass(it, $parent.$parent.$parent.$index)"
|
||||
ng-style="::{'borderColor': getTrackEntryForIndex(it, $parent.$parent.$parent.$index).color}"></span>
|
||||
</td>
|
||||
|
||||
<td class="hidden-xs hidden-sm image-id-col">
|
||||
<manifest-link repository="repository" manifest-digest="manifest.digest"></manifest-link>
|
||||
</td>
|
||||
|
||||
<td colspan="2" class="hidden-xs hidden-sm hidden-md"></td>
|
||||
</tr>
|
||||
|
||||
<!-- Expanded View -->
|
||||
<tr ng-if="expandedView" class="expanded-view" ng-class="{'manifest-list': tag.is_manifest_list}">
|
||||
<td class="checkbox-col"></td>
|
||||
<td class="labels-col" colspan="{{6 + (repository.trust_enabled ? 1 : 0) + (Features.SECURITY_SCANNER ? 1 : 0) }}">
|
||||
<!-- Image ID -->
|
||||
|
@ -331,7 +316,7 @@
|
|||
ng-class="::trackLineExpandedClass(it, $parent.$parent.$parent.$index)"
|
||||
ng-style="::{'borderColor': getTrackEntryForIndex(it, $parent.$parent.$parent.$index).color}"></span>
|
||||
</td>
|
||||
<td colspan="1" class="hidden-xs hidden-sm hidden-md"></td>
|
||||
<td colspan="2" class="hidden-xs hidden-sm hidden-md"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -39,7 +39,7 @@ angular.module('quay').directive('repoPanelTags', function () {
|
|||
$scope.expandedView = false;
|
||||
$scope.labelCache = {};
|
||||
|
||||
$scope.imageVulnerabilities = {};
|
||||
$scope.manifestVulnerabilities = {};
|
||||
$scope.repoDelegationsInfo = null;
|
||||
|
||||
$scope.defcon1 = {};
|
||||
|
@ -251,76 +251,6 @@ angular.module('quay').directive('repoPanelTags', function () {
|
|||
loadRepoSignatures();
|
||||
}, true);
|
||||
|
||||
$scope.loadImageVulnerabilities = function(image_id, imageData) {
|
||||
VulnerabilityService.loadImageVulnerabilities($scope.repository, image_id, function(resp) {
|
||||
imageData.loading = false;
|
||||
imageData.status = resp['status'];
|
||||
|
||||
if (imageData.status == 'scanned') {
|
||||
var vulnerabilities = [];
|
||||
var highest = {
|
||||
'Severity': 'Unknown',
|
||||
'Count': 0,
|
||||
'index': 100000
|
||||
};
|
||||
|
||||
VulnerabilityService.forEachVulnerability(resp, function(vuln) {
|
||||
if (VulnerabilityService.LEVELS[vuln.Severity].index == 0) {
|
||||
$scope.defcon1[vuln.Name] = vuln;
|
||||
$scope.hasDefcon1 = true;
|
||||
}
|
||||
|
||||
if (VulnerabilityService.LEVELS[vuln.Severity].index < highest.index) {
|
||||
highest = {
|
||||
'Priority': vuln.Severity,
|
||||
'Count': 1,
|
||||
'index': VulnerabilityService.LEVELS[vuln.Severity].index,
|
||||
'Color': VulnerabilityService.LEVELS[vuln.Severity].color
|
||||
}
|
||||
} else if (VulnerabilityService.LEVELS[vuln.Severity].index == highest.index) {
|
||||
highest['Count']++;
|
||||
}
|
||||
|
||||
vulnerabilities.push(vuln);
|
||||
});
|
||||
|
||||
imageData.hasFeatures = VulnerabilityService.hasFeatures(resp);
|
||||
imageData.hasVulnerabilities = !!vulnerabilities.length;
|
||||
imageData.vulnerabilities = vulnerabilities;
|
||||
imageData.highestVulnerability = highest;
|
||||
imageData.featuresInfo = VulnerabilityService.buildFeaturesInfo(null, resp);
|
||||
imageData.vulnerabilitiesInfo = VulnerabilityService.buildVulnerabilitiesInfo(null, resp);
|
||||
}
|
||||
}, function() {
|
||||
imageData.loading = false;
|
||||
imageData.hasError = true;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.getTagVulnerabilities = function(tag) {
|
||||
if (!tag.manifest_digest) {
|
||||
return 'nodigest';
|
||||
}
|
||||
|
||||
return $scope.getImageVulnerabilities(tag.image_id);
|
||||
};
|
||||
|
||||
$scope.getImageVulnerabilities = function(image_id) {
|
||||
if (!$scope.repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$scope.imageVulnerabilities[image_id]) {
|
||||
$scope.imageVulnerabilities[image_id] = {
|
||||
'loading': true
|
||||
};
|
||||
|
||||
$scope.loadImageVulnerabilities(image_id, $scope.imageVulnerabilities[image_id]);
|
||||
}
|
||||
|
||||
return $scope.imageVulnerabilities[image_id];
|
||||
};
|
||||
|
||||
$scope.clearSelectedTags = function() {
|
||||
$scope.checkedTags.setChecked([]);
|
||||
};
|
||||
|
@ -499,6 +429,27 @@ angular.module('quay').directive('repoPanelTags', function () {
|
|||
$scope.handleLabelsChanged = function(manifest_digest) {
|
||||
delete $scope.labelCache[manifest_digest];
|
||||
};
|
||||
|
||||
$scope.manifestsOf = function(tag) {
|
||||
if (!tag.is_manifest_list || !tag.manifest) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!tag._mapped_manifests) {
|
||||
// Calculate once and cache to avoid angular digest cycles.
|
||||
tag._mapped_manifests = tag.manifest.manifests.map(function(manifest) {
|
||||
return {
|
||||
'raw': manifest,
|
||||
'os': manifest.platform.os,
|
||||
'size': manifest.size,
|
||||
'digest': manifest.digest,
|
||||
'description': `${manifest.platform.os} on ${manifest.platform.architecture}`,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return tag._mapped_manifests;
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
|
|
|
@ -39,7 +39,7 @@ angular.module('quay').directive('fetchTagDialog', function () {
|
|||
});
|
||||
}
|
||||
|
||||
if ($scope.repository) {
|
||||
if ($scope.repository && $scope.currentTag && !$scope.currentTag.is_manifest_list) {
|
||||
$scope.formats.push({
|
||||
'title': 'Squashed Docker Image',
|
||||
'icon': 'ci-squashed',
|
||||
|
@ -49,7 +49,7 @@ angular.module('quay').directive('fetchTagDialog', function () {
|
|||
});
|
||||
}
|
||||
|
||||
if (Features.ACI_CONVERSION) {
|
||||
if (Features.ACI_CONVERSION && $scope.currentTag && !$scope.currentTag.is_manifest_list) {
|
||||
$scope.formats.push({
|
||||
'title': 'rkt Fetch',
|
||||
'icon': 'rocket-icon',
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
<div class="manifest-security-view-element">
|
||||
<span class="cor-loader-inline" ng-if="$ctrl.getSecurityStatus($ctrl.repository, $ctrl.manifestDigest).loading"></span>
|
||||
<span class="vuln-load-error" ng-if="$ctrl.getSecurityStatus($ctrl.repository, $ctrl.manifestDigest).hasError"
|
||||
data-title="The vulnerabilities for this tag could not be retrieved at the present time, try again later"
|
||||
bs-tooltip>
|
||||
<i class="fa fa-times-circle"></i>
|
||||
Could not load security information
|
||||
</span>
|
||||
|
||||
<span ng-if="!$ctrl.getSecurityStatus($ctrl.repository, $ctrl.manifestDigest).loading">
|
||||
<!-- Queued -->
|
||||
<span class="scanning" ng-if="$ctrl.getSecurityStatus($ctrl.repository, $ctrl.manifestDigest).status == 'queued'"
|
||||
data-title="The image for this tag is queued to be scanned for vulnerabilities"
|
||||
bs-tooltip>
|
||||
<i class="fa fa-ellipsis-h"></i>
|
||||
Queued
|
||||
</span>
|
||||
|
||||
<!-- Scan Failed -->
|
||||
<span class="failed-scan" ng-if="$ctrl.getSecurityStatus($ctrl.repository, $ctrl.manifestDigest).status == 'failed'"
|
||||
data-title="The image for this tag could not be scanned for vulnerabilities"
|
||||
bs-tooltip>
|
||||
<span class="donut-chart" width="22" data="[{'index': 0, 'value': 1, 'color': '#eee'}]"></span>
|
||||
Unable to scan
|
||||
</span>
|
||||
|
||||
<!-- No Features -->
|
||||
<span class="failed-scan"
|
||||
ng-if="$ctrl.getSecurityStatus($ctrl.repository, $ctrl.manifestDigest).status == 'scanned' && !$ctrl.getSecurityStatus($ctrl.repository, $ctrl.manifestDigest).hasFeatures"
|
||||
data-title="The image for this tag has an operating system or package manager unsupported by Quay Security Scanner"
|
||||
bs-tooltip
|
||||
bindonce>
|
||||
<span class="donut-chart" width="22" data="[{'index': 0, 'value': 1, 'color': '#eee'}]"></span>
|
||||
Unsupported
|
||||
</span>
|
||||
|
||||
<!-- Features and No Vulns -->
|
||||
<span class="no-vulns"
|
||||
ng-if="$ctrl.getSecurityStatus($ctrl.repository, $ctrl.manifestDigest).status == 'scanned' && $ctrl.getSecurityStatus($ctrl.repository, $ctrl.manifestDigest).hasFeatures && !$ctrl.getSecurityStatus($ctrl.repository, $ctrl.manifestDigest).hasVulnerabilities"
|
||||
data-title="The image for this tag has no vulnerabilities as found in our database"
|
||||
bs-tooltip
|
||||
bindonce>
|
||||
<a bo-href-i="/repository/{{ $ctrl.repository.namespace }}/{{ $ctrl.repository.name }}/manifest/{{ tag.manifest_digest }}?tab=vulnerabilities">
|
||||
<span class="donut-chart" width="22" data="[{'index': 0, 'value': 1, 'color': '#2FC98E'}]"></span>
|
||||
Passed
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<!-- Vulns -->
|
||||
<span ng-if="$ctrl.getSecurityStatus($ctrl.repository, $ctrl.manifestDigest).status == 'scanned' && $ctrl.getSecurityStatus($ctrl.repository, $ctrl.manifestDigest).hasFeatures && $ctrl.getSecurityStatus($ctrl.repository, $ctrl.manifestDigest).hasVulnerabilities"
|
||||
ng-class="$ctrl.getSecurityStatus($ctrl.repository, $ctrl.manifestDigest).highestVulnerability.Priority"
|
||||
class="has-vulns" bindonce>
|
||||
|
||||
<a class="vuln-link" bo-href-i="/repository/{{ $ctrl.repository.namespace }}/{{ $ctrl.repository.name }}/manifest/{{ tag.manifest_digest }}?tab=vulnerabilities"
|
||||
data-title="This tag has {{ $ctrl.getSecurityStatus($ctrl.repository, $ctrl.manifestDigest).vulnerabilities.length }} vulnerabilities across {{ $ctrl.getSecurityStatus($ctrl.repository, $ctrl.manifestDigest).featuresInfo.brokenFeaturesCount }} packages"
|
||||
bs-tooltip>
|
||||
<!-- Donut -->
|
||||
<span class="donut-chart" min-percent="10" width="22" data="$ctrl.getSecurityStatus($ctrl.repository, $ctrl.manifestDigest).vulnerabilitiesInfo.severityBreakdown"></span>
|
||||
|
||||
<!-- Messaging -->
|
||||
<span class="highest-vuln">
|
||||
<span class="vulnerability-priority-view" hide-icon="true" priority="$ctrl.getSecurityStatus($ctrl.repository, $ctrl.manifestDigest).highestVulnerability.Priority">
|
||||
{{ $ctrl.getSecurityStatus($ctrl.repository, $ctrl.manifestDigest).highestVulnerability.Count }}
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
<span class="dot" ng-if="$ctrl.getSecurityStatus($ctrl.repository, $ctrl.manifestDigest).vulnerabilitiesInfo.fixable.length">·</span>
|
||||
<a class="vuln-link" bo-href-i="/repository/{{ $ctrl.repository.namespace }}/{{ $ctrl.repository.name }}/manifest/{{ tag.manifest_digest }}?tab=vulnerabilities&fixable=true" ng-if="$ctrl.getSecurityStatus($ctrl.repository, $ctrl.manifestDigest).vulnerabilitiesInfo.fixable.length">
|
||||
{{ $ctrl.getSecurityStatus($ctrl.repository, $ctrl.manifestDigest).vulnerabilitiesInfo.fixable.length }} fixable
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
|
@ -0,0 +1,76 @@
|
|||
import { Input, Component, Inject } from 'ng-metadata/core';
|
||||
import { Repository } from '../../../types/common.types';
|
||||
|
||||
|
||||
/**
|
||||
* A component that displays the security status of a manifest.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'manifest-security-view',
|
||||
templateUrl: '/static/js/directives/ui/manifest-security-view/manifest-security-view.component.html',
|
||||
})
|
||||
export class ManifestSecurityView {
|
||||
@Input('<') public repository: Repository;
|
||||
@Input('<') public manifestDigest: string;
|
||||
|
||||
private cachedSecurityStatus: Object = null;
|
||||
|
||||
constructor(@Inject('VulnerabilityService') private VulnerabilityService: any) {
|
||||
}
|
||||
|
||||
private getSecurityStatus(repository: Repository, manifestDigest: string): Object {
|
||||
if (repository == null || !manifestDigest) {
|
||||
return {'status': 'loading'};
|
||||
}
|
||||
|
||||
if (this.cachedSecurityStatus) {
|
||||
return this.cachedSecurityStatus;
|
||||
}
|
||||
|
||||
this.cachedSecurityStatus = {'status': 'loading'};
|
||||
this.loadManifestVulnerabilities(this.cachedSecurityStatus);
|
||||
return this.cachedSecurityStatus;
|
||||
}
|
||||
|
||||
private loadManifestVulnerabilities(securityStatus) {
|
||||
this.VulnerabilityService.loadManifestVulnerabilities(this.repository, this.manifestDigest, (resp) => {
|
||||
securityStatus.loading = false;
|
||||
securityStatus.status = resp['status'];
|
||||
|
||||
if (securityStatus.status == 'scanned') {
|
||||
var vulnerabilities = [];
|
||||
var highest = {
|
||||
'Priority': 'Unknown',
|
||||
'Count': 0,
|
||||
'index': 100000,
|
||||
'Color': 'gray',
|
||||
};
|
||||
|
||||
this.VulnerabilityService.forEachVulnerability(resp, function(vuln) {
|
||||
if (this.VulnerabilityService.LEVELS[vuln.Severity].index < highest.index) {
|
||||
highest = {
|
||||
'Priority': vuln.Severity,
|
||||
'Count': 1,
|
||||
'index': this.VulnerabilityService.LEVELS[vuln.Severity].index,
|
||||
'Color': this.VulnerabilityService.LEVELS[vuln.Severity].color
|
||||
}
|
||||
} else if (this.VulnerabilityService.LEVELS[vuln.Severity].index == highest.index) {
|
||||
highest['Count']++;
|
||||
}
|
||||
|
||||
vulnerabilities.push(vuln);
|
||||
});
|
||||
|
||||
securityStatus.hasFeatures = this.VulnerabilityService.hasFeatures(resp);
|
||||
securityStatus.hasVulnerabilities = !!vulnerabilities.length;
|
||||
securityStatus.vulnerabilities = vulnerabilities;
|
||||
securityStatus.highestVulnerability = highest;
|
||||
securityStatus.featuresInfo = this.VulnerabilityService.buildFeaturesInfo(null, resp);
|
||||
securityStatus.vulnerabilitiesInfo = this.VulnerabilityService.buildVulnerabilitiesInfo(null, resp);
|
||||
}
|
||||
}, function() {
|
||||
securityStatus.loading = false;
|
||||
securityStatus.hasError = true;
|
||||
});
|
||||
};
|
||||
}
|
|
@ -41,6 +41,7 @@ import { TimeAgoComponent } from './directives/ui/time-ago/time-ago.component';
|
|||
import { TimeDisplayComponent } from './directives/ui/time-display/time-display.component';
|
||||
import { AppSpecificTokenManagerComponent } from './directives/ui/app-specific-token-manager/app-specific-token-manager.component';
|
||||
import { ManifestLinkComponent } from './directives/ui/manifest-link/manifest-link.component';
|
||||
import { ManifestSecurityView } from './directives/ui/manifest-security-view/manifest-security-view.component';
|
||||
import { MarkdownModule } from './directives/ui/markdown/markdown.module';
|
||||
import * as Clipboard from 'clipboard';
|
||||
|
||||
|
@ -87,6 +88,7 @@ import * as Clipboard from 'clipboard';
|
|||
TimeDisplayComponent,
|
||||
AppSpecificTokenManagerComponent,
|
||||
ManifestLinkComponent,
|
||||
ManifestSecurityView,
|
||||
],
|
||||
providers: [
|
||||
ViewArrayImpl,
|
||||
|
|
|
@ -86,6 +86,10 @@ class BrokenManifest(ManifestInterface):
|
|||
def schema_version():
|
||||
return 1
|
||||
|
||||
@property
|
||||
def layers_compressed_size():
|
||||
return None
|
||||
|
||||
|
||||
class ManifestBackfillWorker(Worker):
|
||||
def __init__(self):
|
||||
|
|
Reference in a new issue