1
0
mirror of https://github.com/quay/quay.git synced 2025-04-18 10:44:06 +03:00

ui: basic support for cosign in the UI (PROJQUAY-3965) (#1380)

* ui: basic support for cosign in the UI (PROJQUAY-3965)

* sharpen regex for cosign manifest tag name convention

Co-authored-by: Oleg Bulatov <oleg@bulatov.me>

* fix broken signature match

* addressing review comments

* fix tool tip width issues

Co-authored-by: Oleg Bulatov <oleg@bulatov.me>
This commit is contained in:
Daniel Messer 2022-06-16 14:06:56 +02:00 committed by GitHub
parent 8f5b14ba2b
commit d9dbbd88db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 200 additions and 5 deletions

View File

@ -130,8 +130,6 @@
content:"\f113";
}
.ci-shield-none:before {
content:"\f114";
}

View File

@ -171,6 +171,27 @@
color: #888;
}
.repo-panel-tags-element .signing-valid .tooltip-inner {
max-width: 250px;
}
.repo-panel-tags-element .cosign-signature-row {
margin-top: 10px;
margin-bottom: 6px;
padding-left: 6px;
position: relative;
}
.repo-panel-tags-element .cosign-signature-row:before {
content: "\f040";
font-family: FontAwesome;
position: absolute;
left: -22px;
top: 0px;
font-size: 15px;
color: #888;
}
.repo-panel-tags-element .labels-col {
padding-top: 0px;
}

View File

@ -10,6 +10,13 @@
Expanded
</button>
</div>
<div class="btn-group btn-group-sm">
<button class="btn" ng-class="!showCosignSignatures ? 'btn-default' : 'btn-primary active'"
ng-click="toggleCosignSignatureDisplay()">
{{ !showCosignSignatures ? 'Show Signatures' : 'Hide Signatures' }}
</button>
</div>
</div>
<h3 class="tab-header"><span class="hidden-xs">Repository </span>Tags</h3>
@ -22,9 +29,18 @@
<div class="cor-checkable-menu-item" item-filter="noTagFilter(item)">
<i class="fa fa-square-o"></i>No Tags
</div>
<div class="cor-checkable-menu-item" item-filter="signedTagFilter(item)">
<i class="fa fa-shield"></i>Signed Tags
</div>
<div class="cor-checkable-menu-item" item-filter="unsignedTagFilter(item)">
<i class="fa ci-shield-invalid-outline"></i>Unsigned Tags
</div>
<div class="cor-checkable-menu-item" item-filter="commitTagFilter(item)">
<i class="fa fa-git"></i>Commit SHAs
</div>
<div class="cor-checkable-menu-item" item-filter="cosignTagFilter(item)">
<i class="fa fa-pencil-square"></i>Cosign Signatures
</div>
<div class="cor-checkable-menu-item" item-filter="manifestDigestFilter(mt.manifest_digest, item)"
ng-repeat="mt in manifestTrackEntries" ng-if="::it.visible">
@ -132,7 +148,12 @@
<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>
<span class="tag-span">
<span bo-text="tag.name"></span>
<span class="signing-valid" ng-if="::tag.cosign_signature_tag">
<i class="fa shield-icon ci-shield-check-full" data-title="This tag has been signed via cosign." bs-tooltip></i>
</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)"
@ -304,6 +325,11 @@
<div class="manifest-label-list" repository="repository"
manifest-digest="tag.manifest_digest" cache="labelCache"></div>
<!-- Cosign Signatures -->
<div class="cosign-signature-row" ng-if="tag.cosign_signature_tag">
<cosign-link repository="repository" image-id="tag.image_id" manifest-digest="tag.cosign_signature_manifest_digest" tag-name="tag.cosign_signature_tag" tag-name="tag.cosign_signature_tag" signed-image-id="tag.image_id" signed-manifest-digest="tag.manifest_digest"></cosign-link>
</div>
<!-- Delegations -->
<div class="signing-delegations-list" ng-if="repository.trust_enabled">
<tag-signing-display compact="false" tag="tag" delegations="repoDelegationsInfo"></tag-signing-display>

View File

