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 import model
|
||||||
from data.registry_model.datatype import datatype, requiresinput, optionalinput
|
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.schemas import parse_manifest_from_bytes
|
||||||
from image.docker.schema1 import DOCKER_SCHEMA1_SIGNED_MANIFEST_CONTENT_TYPE
|
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', [])):
|
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. """
|
""" Returns the manifest for this tag. Will only apply to new-style OCI tags. """
|
||||||
return manifest
|
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
|
@property
|
||||||
@requiresinput('repository')
|
@requiresinput('repository')
|
||||||
def repository(self, 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. """
|
""" Returns the parsed manifest for this manifest. """
|
||||||
return parse_manifest_from_bytes(self.manifest_bytes, self.media_type, validate=validate)
|
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',
|
class LegacyImage(datatype('LegacyImage', ['docker_image_id', 'created', 'comment', 'command',
|
||||||
'image_size', 'aggregate_size', 'uploading',
|
'image_size', 'aggregate_size', 'uploading',
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
""" Manage the tags of a repository. """
|
""" Manage the tags of a repository. """
|
||||||
|
import json
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from flask import request, abort
|
from flask import request, abort
|
||||||
|
@ -27,14 +28,26 @@ def _tag_dict(tag):
|
||||||
if tag.lifetime_end_ts > 0:
|
if tag.lifetime_end_ts > 0:
|
||||||
tag_info['end_ts'] = tag.lifetime_end_ts
|
tag_info['end_ts'] = tag.lifetime_end_ts
|
||||||
|
|
||||||
if tag.manifest_digest:
|
# TODO(jschorr): Remove this once fully on OCI data model.
|
||||||
tag_info['manifest_digest'] = tag.manifest_digest
|
if tag.legacy_image_if_present:
|
||||||
|
|
||||||
if tag.legacy_image:
|
|
||||||
tag_info['docker_image_id'] = tag.legacy_image.docker_image_id
|
tag_info['docker_image_id'] = tag.legacy_image.docker_image_id
|
||||||
tag_info['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
|
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:
|
if tag.lifetime_start_ts > 0:
|
||||||
last_modified = format_date(datetime.utcfromtimestamp(tag.lifetime_start_ts))
|
last_modified = format_date(datetime.utcfromtimestamp(tag.lifetime_start_ts))
|
||||||
tag_info['last_modified'] = last_modified
|
tag_info['last_modified'] = last_modified
|
||||||
|
|
|
@ -46,6 +46,12 @@ class ManifestInterface(object):
|
||||||
"""
|
"""
|
||||||
pass
|
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
|
@abstractproperty
|
||||||
def blob_digests(self):
|
def blob_digests(self):
|
||||||
""" Returns an iterator over all the blob digests referenced by this manifest,
|
""" Returns an iterator over all the blob digests referenced by this manifest,
|
||||||
|
|
|
@ -251,6 +251,10 @@ class DockerSchema1Manifest(ManifestInterface):
|
||||||
def manifest_dict(self):
|
def manifest_dict(self):
|
||||||
return self._parsed
|
return self._parsed
|
||||||
|
|
||||||
|
@property
|
||||||
|
def layers_compressed_size(self):
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def digest(self):
|
def digest(self):
|
||||||
return digest_tools.sha256_digest(self._payload)
|
return digest_tools.sha256_digest(self._payload)
|
||||||
|
|
|
@ -228,6 +228,10 @@ class DockerSchema2ManifestList(ManifestInterface):
|
||||||
def local_blob_digests(self):
|
def local_blob_digests(self):
|
||||||
return self.blob_digests
|
return self.blob_digests
|
||||||
|
|
||||||
|
@property
|
||||||
|
def layers_compressed_size(self):
|
||||||
|
return None
|
||||||
|
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
def manifests(self, lookup_manifest_fn):
|
def manifests(self, lookup_manifest_fn):
|
||||||
""" Returns the manifests in the list. The `lookup_manifest_fn` is a function
|
""" 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())
|
self._layers = list(self._generate_layers())
|
||||||
return self._layers
|
return self._layers
|
||||||
|
|
||||||
|
@property
|
||||||
|
def layers_compressed_size(self):
|
||||||
|
return sum(layer.compressed_size for layer in self.layers)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def leaf_layer(self):
|
def leaf_layer(self):
|
||||||
return self.layers[-1]
|
return self.layers[-1]
|
||||||
|
|
|
@ -176,6 +176,10 @@
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repo-panel-tags-element .manifest-list .labels-col {
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.repo-panel-tags-element .signing-delegations-list {
|
.repo-panel-tags-element .signing-delegations-list {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
@ -259,3 +263,34 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 4px;
|
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"
|
<td class="hidden-xs hidden-sm"
|
||||||
ng-class="tablePredicateClass('image_id', options.predicate, options.reverse)"
|
ng-class="tablePredicateClass('image_id', options.predicate, options.reverse)"
|
||||||
style="width: 140px;">
|
style="width: 140px;">
|
||||||
<a ng-click="orderBy('image_id')">Image</a>
|
<a ng-click="orderBy('image_id')">Manifest</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="hidden-xs hidden-sm hidden-md image-track" ng-repeat="it in imageTracks"
|
<td class="hidden-xs hidden-sm hidden-md image-track" ng-repeat="it in imageTracks"
|
||||||
ng-if="imageTracks.length <= maxTrackCount"></td>
|
ng-if="imageTracks.length <= maxTrackCount"></td>
|
||||||
|
@ -137,7 +137,15 @@
|
||||||
bindonce>
|
bindonce>
|
||||||
<tr ng-class="expandedView ? 'expanded-view': ''">
|
<tr ng-class="expandedView ? 'expanded-view': ''">
|
||||||
<td><span class="cor-checkable-item" controller="checkedTags" item="tag"></span></td>
|
<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"
|
<td class="signing-col hidden-xs"
|
||||||
quay-require="['SIGNING']"
|
quay-require="['SIGNING']"
|
||||||
ng-if="repository.trust_enabled">
|
ng-if="repository.trust_enabled">
|
||||||
|
@ -151,89 +159,33 @@
|
||||||
|
|
||||||
<!-- Security scanning -->
|
<!-- Security scanning -->
|
||||||
<td quay-require="['SECURITY_SCANNER']" class="security-scan-col hidden-sm hidden-xs">
|
<td quay-require="['SECURITY_SCANNER']" class="security-scan-col hidden-sm hidden-xs">
|
||||||
<span class="cor-loader-inline" ng-if="getTagVulnerabilities(tag).loading"></span>
|
<!-- Manifest List -->
|
||||||
<span class="vuln-load-error" ng-if="getTagVulnerabilities(tag).hasError"
|
<span class="secscan-manifestlist" ng-if="::tag.is_manifest_list"
|
||||||
data-title="The vulnerabilities for this tag could not be retrieved at the present time, try again later"
|
ng-click="setExpanded(true)"
|
||||||
|
data-title="The tag points to a list of manifests. Click 'Expanded' to view."
|
||||||
bs-tooltip>
|
bs-tooltip>
|
||||||
<i class="fa fa-times-circle"></i>
|
See Child Manifests
|
||||||
Could not load security information
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span ng-if="!getTagVulnerabilities(tag).loading">
|
<!-- No Digest -->
|
||||||
<!-- No Digest -->
|
<span class="nodigest" ng-if="::!tag.manifest_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"
|
||||||
data-title="The tag does not have a V2 digest and so is unsupported for scan"
|
bs-tooltip>
|
||||||
bs-tooltip>
|
<span class="donut-chart" width="22" data="[{'index': 0, 'value': 1, 'color': '#eee'}]"></span>
|
||||||
<span class="donut-chart" width="22" data="[{'index': 0, 'value': 1, 'color': '#eee'}]"></span>
|
Unsupported
|
||||||
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>
|
|
||||||
</span>
|
</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>
|
</td>
|
||||||
|
|
||||||
<!-- Size -->
|
<!-- 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 -->
|
<!-- Expiration -->
|
||||||
<td class="hidden-xs hidden-sm hidden-md">
|
<td class="hidden-xs hidden-sm hidden-md">
|
||||||
|
@ -307,7 +259,40 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="options-col hidden-xs hidden-sm"><!-- Whitespace col --></td>
|
<td class="options-col hidden-xs hidden-sm"><!-- Whitespace col --></td>
|
||||||
</tr>
|
</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="checkbox-col"></td>
|
||||||
<td class="labels-col" colspan="{{6 + (repository.trust_enabled ? 1 : 0) + (Features.SECURITY_SCANNER ? 1 : 0) }}">
|
<td class="labels-col" colspan="{{6 + (repository.trust_enabled ? 1 : 0) + (Features.SECURITY_SCANNER ? 1 : 0) }}">
|
||||||
<!-- Image ID -->
|
<!-- Image ID -->
|
||||||
|
@ -331,7 +316,7 @@
|
||||||
ng-class="::trackLineExpandedClass(it, $parent.$parent.$parent.$index)"
|
ng-class="::trackLineExpandedClass(it, $parent.$parent.$parent.$index)"
|
||||||
ng-style="::{'borderColor': getTrackEntryForIndex(it, $parent.$parent.$parent.$index).color}"></span>
|
ng-style="::{'borderColor': getTrackEntryForIndex(it, $parent.$parent.$parent.$index).color}"></span>
|
||||||
</td>
|
</td>
|
||||||
<td colspan="1" class="hidden-xs hidden-sm hidden-md"></td>
|
<td colspan="2" class="hidden-xs hidden-sm hidden-md"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -39,7 +39,7 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
$scope.expandedView = false;
|
$scope.expandedView = false;
|
||||||
$scope.labelCache = {};
|
$scope.labelCache = {};
|
||||||
|
|
||||||
$scope.imageVulnerabilities = {};
|
$scope.manifestVulnerabilities = {};
|
||||||
$scope.repoDelegationsInfo = null;
|
$scope.repoDelegationsInfo = null;
|
||||||
|
|
||||||
$scope.defcon1 = {};
|
$scope.defcon1 = {};
|
||||||
|
@ -251,76 +251,6 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
loadRepoSignatures();
|
loadRepoSignatures();
|
||||||
}, true);
|
}, 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.clearSelectedTags = function() {
|
||||||
$scope.checkedTags.setChecked([]);
|
$scope.checkedTags.setChecked([]);
|
||||||
};
|
};
|
||||||
|
@ -499,6 +429,27 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
$scope.handleLabelsChanged = function(manifest_digest) {
|
$scope.handleLabelsChanged = function(manifest_digest) {
|
||||||
delete $scope.labelCache[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;
|
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({
|
$scope.formats.push({
|
||||||
'title': 'Squashed Docker Image',
|
'title': 'Squashed Docker Image',
|
||||||
'icon': 'ci-squashed',
|
'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({
|
$scope.formats.push({
|
||||||
'title': 'rkt Fetch',
|
'title': 'rkt Fetch',
|
||||||
'icon': 'rocket-icon',
|
'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 { TimeDisplayComponent } from './directives/ui/time-display/time-display.component';
|
||||||
import { AppSpecificTokenManagerComponent } from './directives/ui/app-specific-token-manager/app-specific-token-manager.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 { 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 { MarkdownModule } from './directives/ui/markdown/markdown.module';
|
||||||
import * as Clipboard from 'clipboard';
|
import * as Clipboard from 'clipboard';
|
||||||
|
|
||||||
|
@ -87,6 +88,7 @@ import * as Clipboard from 'clipboard';
|
||||||
TimeDisplayComponent,
|
TimeDisplayComponent,
|
||||||
AppSpecificTokenManagerComponent,
|
AppSpecificTokenManagerComponent,
|
||||||
ManifestLinkComponent,
|
ManifestLinkComponent,
|
||||||
|
ManifestSecurityView,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
ViewArrayImpl,
|
ViewArrayImpl,
|
||||||
|
|
|
@ -86,6 +86,10 @@ class BrokenManifest(ManifestInterface):
|
||||||
def schema_version():
|
def schema_version():
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
@property
|
||||||
|
def layers_compressed_size():
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class ManifestBackfillWorker(Worker):
|
class ManifestBackfillWorker(Worker):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
Reference in a new issue