feat(ql3): bind private release evidence receipts

This commit is contained in:
whyour
2026-08-18 01:48:27 +08:00
parent fd433c41cd
commit 3e7bfa8087
17 changed files with 1452 additions and 66 deletions
+63 -15
View File
@@ -54,13 +54,15 @@ test('accepts the reviewed native CI and digest release contracts', () => {
sourceAware: true,
privateEphemeralRunner: true,
maximumAgeSeconds: 86400,
artifactUpload: false,
privateReportUpload: false,
contentFreeReceiptPublished: true,
},
cloudNativePgDisasterRecoveryEvidence: {
sourceAware: true,
privateEphemeralRunner: true,
maximumAgeSeconds: 86400,
artifactUpload: false,
privateReportUpload: false,
contentFreeReceiptPublished: true,
staticLocksReaudited: true,
},
osVulnerabilityScan: {
@@ -96,6 +98,7 @@ test('accepts the reviewed native CI and digest release contracts', () => {
releaseSet: {
sourceDerived: true,
sameRunRecords: true,
sameRunPrivateEvidenceReceipts: true,
exactScopeClosure: true,
standaloneInspection: true,
tagPromotionAuthority: 'complete_verified_release_set',
@@ -105,7 +108,7 @@ test('accepts the reviewed native CI and digest release contracts', () => {
},
durableCatalog: {
repository: 'qinglong3-release-catalog',
artifactType: 'application/vnd.qinglong.release-set.v1+json',
artifactType: 'application/vnd.qinglong.release-set.v2+json',
basenameOnly: true,
crossRunnerDeterministic: true,
byteExactRoundTrip: true,
@@ -175,6 +178,17 @@ test('rejects removal of the durable release-catalog contract tests', () => {
);
});
test('rejects removal of the private release evidence receipt contract tests', () => {
const mutated = ciSource.replace(
'test/back/ql3PrivateReleaseEvidenceReceiptContract.test.cjs',
'test/back/private-receipt-tests-removed.test.cjs',
);
assert.throws(
() => auditClusterImageCiWorkflow(mutated),
/private evidence receipt/,
);
});
test('rejects removal of the deployment-lock materialization contract tests', () => {
const mutated = ciSource.replace(
'test/back/ql3DeploymentLockContract.test.cjs',
@@ -470,14 +484,14 @@ test('rejects a reusable private evidence runner', () => {
);
});
test('rejects private evidence uploaded as a workflow artifact', () => {
test('rejects private report content uploaded instead of its digest-only receipt', () => {
const mutated = releaseSource.replace(
' - name: Re-audit commit-scoped private production evidence',
' - uses: actions/upload-artifact@v4\n - name: Re-audit commit-scoped private production evidence',
'path: ${{ runner.temp }}/ql3-private-release-evidence-receipts/worker-management.json',
'path: /run/qinglong3-release-evidence/${{ github.sha }}/worker-management-release-evidence.json',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/only the reviewed|never persist private evidence/,
/content-free receipt and upload steps/,
);
});
@@ -655,8 +669,8 @@ test('rejects scanning a daemon tag instead of the exact OCI tar', () => {
test('rejects retaining scanned native release artifacts for more than one day', () => {
const mutated = releaseSource.replace(
' retention-days: 1',
' retention-days: 2',
'name: ql3-release-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.image }}-${{ matrix.image_arch }}\n path: ${{ runner.temp }}/ql3-native\n if-no-files-found: error\n retention-days: 1',
'name: ql3-release-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.image }}-${{ matrix.image_arch }}\n path: ${{ runner.temp }}/ql3-native\n if-no-files-found: error\n retention-days: 2',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
@@ -666,8 +680,8 @@ test('rejects retaining scanned native release artifacts for more than one day',
test('rejects overwrite authority on a scanned native artifact', () => {
const mutated = releaseSource.replace(
' overwrite: false',
' overwrite: true',
'path: ${{ runner.temp }}/ql3-native\n if-no-files-found: error\n retention-days: 1\n compression-level: 0\n overwrite: false',
'path: ${{ runner.temp }}/ql3-native\n if-no-files-found: error\n retention-days: 1\n compression-level: 0\n overwrite: true',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
@@ -676,10 +690,16 @@ test('rejects overwrite authority on a scanned native artifact', () => {
});
test('rejects a movable upload-artifact action', () => {
const mutated = releaseSource.replace(
'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1',
'actions/upload-artifact@v7.0.1',
);
const marker =
' - name: Upload the scanned immutable native OCI artifact';
const offset = releaseSource.indexOf(marker);
assert.notEqual(offset, -1);
const mutated = `${releaseSource.slice(0, offset)}${releaseSource
.slice(offset)
.replace(
'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1',
'actions/upload-artifact@v7.0.1',
)}`;
assert.throws(
() => auditReleaseWorkflow(mutated),
/upload only bound immutable evidence/,
@@ -864,6 +884,34 @@ test('rejects release-set closure before every image publisher succeeds', () =>
assert.throws(() => auditReleaseWorkflow(mutated), /complete publish matrix/);
});
test('rejects release-set closure detached from same-run private evidence receipts', () => {
const mutated = releaseSource.replace(
'ql3-private-release-evidence-${{ github.run_id }}-${{ github.run_attempt }}-*',
'ql3-private-release-evidence-${{ github.run_id }}-*',
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/release-set job must download only same-run records/,
);
});
test('rejects release-set aggregation that ignores private evidence receipts', () => {
const marker =
' - name: Aggregate and independently audit the complete release set';
const offset = releaseSource.indexOf(marker);
assert.notEqual(offset, -1);
const mutated = `${releaseSource.slice(0, offset)}${releaseSource
.slice(offset)
.replace(
' --evidence-receipts="${evidence_receipts}" \\',
' --evidence-receipts="${RUNNER_TEMP}/empty" \\',
)}`;
assert.throws(
() => auditReleaseWorkflow(mutated),
/release-set job must download only same-run records/,
);
});
test('rejects image records detached from the same workflow attempt', () => {
const mutated = releaseSource.replace(
'ql3-release-record-${{ github.run_id }}-${{ github.run_attempt }}-*',
@@ -42,6 +42,9 @@ const {
catalogLockedArtifacts,
deploymentArtifacts,
} = require('../../scripts/ql3-kubernetes-deployment-live-contract.cjs');
const {
privateReleaseEvidenceReceipts,
} = require('./ql3ReleaseEvidenceFixture.cjs');
const root = path.resolve(__dirname, '../..');
const version = readReleaseIdentity(root).version;
@@ -97,6 +100,7 @@ function releaseSet(scope) {
root,
candidate,
records,
evidenceReceipts: privateReleaseEvidenceReceipts(candidate.release),
...identity,
releaseScope: scope,
});
@@ -0,0 +1,275 @@
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const test = require('node:test');
const {
createCloudNativePgReceipt,
createWorkerReceipt,
inspectPrivateReleaseEvidenceReceipt,
parseArguments,
runCli,
} = require('../../scripts/ql3-private-release-evidence-receipt-contract.cjs');
const {
privateReleaseEvidenceReceipt,
privateReleaseEvidenceReceipts,
} = require('./ql3ReleaseEvidenceFixture.cjs');
const RELEASE = Object.freeze({
version: '3.0.0-alpha.0',
sourceRevision: 'a'.repeat(40),
sourceRef: 'refs/tags/v3.0.0-alpha.0',
scope: 'cluster',
});
function options(evidenceKind) {
return Object.freeze({
version: RELEASE.version,
sourceRevision: RELEASE.sourceRevision,
sourceRef: RELEASE.sourceRef,
releaseScope: RELEASE.scope,
evidenceKind,
});
}
function temporaryDirectory(t) {
const directory = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-private-release-receipt-')),
);
fs.chmodSync(directory, 0o700);
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
return directory;
}
test('accepts only the exact Worker and disaster-recovery receipt projections', () => {
const receipts = privateReleaseEvidenceReceipts(RELEASE);
assert.deepEqual(
receipts.map((receipt) => receipt.evidenceKind),
['worker-management', 'cloudnativepg-disaster-recovery'],
);
for (const receipt of receipts) {
const result = inspectPrivateReleaseEvidenceReceipt(
receipt,
options(receipt.evidenceKind),
);
assert.equal(result.compatible, true);
assert.equal(result.receiptDigest, receipt.receiptDigest);
assert.equal(
result.publicConsumerReplay,
'not_possible_without_private_reports',
);
assert.equal(JSON.stringify(receipt).includes('/run/qinglong3'), false);
assert.equal(JSON.stringify(receipt).includes('token'), false);
}
});
test('rejects source, scope, freshness, digest and static-lock drift', () => {
const mutations = [
(receipt) => {
receipt.release.sourceRevision = 'b'.repeat(40);
},
(receipt) => {
receipt.release.scope = 'all';
},
(receipt) => {
receipt.evidence.observedAt = '2026-08-16T00:00:00.000Z';
},
(receipt) => {
receipt.evidence.reportDigest = `sha256:${'0'.repeat(64)}`;
},
(receipt) => {
receipt.staticAudits.pop();
},
];
for (const mutate of mutations) {
const receipt = JSON.parse(
JSON.stringify(
privateReleaseEvidenceReceipt(
RELEASE,
'cloudnativepg-disaster-recovery',
),
),
);
mutate(receipt);
assert.throws(
() =>
inspectPrivateReleaseEvidenceReceipt(
receipt,
options('cloudnativepg-disaster-recovery'),
),
/receipt|freshness/,
);
}
});
test('rejects publishing private material in an otherwise re-digested receipt', () => {
const receipt = JSON.parse(
JSON.stringify(privateReleaseEvidenceReceipt(RELEASE, 'worker-management')),
);
receipt.evidence.privateReport = { credential: 'must-not-publish' };
assert.throws(
() =>
inspectPrivateReleaseEvidenceReceipt(
receipt,
options('worker-management'),
),
/receipt shape/,
);
});
test('audit CLI accepts a canonical receipt and rejects open argument shapes', (t) => {
const directory = temporaryDirectory(t);
const receiptPath = path.join(directory, 'worker-management.json');
const receipt = privateReleaseEvidenceReceipt(RELEASE, 'worker-management');
fs.writeFileSync(receiptPath, `${JSON.stringify(receipt)}\n`, {
mode: 0o600,
});
const args = [
'--mode=audit',
`--version=${RELEASE.version}`,
`--source-revision=${RELEASE.sourceRevision}`,
`--source-ref=${RELEASE.sourceRef}`,
`--release-scope=${RELEASE.scope}`,
'--evidence-kind=worker-management',
`--receipt=${receiptPath}`,
];
const writes = [];
assert.equal(
runCli(args, path.resolve(__dirname, '../..'), {
write(value) {
writes.push(value);
},
}).compatible,
true,
);
assert.equal(
JSON.parse(writes.join('')).receiptDigest,
receipt.receiptDigest,
);
assert.throws(
() => parseArguments([...args, '--extra=true']),
/arguments are invalid/,
);
assert.throws(
() =>
runCli(
args.map((entry) =>
entry === `--release-scope=${RELEASE.scope}`
? '--release-scope=local'
: entry,
),
path.resolve(__dirname, '../..'),
{ write() {} },
),
/Cluster-capable/,
);
});
test('create wrappers publish only no-replace digest projections after their source-aware gates', (t) => {
const directory = temporaryDirectory(t);
const reportPath = path.join(directory, 'private-report.json');
fs.writeFileSync(
reportPath,
`${JSON.stringify({ observedAt: '2026-08-18T00:00:00.000Z' })}\n`,
{ mode: 0o600 },
);
const common = {
root: path.resolve(__dirname, '../..'),
version: RELEASE.version,
sourceRevision: RELEASE.sourceRevision,
sourceRef: RELEASE.sourceRef,
releaseScope: RELEASE.scope,
reportFile: reportPath,
};
const now = () => Date.parse('2026-08-18T00:05:00.000Z');
const workerPath = path.join(directory, 'worker.json');
const worker = createWorkerReceipt(
{
...common,
ceremonyReportFile: path.join(directory, 'private-ceremony.json'),
durableAuditReportFile: path.join(directory, 'private-durable.json'),
pkiRotationReportFile: path.join(directory, 'private-pki.json'),
caRolloverReportFile: path.join(directory, 'private-ca.json'),
outputFile: workerPath,
},
{
now,
auditGate(options) {
assert.equal(options.sourceCommit, RELEASE.sourceRevision);
return {
fixture: 'qinglong/worker-credential-management-release-gate@v1',
evidenceReportSha256: `sha256:${'5'.repeat(64)}`,
};
},
},
);
assert.equal(worker.staticAudits.length, 0);
assert.equal(fs.statSync(workerPath).mode & 0o777, 0o600);
assert.equal(JSON.stringify(worker).includes('private-report'), false);
const drPath = path.join(directory, 'dr.json');
const dr = createCloudNativePgReceipt(
{ ...common, outputFile: drPath },
{
now,
validateEvidence(_report, options) {
assert.equal(options.sourceCommit, RELEASE.sourceRevision);
return {
fixture: 'qinglong/cloudnativepg-disaster-recovery@v1',
compatible: true,
};
},
},
);
assert.deepEqual(
dr.staticAudits.map((entry) => entry.name),
[
'cloudnativepg-backup',
'barman-cloud-supply-chain',
'cert-manager-selection',
],
);
assert.equal(fs.statSync(drPath).mode & 0o777, 0o600);
assert.throws(
() =>
createCloudNativePgReceipt(
{ ...common, outputFile: drPath },
{
now,
validateEvidence() {
return {
fixture: 'qinglong/cloudnativepg-disaster-recovery@v1',
compatible: true,
};
},
},
),
/output must not already exist/,
);
fs.chmodSync(reportPath, 0o640);
assert.throws(
() =>
createWorkerReceipt(
{
...common,
ceremonyReportFile: path.join(directory, 'private-ceremony.json'),
durableAuditReportFile: path.join(directory, 'private-durable.json'),
pkiRotationReportFile: path.join(directory, 'private-pki.json'),
caRolloverReportFile: path.join(directory, 'private-ca.json'),
outputFile: path.join(directory, 'untrusted-worker.json'),
},
{
now,
auditGate() {
return {
fixture: 'qinglong/worker-credential-management-release-gate@v1',
evidenceReportSha256: `sha256:${'5'.repeat(64)}`,
};
},
},
),
/bounded canonical regular file/,
);
});
@@ -19,6 +19,9 @@ const {
const {
auditClusterImageCiWorkflow,
} = require('../../scripts/ql3-cluster-image-release-audit.cjs');
const {
privateReleaseEvidenceReceipts,
} = require('./ql3ReleaseEvidenceFixture.cjs');
const ROOT = path.resolve(__dirname, '../..');
const SCRIPT = path.join(
@@ -53,6 +56,12 @@ function selectedImages(scope) {
function releaseSet(scope) {
const selected = selectedImages(scope);
const release = {
version: VERSION,
sourceRevision: REVISION,
sourceRef: SOURCE_REF,
scope,
};
const images = selected.map(([name, repository], index) => {
const digest = `sha256:${(index + 1).toString(16).repeat(64)}`;
const reference = `ghcr.io/${OWNER}/${repository}`;
@@ -70,13 +79,8 @@ function releaseSet(scope) {
const names = images.map((entry) => entry.name);
const unsigned = {
schemaVersion: 1,
schema: 'qinglong/release-set@v1',
release: {
version: VERSION,
sourceRevision: REVISION,
sourceRef: SOURCE_REF,
scope,
},
schema: 'qinglong/release-set@v2',
release,
candidate: {
schema: 'qinglong/release-candidate-contract@v1',
contractDigest: `sha256:${'b'.repeat(64)}`,
@@ -95,6 +99,7 @@ function releaseSet(scope) {
images: names.filter((name) => name !== 'local'),
},
},
evidenceReceipts: privateReleaseEvidenceReceipts(release),
images,
promotion: {
authority: 'complete_verified_release_set',
@@ -111,6 +116,10 @@ function releaseSet(scope) {
'os-vulnerability',
'release-candidate-contract',
],
privateReleaseEvidenceReceipts:
scope === 'local'
? []
: ['worker-management', 'cloudnativepg-disaster-recovery'],
releaseSetBuildProvenance: true,
},
};
@@ -28,6 +28,9 @@ const {
const {
readReleaseIdentity,
} = require('../../scripts/lib/ql3-release-identity.cjs');
const {
privateReleaseEvidenceReceipts,
} = require('./ql3ReleaseEvidenceFixture.cjs');
const root = path.resolve(__dirname, '../..');
const version = readReleaseIdentity(root).version;
@@ -65,6 +68,7 @@ function releaseSet(scope) {
root,
candidate,
records,
evidenceReceipts: privateReleaseEvidenceReceipts(candidate.release),
...identity,
releaseScope: scope,
});
+75
View File
@@ -0,0 +1,75 @@
'use strict';
const crypto = require('node:crypto');
const RECEIPT_SCHEMA = 'qinglong/private-release-evidence-receipt@v1';
const FIXTURES = Object.freeze({
'worker-management': 'qinglong/worker-credential-management-release-gate@v1',
'cloudnativepg-disaster-recovery':
'qinglong/cloudnativepg-disaster-recovery@v1',
});
const STATIC_AUDITS = Object.freeze([
'cloudnativepg-backup',
'barman-cloud-supply-chain',
'cert-manager-selection',
]);
function sha256(value) {
return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
}
function privateReleaseEvidenceReceipt(release, evidenceKind) {
const staticAudits =
evidenceKind === 'cloudnativepg-disaster-recovery'
? STATIC_AUDITS.map((name, index) => ({
name,
auditDigest: `sha256:${String(index + 7).repeat(64)}`,
compatible: true,
}))
: [];
const unsigned = {
schemaVersion: 1,
schema: RECEIPT_SCHEMA,
release: { ...release },
evidenceKind,
evidence: {
fixture: FIXTURES[evidenceKind],
observedAt: '2026-08-18T00:00:00.000Z',
validatedAt: '2026-08-18T00:05:00.000Z',
maximumAgeSeconds: 86_400,
reportDigest: `sha256:${
evidenceKind === 'worker-management' ? '5' : '6'
}`.padEnd(71, evidenceKind === 'worker-management' ? '5' : '6'),
sourceReportsUploaded: false,
},
staticAudits,
verification: {
sourceAwareAudit: true,
privateEvidenceReplayed: true,
publicConsumerReplay: 'not_possible_without_private_reports',
privateReportContentPublished: false,
compatible: true,
},
};
return Object.freeze({
...unsigned,
receiptDigest: sha256(JSON.stringify(unsigned)),
});
}
function privateReleaseEvidenceReceipts(release) {
return release.scope === 'local'
? []
: [
privateReleaseEvidenceReceipt(release, 'worker-management'),
privateReleaseEvidenceReceipt(
release,
'cloudnativepg-disaster-recovery',
),
];
}
module.exports = Object.freeze({
privateReleaseEvidenceReceipt,
privateReleaseEvidenceReceipts,
});
+80 -1
View File
@@ -19,6 +19,9 @@ const {
const {
readReleaseIdentity,
} = require('../../scripts/lib/ql3-release-identity.cjs');
const {
privateReleaseEvidenceReceipts,
} = require('./ql3ReleaseEvidenceFixture.cjs');
const root = path.resolve(__dirname, '../..');
const version = readReleaseIdentity(root).version;
@@ -52,6 +55,10 @@ function recordsFor(releaseCandidate) {
);
}
function evidenceFor(releaseCandidate) {
return privateReleaseEvidenceReceipts(releaseCandidate.release);
}
function temporaryDirectory(t) {
const directory = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-release-set-')),
@@ -71,6 +78,7 @@ test('aggregates the independent Local image into one immutable release set', ()
root,
candidate: releaseCandidate,
records,
evidenceReceipts: evidenceFor(releaseCandidate),
...identity,
releaseScope: 'local',
});
@@ -88,6 +96,7 @@ test('aggregates the independent Local image into one immutable release set', ()
root,
candidate: releaseCandidate,
records,
evidenceReceipts: evidenceFor(releaseCandidate),
...identity,
releaseScope: 'local',
}).compatible,
@@ -101,6 +110,7 @@ test('closes cluster and all scopes over the exact candidate image order', () =>
root,
candidate: clusterCandidate,
records: recordsFor(clusterCandidate).reverse(),
evidenceReceipts: evidenceFor(clusterCandidate),
...identity,
releaseScope: 'cluster',
});
@@ -114,12 +124,17 @@ test('closes cluster and all scopes over the exact candidate image order', () =>
'admin',
'worker',
]);
assert.deepEqual(
clusterSet.evidenceReceipts.map((entry) => entry.evidenceKind),
['worker-management', 'cloudnativepg-disaster-recovery'],
);
const allCandidate = candidate('all');
const allSet = createReleaseSet({
root,
candidate: allCandidate,
records: recordsFor(allCandidate),
evidenceReceipts: evidenceFor(allCandidate),
...identity,
releaseScope: 'all',
});
@@ -127,6 +142,51 @@ test('closes cluster and all scopes over the exact candidate image order', () =>
assert.deepEqual(allSet.deploymentFamilies.local.images, ['local']);
});
test('requires exact private evidence receipts only for Cluster-capable scopes', () => {
const releaseCandidate = candidate('cluster');
const records = recordsFor(releaseCandidate);
const receipts = evidenceFor(releaseCandidate);
assert.throws(
() =>
createReleaseSet({
root,
candidate: releaseCandidate,
records,
evidenceReceipts: receipts.slice(1),
...identity,
releaseScope: 'cluster',
}),
/receipt count differs/,
);
const drifted = JSON.parse(JSON.stringify(receipts));
drifted[0].release.sourceRevision = 'c'.repeat(40);
assert.throws(
() =>
createReleaseSet({
root,
candidate: releaseCandidate,
records,
evidenceReceipts: drifted,
...identity,
releaseScope: 'cluster',
}),
/receipt shape|release binding/,
);
const localCandidate = candidate('local');
assert.throws(
() =>
createReleaseSet({
root,
candidate: localCandidate,
records: recordsFor(localCandidate),
evidenceReceipts: [receipts[0]],
...identity,
releaseScope: 'local',
}),
/receipt count differs/,
);
});
test('rejects missing, duplicate and cross-candidate image evidence', () => {
const releaseCandidate = candidate('cluster');
const records = recordsFor(releaseCandidate);
@@ -136,6 +196,7 @@ test('rejects missing, duplicate and cross-candidate image evidence', () => {
root,
candidate: releaseCandidate,
records: records.slice(1),
evidenceReceipts: evidenceFor(releaseCandidate),
...identity,
releaseScope: 'cluster',
}),
@@ -147,6 +208,7 @@ test('rejects missing, duplicate and cross-candidate image evidence', () => {
root,
candidate: releaseCandidate,
records: [records[0], records[0], records[2], records[3]],
evidenceReceipts: evidenceFor(releaseCandidate),
...identity,
releaseScope: 'cluster',
}),
@@ -160,6 +222,7 @@ test('rejects missing, duplicate and cross-candidate image evidence', () => {
root,
candidate: releaseCandidate,
records: [mutated, ...records.slice(1)],
evidenceReceipts: evidenceFor(releaseCandidate),
...identity,
releaseScope: 'cluster',
}),
@@ -199,6 +262,7 @@ test('rejects mutable identity, malformed owner and post-aggregate drift', () =>
root,
candidate: releaseCandidate,
records,
evidenceReceipts: evidenceFor(releaseCandidate),
...identity,
releaseScope: 'local',
});
@@ -210,6 +274,7 @@ test('rejects mutable identity, malformed owner and post-aggregate drift', () =>
root,
candidate: releaseCandidate,
records,
evidenceReceipts: evidenceFor(releaseCandidate),
...identity,
releaseScope: 'local',
}),
@@ -224,6 +289,7 @@ test('inspects a release set without short-lived candidate or image records', ()
root,
candidate: releaseCandidate,
records: recordsFor(releaseCandidate),
evidenceReceipts: evidenceFor(releaseCandidate),
...identity,
releaseScope: scope,
});
@@ -243,6 +309,7 @@ test('standalone inspection rejects image, family and self-digest drift', () =>
root,
candidate: releaseCandidate,
records: recordsFor(releaseCandidate),
evidenceReceipts: evidenceFor(releaseCandidate),
...identity,
releaseScope: 'cluster',
});
@@ -256,6 +323,9 @@ test('standalone inspection rejects image, family and self-digest drift', () =>
(value) => {
value.releaseSetDigest = `sha256:${'0'.repeat(64)}`;
},
(value) => {
value.evidenceReceipts[0].receiptDigest = `sha256:${'0'.repeat(64)}`;
},
]) {
const drifted = JSON.parse(JSON.stringify(releaseSet));
mutate(drifted);
@@ -265,7 +335,7 @@ test('standalone inspection rejects image, family and self-digest drift', () =>
...identity,
releaseScope: 'cluster',
}),
/standalone release set/,
/standalone release set|receipt digest/,
);
}
assert.throws(
@@ -283,7 +353,9 @@ test('standalone inspection rejects image, family and self-digest drift', () =>
test('CLI records, aggregates and audits exact no-replace files', (t) => {
const directory = temporaryDirectory(t);
const recordsDirectory = path.join(directory, 'records');
const evidenceDirectory = path.join(directory, 'evidence');
fs.mkdirSync(recordsDirectory);
fs.mkdirSync(evidenceDirectory);
const releaseCandidate = candidate('local');
const candidatePath = path.join(directory, 'candidate.json');
writeCanonical(candidatePath, releaseCandidate);
@@ -315,6 +387,7 @@ test('CLI records, aggregates and audits exact no-replace files', (t) => {
'--mode=aggregate',
...common,
`--records=${recordsDirectory}`,
`--evidence-receipts=${evidenceDirectory}`,
`--output=${setPath}`,
],
root,
@@ -326,6 +399,7 @@ test('CLI records, aggregates and audits exact no-replace files', (t) => {
'--mode=audit',
...common,
`--records=${recordsDirectory}`,
`--evidence-receipts=${evidenceDirectory}`,
`--report=${setPath}`,
],
root,
@@ -356,6 +430,7 @@ test('CLI records, aggregates and audits exact no-replace files', (t) => {
'--mode=aggregate',
...common,
`--records=${recordsDirectory}`,
`--evidence-receipts=${evidenceDirectory}`,
`--output=${setPath}`,
],
root,
@@ -368,7 +443,9 @@ test('CLI records, aggregates and audits exact no-replace files', (t) => {
test('CLI rejects extra records, symlinks and open argument shapes', (t) => {
const directory = temporaryDirectory(t);
const recordsDirectory = path.join(directory, 'records');
const evidenceDirectory = path.join(directory, 'evidence');
fs.mkdirSync(recordsDirectory);
fs.mkdirSync(evidenceDirectory);
const releaseCandidate = candidate('local');
const candidatePath = path.join(directory, 'candidate.json');
writeCanonical(candidatePath, releaseCandidate);
@@ -392,6 +469,7 @@ test('CLI rejects extra records, symlinks and open argument shapes', (t) => {
'--mode=aggregate',
...common,
`--records=${recordsDirectory}`,
`--evidence-receipts=${evidenceDirectory}`,
`--output=${path.join(directory, 'set.json')}`,
],
root,
@@ -409,6 +487,7 @@ test('CLI rejects extra records, symlinks and open argument shapes', (t) => {
'--mode=aggregate',
...common,
`--records=${recordsDirectory}`,
`--evidence-receipts=${evidenceDirectory}`,
`--output=${path.join(directory, 'set.json')}`,
],
root,