mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): revalidate release evidence at closure
This commit is contained in:
@@ -1304,6 +1304,7 @@ function auditReleaseWorkflow(source) {
|
||||
sameRunRecords: true,
|
||||
sameRunPrivateEvidenceReceipts: true,
|
||||
deterministicPrivateEvidenceReceipts: true,
|
||||
privateEvidenceFreshnessRevalidatedAtClosure: true,
|
||||
exactScopeClosure: true,
|
||||
standaloneInspection: true,
|
||||
tagPromotionAuthority: 'complete_verified_release_set',
|
||||
|
||||
@@ -382,9 +382,20 @@ function inspectPrivateReleaseEvidenceReceipt(actual, options) {
|
||||
) {
|
||||
fail('receipt shape or release binding is invalid');
|
||||
}
|
||||
if (!Number.isFinite(Date.parse(actual.evidence.observedAt))) {
|
||||
const observedAtMs = Date.parse(actual.evidence.observedAt);
|
||||
if (!Number.isFinite(observedAtMs)) {
|
||||
fail('receipt freshness binding is invalid');
|
||||
}
|
||||
if (options.validationClockMs !== undefined) {
|
||||
const validationClockMs = options.validationClockMs;
|
||||
if (
|
||||
!Number.isSafeInteger(validationClockMs) ||
|
||||
observedAtMs > validationClockMs + MAX_FUTURE_SKEW_MS ||
|
||||
validationClockMs - observedAtMs > MAX_EVIDENCE_AGE_SECONDS * 1000
|
||||
) {
|
||||
fail('receipt is outside the release freshness window');
|
||||
}
|
||||
}
|
||||
const { receiptDigest, ...unsigned } = actual;
|
||||
if (
|
||||
!DIGEST_PATTERN.test(receiptDigest || '') ||
|
||||
|
||||
@@ -245,11 +245,14 @@ function expectedEvidenceKinds(releaseScope) {
|
||||
return releaseScope === 'local' ? [] : [...EVIDENCE_KINDS];
|
||||
}
|
||||
|
||||
function validateEvidenceReceipts(receipts, candidate) {
|
||||
function validateEvidenceReceipts(receipts, candidate, validationClockMs) {
|
||||
if (!Array.isArray(receipts)) {
|
||||
fail('private release evidence receipts must be an array');
|
||||
}
|
||||
const expectedKinds = expectedEvidenceKinds(candidate.release.scope);
|
||||
if (expectedKinds.length > 0 && !Number.isSafeInteger(validationClockMs)) {
|
||||
fail('private evidence closure validation clock is invalid');
|
||||
}
|
||||
if (receipts.length !== expectedKinds.length) {
|
||||
fail('private release evidence receipt count differs from release scope');
|
||||
}
|
||||
@@ -268,6 +271,7 @@ function validateEvidenceReceipts(receipts, candidate) {
|
||||
sourceRef: candidate.release.sourceRef,
|
||||
releaseScope: candidate.release.scope,
|
||||
evidenceKind,
|
||||
validationClockMs,
|
||||
});
|
||||
receiptsByKind.set(evidenceKind, receipt);
|
||||
}
|
||||
@@ -282,6 +286,7 @@ function createReleaseSet(options) {
|
||||
const evidenceReceipts = validateEvidenceReceipts(
|
||||
options.evidenceReceipts,
|
||||
candidate,
|
||||
options.validationClockMs,
|
||||
);
|
||||
if (!Array.isArray(options.records)) fail('image records must be an array');
|
||||
if (options.records.length !== candidate.images.length) {
|
||||
@@ -363,6 +368,8 @@ function auditReleaseSet(actual, options) {
|
||||
evidenceReceiptDigests: Object.freeze(
|
||||
actual.evidenceReceipts.map((entry) => entry.receiptDigest),
|
||||
),
|
||||
privateEvidenceFreshnessRevalidatedAtClosure:
|
||||
actual.evidenceReceipts.length === 0 ? 'not_applicable' : true,
|
||||
tagPromotionAuthority: actual.promotion.authority,
|
||||
});
|
||||
}
|
||||
@@ -646,7 +653,12 @@ function parseArguments(argv) {
|
||||
});
|
||||
}
|
||||
|
||||
function runCli(argv, root = DEFAULT_ROOT, output = process.stdout) {
|
||||
function runCli(
|
||||
argv,
|
||||
root = DEFAULT_ROOT,
|
||||
output = process.stdout,
|
||||
dependencies = { now: Date.now },
|
||||
) {
|
||||
const options = parseArguments(argv);
|
||||
if (options.mode === 'inspect') {
|
||||
const report = readCanonicalJson(options.report, 'release set');
|
||||
@@ -666,12 +678,20 @@ function runCli(argv, root = DEFAULT_ROOT, output = process.stdout) {
|
||||
options.evidenceReceipts,
|
||||
candidate,
|
||||
);
|
||||
let validationClockMs;
|
||||
if (candidate.release.scope !== 'local') {
|
||||
if (typeof dependencies?.now !== 'function') {
|
||||
fail('release closure clock dependency is invalid');
|
||||
}
|
||||
validationClockMs = dependencies.now();
|
||||
}
|
||||
if (options.mode === 'aggregate') {
|
||||
const releaseSet = createReleaseSet({
|
||||
...options,
|
||||
candidate,
|
||||
records,
|
||||
evidenceReceipts,
|
||||
validationClockMs,
|
||||
root,
|
||||
});
|
||||
writeNoReplace(options.output, releaseSet);
|
||||
@@ -684,6 +704,7 @@ function runCli(argv, root = DEFAULT_ROOT, output = process.stdout) {
|
||||
candidate,
|
||||
records,
|
||||
evidenceReceipts,
|
||||
validationClockMs,
|
||||
root,
|
||||
});
|
||||
output.write(canonicalJson(audit));
|
||||
|
||||
Reference in New Issue
Block a user