@ -43,10 +43,12 @@ angular.module('quay').directive('repoPanelTags', function () {
$scope.tagsPerPage = 25;
$scope.expandedView = false;
$scope.showCosignSignatures = false;
$scope.labelCache = {};
$scope.manifestVulnerabilities = {};
$scope.repoDelegationsInfo = null;
$scope.cosignedManifests = {};
var loadRepoSignatures = function() {
if (!$scope.repository || !$scope.repository.trust_enabled) {
@ -67,6 +69,35 @@ angular.module('quay').directive('repoPanelTags', function () {
});
};
var matchCosignSignature = function(tag) {
// matches cosign style tags and returns the match with a matching group containing the signed manifest digest
var cosignNamingPattern = new RegExp('^sha256-([a-f0-9]{64})\.sig$');
tag = tag.trim()
return tag.match(cosignNamingPattern);
}
var getCosignSignatures = function() {
if (!$scope.repositoryTags || !$scope.selectedTags) { return; }
// Build a list of all digests which are signed by cosign
$scope.cosignedManifests = [];
for (var tag in $scope.repositoryTags) {
if (!$scope.repositoryTags.hasOwnProperty(tag)) { continue; }
var cosignSignatureTag = matchCosignSignature(tag);
if (cosignSignatureTag) {
signedManifestDigest = cosignSignatureTag[1]; // cosign signature tags contain the signature of the signed manifest
$scope.cosignedManifests["sha256:" + signedManifestDigest] = { // map signed manifests to their cosign signature artifact
'signatureTagName': tag,
'signatureManifestDigest': $scope.repositoryTags[tag].manifest_digest
};
}
}
}
var setTagState = function() {
if (!$scope.repositoryTags || !$scope.selectedTags) { return; }
@ -75,11 +106,15 @@ angular.module('quay').directive('repoPanelTags', function () {
for (var tag in $scope.repositoryTags) {
if (!$scope.repositoryTags.hasOwnProperty(tag)) { continue; }
if (matchCosignSignature(tag) && !$scope.showCosignSignatures) { continue; }
var tagData = $scope.repositoryTags[tag];
var tagInfo = $.extend(tagData, {
'name': tag,
'last_modified_datetime': TableService.getReversedTimestamp(tagData.last_modified),
'expiration_date': tagData.expiration ? TableService.getReversedTimestamp(tagData.expiration) : null,
'cosign_signature_tag': $scope.cosignedManifests.hasOwnProperty(tagData.manifest_digest) ? $scope.cosignedManifests[tagData.manifest_digest].signatureTagName : null,
'cosign_signature_manifest_digest': $scope.cosignedManifests.hasOwnProperty(tagData.manifest_digest) ? $scope.cosignedManifests[tagData.manifest_digest].signatureManifestDigest : null,
});
allTags.push(tagInfo);
@ -230,6 +265,7 @@ angular.module('quay').directive('repoPanelTags', function () {
$scope.$watch('options.predicate', setTagState);
$scope.$watch('options.reverse', setTagState);
$scope.$watch('options.filter', setTagState);
$scope.$watch('showCosignSignatures', setTagState);
$scope.$watch('options.page', function(page) {
if (page != null && $scope.checkedTags) {
@ -247,6 +283,7 @@ angular.module('quay').directive('repoPanelTags', function () {
$scope.$watch('repository', function(updatedRepoObject, previousRepoObject) {
// Process each of the tags.
getCosignSignatures();
setTagState();
loadRepoSignatures();
});
@ -254,6 +291,7 @@ angular.module('quay').directive('repoPanelTags', function () {
$scope.$watch('repositoryTags', function(newTags, oldTags) {
if (newTags === oldTags) { return; }
// Process each of the tags.
getCosignSignatures();
setTagState();
loadRepoSignatures();
}, true);
@ -399,6 +437,19 @@ angular.module('quay').directive('repoPanelTags', function () {
return tag.name.match(r);
};
$scope.cosignTagFilter = function(tag) {
var r = new RegExp('^sha256-[A-Fa-f0-9]{64}\.sig$');
return tag.name.match(r);
};
$scope.signedTagFilter = function(tag) {
return tag.hasOwnProperty("cosign_signature_manifest_digest") && tag.cosign_signature_manifest_digest != null;
};
$scope.unsignedTagFilter = function(tag) {
return tag.hasOwnProperty("cosign_signature_manifest_digest") && tag.cosign_signature_manifest_digest == null;
};
$scope.allTagFilter = function(tag) {
return true;
};
@ -438,6 +489,10 @@ angular.module('quay').directive('repoPanelTags', function () {
$scope.expandedView = expanded;
};
$scope.toggleCosignSignatureDisplay = function() {
$scope.showCosignSignatures = !$scope.showCosignSignatures;
};
$scope.getTagNames = function(checked) {
var names = checked.map(function(tag) {
return tag.name;

View File

@ -0,0 +1,35 @@
<span class="manifest-link">
<span class="id-label cas" ng-if="::$ctrl.hasSHA256($ctrl.manifestDigest)"
data-title="The artifact containing the cosign signature for this tag."
data-container="body"
ng-click="$ctrl.showCopyBox()"
bs-tooltip>cosign</span>
<a ng-href="/repository/{{ ::$ctrl.repository.namespace }}/{{ ::$ctrl.repository.name }}/manifest/{{ ::$ctrl.manifestDigest }}">
{{ $ctrl.tagName }}
</a>
<span ng-if="::!$ctrl.hasSHA256($ctrl.manifestDigest)">{{ ::$ctrl.imageId.substr(0, 12) }}</span>
<div class="modal fade co-dialog" ng-if="$ctrl.showingCopyBox">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" ng-click="$ctrl.hideCopyBox()"
aria-hidden="true">&times;</button>
<h4 class="modal-title"><span ng-if="$ctrl.hasSHA256($ctrl.manifestDigest)">Cosign Signature artifact</span><span ng-if="!$ctrl.hasSHA256($ctrl.manifestDigest)">V1 ID</span></h4>
</div>
<div class="modal-body">
<label>Signature digest:</label>
<div class="copy-box" value="$ctrl.manifestDigest"></div>
<label style="margin-top: 20px;">Signature tag:</label>
<div class="copy-box" value="$ctrl.tagName"></div>
</div>
<div class="modal-footer" ng-show="!working">
<button type="button" class="btn btn-default" ng-click="$ctrl.hideCopyBox()">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</span>

View File

@ -0,0 +1,58 @@
import { Input, Component, Inject } from 'ng-metadata/core';
import { Repository } from '../../../types/common.types';
/**
* A component that links to a manifest view.
*/
@Component({
selector: 'cosign-link',
templateUrl: '/static/js/directives/ui/cosign-link/cosign-link.component.html'
})
export class CosignLinkComponent {
@Input('<') public repository: Repository;
@Input('<') public manifestDigest: string;
@Input('<') public imageId: string;
@Input('<') public tagName: string;
@Input('<') public signedTagName: string;
@Input('<') public signedImageId: string;
@Input('<') public signedManifestDigest: string;
private showingCopyBox: boolean = false;
constructor(@Inject('$timeout') private $timeout, @Inject('$element') private $element) {
}
private hasSHA256(digest: string) {
return digest && digest.indexOf('sha256:') == 0;
}
private getShortDigest(digest: string) {
if (!digest) { return ''; }
return digest.substr('sha256:'.length).substr(0, 12);
}
private getShortTagName(name: string) {
if (!name) { return ''; }
return name.substring(0, 'sha256-'.length + 10) + '...' + name.substring(name.length-('.sig').length-10,name.length);
}
private showCopyBox() {
this.showingCopyBox = true;
// Necessary to wait for digest cycle to complete.
this.$timeout(() => {
this.$element.find('.modal').modal('show');
}, 10);
};
private hideCopyBox() {
this.$element.find('.modal').modal('hide');
// Wait for the modal to hide before removing from the DOM.
this.$timeout(() => {
this.showingCopyBox = false;
}, 10);
};
}

View File

@ -77,8 +77,8 @@
if (resp.has_additional) {
loadPaginatedRepositoryTags(page + 1);
} else {
$scope.viewScope.tagsLoading = false;
}
$scope.viewScope.tagsLoading = false;
}
});
};

View File

@ -41,6 +41,7 @@ import { TriggerDescriptionComponent } from './directives/ui/trigger-description
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 { CosignLinkComponent } from './directives/ui/cosign-link/cosign-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';
@ -89,6 +90,7 @@ import * as Clipboard from 'clipboard';
TimeAgoComponent,
TimeDisplayComponent,
AppSpecificTokenManagerComponent,
CosignLinkComponent,
ManifestLinkComponent,
ManifestSecurityView,
RepoStateComponent,