Files
clean-architecture-frontend…/tests/unit/security-followup.test.ts
T

864 lines
30 KiB
TypeScript

import {
createHash,
generateKeyPairSync,
sign,
type KeyObject,
} from "node:crypto";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { localEvidenceAssessmentArtifactSchema } from "../../scripts/contracts/release-artifacts.ts";
import { verifyArchivedLocalEvidence } from "../../scripts/lib/local-release-evidence.ts";
import {
evaluatePromotionEvidence,
providerEvidenceSignaturePayload,
providerPublicKeyFingerprint,
} from "../../scripts/lib/provider-evidence.ts";
import { readProviderTrust } from "../../scripts/lib/promotion-verifier.ts";
import { superviseProviderEvidence } from "../../scripts/lib/provider-supervisor.ts";
import {
LOCAL_EVIDENCE_ASSESSMENT_PATH,
distSha256,
type ReleaseCandidateManifest,
} from "../../scripts/lib/release-candidate.ts";
import { supplyChainDigest } from "../../scripts/lib/supply-chain.ts";
const digest = (value: string): string =>
createHash("sha256").update(value).digest("hex");
const digestBytes = (value: Buffer): string =>
createHash("sha256").update(value).digest("hex");
function passingAssessment(): any {
return {
schemaVersion: 1 as const,
artifactType: "local-evidence-assessment" as const,
generatedAt: "2026-08-02T00:00:00.000Z",
status: "PASS" as const,
verifier: {
id: "clean-architecture-frontend-template/local-evidence-verifier",
version: "1",
sourceSha256: digest("verifier source"),
},
source: {
revision: "a".repeat(40),
sourceSetSha256: digest("source set"),
},
candidate: {
distSha256: digest("dist"),
lockfileSha256: digest("lockfile"),
sbomSha256: digest("sbom"),
},
policyInputs: [
{
path: "config/security/dependency-policy.json",
bytes: 3,
sha256: digest("{}\n"),
},
],
evidenceInputs: [
{ path: "pnpm-lock.yaml", bytes: 9, sha256: digest("lockfile\n") },
],
checks: {
release: "PASS" as const,
supplyChain: "PASS" as const,
dependencyPolicy: "PASS" as const,
licensePolicy: "PASS" as const,
vulnerabilityPolicy: "PASS" as const,
secretScan: "PASS" as const,
},
failures: [] as string[],
};
}
describe("security follow-up contracts", () => {
it("rejects a PASS local assessment with a failed check or failure diagnostic", () => {
const failedCheck = passingAssessment();
failedCheck.checks.secretScan = "FAIL";
const failureDiagnostic = passingAssessment();
failureDiagnostic.failures.push("secret scan failed");
expect(localEvidenceAssessmentArtifactSchema.safeParse(failedCheck).success).toBe(false);
expect(localEvidenceAssessmentArtifactSchema.safeParse(failureDiagnostic).success).toBe(false);
expect(localEvidenceAssessmentArtifactSchema.parse(passingAssessment()).status).toBe("PASS");
});
it("passes archived verification from extracted members without checkout source or policy paths", async () => {
const fixture = await createArchivedAssessmentFixture();
try {
const result = await verifyArchivedLocalEvidence({
extractionRoot: fixture.root,
expectedManifest: fixture.manifest,
});
expect(result).toEqual({
status: "PASS",
identity: {
sourceRevision: "a".repeat(40),
sourceSetSha256: digest("source set"),
assessmentSha256: fixture.assessmentSha256,
},
failures: [],
});
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});
it("rejects archived verification when the assessment is absent", async () => {
const fixture = await createArchivedAssessmentFixture();
try {
await rm(path.join(fixture.root, LOCAL_EVIDENCE_ASSESSMENT_PATH));
const result = await verifyArchivedLocalEvidence({
extractionRoot: fixture.root,
expectedManifest: fixture.manifest,
});
expect(result.status).toBe("FAIL");
expect(result.failures).toContain("local evidence assessment is missing or invalid");
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});
it("rejects digest-bound raw identity evidence that is not valid under its strict producer schema", async () => {
const fixture = await createArchivedAssessmentFixture();
try {
const provenancePath = "artifacts/release/provenance.json";
const malformed = Buffer.from(
`${JSON.stringify({ predicate: { materials: { sourceSetSha256: digest("source set") } } })}\n`,
);
await writeFile(path.join(fixture.root, provenancePath), malformed);
const assessmentPath = path.join(fixture.root, LOCAL_EVIDENCE_ASSESSMENT_PATH);
const assessment = localEvidenceAssessmentArtifactSchema.parse(
JSON.parse(await readFile(assessmentPath, "utf8")) as unknown,
);
assessment.evidenceInputs = assessment.evidenceInputs.map((row) =>
row.path === provenancePath
? { path: provenancePath, bytes: malformed.byteLength, sha256: digestBytes(malformed) }
: row,
);
const assessmentBytes = Buffer.from(`${JSON.stringify(assessment)}\n`);
await writeFile(assessmentPath, assessmentBytes);
const files = fixture.manifest.files.map((row) => {
if (row.path === provenancePath) {
return { path: provenancePath, bytes: malformed.byteLength, sha256: digestBytes(malformed) };
}
if (row.path === LOCAL_EVIDENCE_ASSESSMENT_PATH) {
return {
path: LOCAL_EVIDENCE_ASSESSMENT_PATH,
bytes: assessmentBytes.byteLength,
sha256: digestBytes(assessmentBytes),
};
}
return row;
});
const manifest = { ...fixture.manifest, files, bundleSha256: supplyChainDigest(files) };
await writeFile(
path.join(fixture.root, "artifacts/release/release-candidate.json"),
`${JSON.stringify(manifest)}\n`,
);
const result = await verifyArchivedLocalEvidence({
extractionRoot: fixture.root,
expectedManifest: manifest,
});
expect(result.status).toBe("FAIL");
expect(result.failures).toContain(
"archived source/build/provenance identities are missing or invalid",
);
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});
it("accepts signed provider v2 evidence only for the exact run, source, archive, and nonce", () => {
const now = Date.parse("2026-08-02T01:00:00.000Z");
const vulnerabilityKeys = generateKeyPairSync("ed25519");
const provenanceKeys = generateKeyPairSync("ed25519");
const expected = providerExpectedContext();
const vulnerability = signedProviderV2(
{
...expected,
schemaVersion: 2,
evidenceType: "vulnerability-report",
provider: "fixture-vulnerability",
issuedAt: "2026-08-02T01:00:00.000Z",
expiresAt: "2026-08-02T02:00:00.000Z",
run: { ...expected.run, invocationNonce: "1".repeat(64) },
findings: [],
},
"vulnerability-key",
vulnerabilityKeys.publicKey,
vulnerabilityKeys.privateKey,
);
const provenance = signedProviderV2(
{
...expected,
schemaVersion: 2,
evidenceType: "provenance-attestation",
provider: "fixture-provenance",
signer: "fixture-workload",
issuedAt: "2026-08-02T01:00:00.000Z",
expiresAt: "2026-08-02T02:00:00.000Z",
run: { ...expected.run, invocationNonce: "2".repeat(64) },
subject: { name: "dist", digest: { sha256: expected.candidate.distSha256 } },
},
"provenance-key",
provenanceKeys.publicKey,
provenanceKeys.privateKey,
);
const result = evaluatePromotionEvidence({
expected: {
...expected,
vulnerabilityInvocationNonce: "1".repeat(64),
provenanceInvocationNonce: "2".repeat(64),
},
localStatus: "PASS",
vulnerabilityReport: vulnerability,
provenanceAttestation: provenance,
vulnerabilityTrust: trust("vulnerability-key", vulnerabilityKeys.publicKey),
provenanceTrust: trust("provenance-key", provenanceKeys.publicKey),
nowEpochMs: () => now,
});
expect(result).toEqual({
status: "PASS",
vulnerabilityStatus: "PASS",
provenanceAttestationStatus: "PASS",
failures: [],
});
const replayed = evaluatePromotionEvidence({
expected: {
...expected,
run: { id: expected.run.id, attempt: 2 },
vulnerabilityInvocationNonce: "1".repeat(64),
provenanceInvocationNonce: "2".repeat(64),
},
localStatus: "PASS",
vulnerabilityReport: vulnerability,
provenanceAttestation: provenance,
vulnerabilityTrust: trust("vulnerability-key", vulnerabilityKeys.publicKey),
provenanceTrust: trust("provenance-key", provenanceKeys.publicKey),
nowEpochMs: () => now,
});
expect(replayed.status).toBe("FAIL_UNVERIFIED");
expect(replayed.failures).toEqual(
expect.arrayContaining([
"vulnerability report run identity mismatch",
"provenance attestation run identity mismatch",
]),
);
});
it.each(["vulnerability", "provenance"] as const)(
"rejects correctly re-signed %s v2 context/time/replay drift",
(kind) => {
const now = Date.parse("2026-08-02T01:00:00.000Z");
const vulnerabilityKeys = generateKeyPairSync("ed25519");
const provenanceKeys = generateKeyPairSync("ed25519");
const expected = providerExpectedContext();
const baseVulnerability = providerUnsigned("vulnerability", expected);
const baseProvenance = providerUnsigned("provenance", expected);
const validVulnerability = signedProviderV2(
baseVulnerability,
"vulnerability-key",
vulnerabilityKeys.publicKey,
vulnerabilityKeys.privateKey,
);
const validProvenance = signedProviderV2(
baseProvenance,
"provenance-key",
provenanceKeys.publicKey,
provenanceKeys.privateKey,
);
const rawCases: Array<readonly [
string,
(value: Record<string, any>) => Record<string, any>,
RegExp,
]> = [
["schema v1", (value) => ({ ...value, schemaVersion: 1 }), /missing or invalid/u],
[
"evidence type",
(value) => ({
...value,
evidenceType:
kind === "vulnerability"
? "provenance-attestation"
: "vulnerability-report",
}),
/missing or invalid/u,
],
...(["archiveSha256", "bundleSha256", "distSha256", "lockfileSha256"] as const).map(
(field) => [
`candidate ${field}`,
(value: Record<string, any>) => ({
...value,
candidate: { ...value.candidate, [field]: "f".repeat(64) },
...(kind === "provenance" && field === "distSha256"
? {
subject: {
name: "dist",
digest: { sha256: "f".repeat(64) },
},
}
: {}),
}),
/candidate identity|subject dist/u,
] as const,
),
[
"different archive with same dist and lockfile",
(value) => ({
...value,
candidate: { ...value.candidate, archiveSha256: "e".repeat(64) },
}),
/candidate identity/u,
],
[
"source revision",
(value) => ({ ...value, source: { ...value.source, revision: "c".repeat(40) } }),
/source identity/u,
],
[
"source set",
(value) => ({ ...value, source: { ...value.source, sourceSetSha256: "c".repeat(64) } }),
/source identity/u,
],
[
"run id",
(value) => ({ ...value, run: { ...value.run, id: "other-run" } }),
/run identity/u,
],
[
"run attempt replay",
(value) => ({ ...value, run: { ...value.run, attempt: 2 } }),
/run identity/u,
],
[
"different nonce",
(value) => ({ ...value, run: { ...value.run, invocationNonce: "3".repeat(64) } }),
/invocation nonce/u,
],
[
"missing nonce",
(value) => {
const run = { ...value.run };
delete run.invocationNonce;
return { ...value, run };
},
/missing or invalid/u,
],
[
"uppercase nonce",
(value) => ({ ...value, run: { ...value.run, invocationNonce: "A".repeat(64) } }),
/missing or invalid/u,
],
[
"short nonce",
(value) => ({ ...value, run: { ...value.run, invocationNonce: "1".repeat(62) } }),
/missing or invalid/u,
],
[
"issued future boundary",
(value) => ({ ...value, issuedAt: "2026-08-02T01:05:00.001Z" }),
/future skew/u,
],
[
"expiry equality",
(value) => ({ ...value, expiresAt: "2026-08-02T01:00:00.000Z" }),
/expired/u,
],
[
"expiry past",
(value) => ({ ...value, expiresAt: "2026-08-02T00:59:59.999Z" }),
/expired/u,
],
[
"zero lifetime",
(value) => ({
...value,
issuedAt: "2026-08-02T01:01:00.000Z",
expiresAt: "2026-08-02T01:01:00.000Z",
}),
/not positive/u,
],
[
"negative lifetime",
(value) => ({
...value,
issuedAt: "2026-08-02T01:02:00.000Z",
expiresAt: "2026-08-02T01:01:59.999Z",
}),
/not positive/u,
],
[
"lifetime above two hours",
(value) => ({
...value,
issuedAt: "2026-08-02T01:00:00.000Z",
expiresAt: "2026-08-02T03:00:00.001Z",
}),
/exceeds two hours/u,
],
[
"wrong fingerprint",
(value) => ({
...value,
signature: {
...value.signature,
publicKeyFingerprint: `sha256:${"d".repeat(64)}`,
},
}),
/trust identity/u,
],
];
const cases = rawCases.map(([name, mutate, failure]) => ({
name,
mutate,
failure,
}));
for (const testCase of cases) {
const base = kind === "vulnerability" ? baseVulnerability : baseProvenance;
const mutated = testCase.mutate(structuredClone(base));
const resigned = signedProviderV2(
mutated,
kind === "vulnerability" ? "vulnerability-key" : "provenance-key",
kind === "vulnerability" ? vulnerabilityKeys.publicKey : provenanceKeys.publicKey,
kind === "vulnerability" ? vulnerabilityKeys.privateKey : provenanceKeys.privateKey,
"signature" in mutated && mutated.signature?.publicKeyFingerprint
? mutated.signature.publicKeyFingerprint
: undefined,
);
const result = evaluatePromotionEvidence({
expected: {
...expected,
vulnerabilityInvocationNonce: "1".repeat(64),
provenanceInvocationNonce: "2".repeat(64),
},
localStatus: "PASS",
vulnerabilityReport:
kind === "vulnerability" ? resigned : validVulnerability,
provenanceAttestation:
kind === "provenance" ? resigned : validProvenance,
vulnerabilityTrust: trust("vulnerability-key", vulnerabilityKeys.publicKey),
provenanceTrust: trust("provenance-key", provenanceKeys.publicKey),
nowEpochMs: () => now,
});
expect(result.status, testCase.name).toBe("FAIL_UNVERIFIED");
expect(result.failures.join("\n"), testCase.name).toMatch(testCase.failure);
}
},
);
it("canonicalizes provider fingerprints from DER SPKI across PEM wrapping and rejects Ed448", async () => {
const root = await mkdtemp(path.join(tmpdir(), "provider-fingerprint-"));
try {
const ed25519 = generateKeyPairSync("ed25519").publicKey;
const pem = ed25519.export({ type: "spki", format: "pem" }).toString();
const body = pem.replace(/-----[^-]+-----|\s/gu, "");
const wrapped = (width: number) =>
`-----BEGIN PUBLIC KEY-----\n${body.match(new RegExp(`.{1,${width}}`, "gu"))!.join("\n")}\n-----END PUBLIC KEY-----\n`;
await writeFile(path.join(root, "a.pem"), wrapped(64));
await writeFile(path.join(root, "b.pem"), wrapped(32));
const first = await readProviderTrust(root, "a.pem", "fixture-key");
const second = await readProviderTrust(root, "b.pem", "fixture-key");
expect(first?.publicKeyFingerprint).toBe(providerPublicKeyFingerprint(ed25519));
expect(second?.publicKeyFingerprint).toBe(first?.publicKeyFingerprint);
const ed448 = generateKeyPairSync("ed448").publicKey;
await writeFile(root + "/ed448.pem", ed448.export({ type: "spki", format: "pem" }));
await expect(readProviderTrust(root, "ed448.pem", "fixture-key")).resolves.toBeNull();
expect(() => providerPublicKeyFingerprint(ed448)).toThrow(/must be Ed25519/u);
} finally {
await rm(root, { recursive: true, force: true });
}
});
it("captures the downloaded archive pathname exactly once in the provider supervisor", async () => {
const keys = generateKeyPairSync("ed25519");
const expected = providerExpectedContext();
let captureCount = 0;
let receivedEnvironment: Readonly<Record<string, string>> | undefined;
const manifest: ReleaseCandidateManifest = {
schemaVersion: 1,
distSha256: expected.candidate.distSha256,
lockfileSha256: expected.candidate.lockfileSha256,
bundleSha256: expected.candidate.bundleSha256,
files: [{ path: "pnpm-lock.yaml", bytes: 1, sha256: expected.candidate.lockfileSha256 }],
};
const result = await superviseProviderEvidence(
{
kind: "vulnerability",
archivePath: "/downloads/candidate.tar.gz",
expectedArchiveSha256: expected.candidate.archiveSha256,
expectedRun: {
id: expected.run.id,
attempt: expected.run.attempt,
sourceRevision: expected.source.revision,
},
trust: trust("vulnerability-key", keys.publicKey),
executeProvider: async ({ environment }) => {
receivedEnvironment = environment;
},
captureReport: async () => Buffer.from("{}\n"),
},
{
captureArchive: async (input) => {
captureCount += 1;
expect(input).toEqual({
archivePath: "/downloads/candidate.tar.gz",
expectedSha256: expected.candidate.archiveSha256,
});
return {
bytes: Buffer.from("captured archive"),
archiveSha256: expected.candidate.archiveSha256,
};
},
withVerifiedCandidate: (async (input: any) =>
input.verify({ extractionRoot: "/captured/extraction", manifest })) as any,
verifyLocalEvidence: async () => ({
status: "PASS",
identity: {
sourceRevision: expected.source.revision,
sourceSetSha256: expected.source.sourceSetSha256,
assessmentSha256: digest("assessment"),
},
failures: [],
}),
validateUpload: (async (input: any) => {
expect("archivePath" in input).toBe(false);
return { sealed: true };
}) as any,
randomBytes: () => Buffer.alloc(32, 0x11),
nowEpochMs: () => Date.parse("2026-08-02T01:00:00.000Z"),
},
);
expect(captureCount).toBe(1);
expect(receivedEnvironment).toEqual(
expect.objectContaining({
PROVIDER_EVIDENCE_SCHEMA_VERSION: "2",
PROVIDER_INVOCATION_NONCE: "11".repeat(32),
PROVIDER_ISSUED_AT: "2026-08-02T01:00:00.000Z",
PROVIDER_EXPIRES_AT: "2026-08-02T02:00:00.000Z",
CI_RUN_ID: expected.run.id,
CI_RUN_ATTEMPT: "1",
SOURCE_REVISION: expected.source.revision,
CANDIDATE_ARCHIVE_SHA256: expected.candidate.archiveSha256,
}),
);
expect(result.evidence).toEqual({ sealed: true });
});
});
function providerExpectedContext() {
return {
run: { id: "run-42", attempt: 1 },
source: { revision: "b".repeat(40), sourceSetSha256: digest("provider source") },
candidate: {
archiveSha256: digest("archive"),
bundleSha256: digest("bundle"),
distSha256: digest("provider dist"),
lockfileSha256: digest("provider lockfile"),
},
} as const;
}
function fingerprint(publicKey: KeyObject): string {
return `sha256:${createHash("sha256")
.update(publicKey.export({ type: "spki", format: "der" }))
.digest("hex")}`;
}
function trust(keyId: string, publicKey: KeyObject) {
return { keyId, publicKey, publicKeyFingerprint: fingerprint(publicKey) };
}
function signedProviderV2(
unsigned: Record<string, unknown>,
keyId: string,
publicKey: KeyObject,
privateKey: KeyObject,
fingerprintOverride?: string,
) {
const { signature: existingSignature, ...payload } = unsigned;
const value = {
...payload,
signature: {
algorithm: "Ed25519" as const,
keyId,
publicKeyFingerprint:
fingerprintOverride ??
(existingSignature && typeof existingSignature === "object" &&
"publicKeyFingerprint" in existingSignature
? String(existingSignature.publicKeyFingerprint)
: fingerprint(publicKey)),
value: "",
},
};
value.signature.value = sign(
null,
providerEvidenceSignaturePayload(value),
privateKey,
).toString("base64");
return value;
}
function providerUnsigned(
kind: "vulnerability" | "provenance",
expected: ReturnType<typeof providerExpectedContext>,
): Record<string, any> {
const common = {
...expected,
schemaVersion: 2,
evidenceType:
kind === "vulnerability"
? "vulnerability-report"
: "provenance-attestation",
provider: `fixture-${kind}`,
issuedAt: "2026-08-02T01:00:00.000Z",
expiresAt: "2026-08-02T02:00:00.000Z",
run: {
...expected.run,
invocationNonce: kind === "vulnerability" ? "1".repeat(64) : "2".repeat(64),
},
};
return kind === "vulnerability"
? { ...common, findings: [] }
: {
...common,
signer: "fixture-workload",
subject: {
name: "dist",
digest: { sha256: expected.candidate.distSha256 },
},
};
}
async function createArchivedAssessmentFixture(): Promise<{
root: string;
manifest: ReleaseCandidateManifest;
assessmentSha256: string;
}> {
const root = await mkdtemp(path.join(tmpdir(), "archived-assessment-"));
const sourceRevision = "a".repeat(40);
const sourceSetSha256 = digest("source set");
const releaseManifestBytes = Buffer.from(
`${JSON.stringify({
schemaVersion: 1,
appVersion: "1.0.0",
buildId: "build-1",
commitSha: sourceRevision,
configSchemaVersion: "1",
apiContractVersion: "1",
assetManifestHash: digest("vite manifest"),
releaseId: "release-1",
builtAt: "2026-08-02T00:00:00.000Z",
routeChunks: { home: "assets/home.js" },
})}\n`,
);
const distInputs = [
{ path: "dist/app.js", bytes: Buffer.byteLength("app\n"), sha256: digest("app\n"), gzipBytes: 0 },
{
path: "dist/release-manifest.json",
bytes: releaseManifestBytes.byteLength,
sha256: digestBytes(releaseManifestBytes),
gzipBytes: 0,
},
];
const candidateDist = distSha256(distInputs);
const sbomBytes = Buffer.from(
`${JSON.stringify({
bomFormat: "CycloneDX",
specVersion: "1.6",
serialNumber: "urn:uuid:00000000-0000-4000-8000-000000000001",
version: 1,
metadata: {
component: { type: "application", name: "fixture", version: "1.0.0" },
properties: [],
},
components: [],
dependencies: [],
})}\n`,
);
const sbomSha256 = digestBytes(sbomBytes);
const lockfileBytes = Buffer.from("lockfile\n");
const lockfileDigest = digestBytes(lockfileBytes);
const buildManifest = {
schemaVersion: 1,
buildId: "build-1",
commitSha: sourceRevision,
releaseId: "release-1",
moduleInventoryHash: digest("module inventory"),
generatedAt: "2026-08-02T00:00:00.000Z",
buildContext: {
nodeVersion: "v24.0.0",
packageManagerVersion: "11.0.0",
runnerImage: "linux-x64",
sourceDateEpoch: "1785638400",
},
outputs: {
directory: "dist",
viteManifest: "dist/.vite/manifest.json",
moduleInventory: "artifacts/quality/vite-module-inventory.json",
routeChunks: { home: "assets/home.js" },
runtimeConfigSchema: "dist/runtime-config.schema.json",
},
};
const provenance = {
_type: "https://in-toto.io/Statement/v1",
subject: [{ name: "dist", digest: { sha256: candidateDist } }],
predicateType: "https://slsa.dev/provenance/v1",
predicate: {
buildDefinition: {
buildType: "https://vite.dev/build/v1",
externalParameters: {},
internalParameters: {},
resolvedDependencies: [
{ uri: "pnpm-lock.yaml", digest: { sha256: lockfileDigest } },
],
},
runDetails: {
builder: { id: "fixture-builder" },
metadata: { invocationId: "LOCAL_UNSIGNED" },
},
materials: { lockfileSha256: lockfileDigest, sourceSetSha256, sbomSha256 },
},
};
const supplyVerification = {
schemaVersion: 1,
localStatus: "PASS",
promotionStatus: "FAIL_UNVERIFIED",
lockfileSha256: lockfileDigest,
sourceSetSha256,
distSha256: candidateDist,
sbomSha256,
dependencyDiff: { added: [], removed: [], changed: [], upgrades: [] },
highRiskReview: [],
vulnerabilityStatus: "FAIL_UNVERIFIED",
provenanceAttestationStatus: "FAIL_UNVERIFIED",
failures: [],
};
const members = new Map<string, Buffer>([
["dist/app.js", Buffer.from("app\n")],
["dist/release-manifest.json", releaseManifestBytes],
["pnpm-lock.yaml", lockfileBytes],
["artifacts/release/build-manifest.json", Buffer.from(`${JSON.stringify(buildManifest)}\n`)],
["artifacts/release/provenance.json", Buffer.from(`${JSON.stringify(provenance)}\n`)],
[
"artifacts/security/supply-chain-verification.json",
Buffer.from(`${JSON.stringify(supplyVerification)}\n`),
],
["artifacts/release/sbom.cdx.json", sbomBytes],
]);
const evidenceInputs = [...members.entries()]
.map(([memberPath, bytes]) => ({
path: memberPath,
bytes: bytes.byteLength,
sha256: digestBytes(bytes),
}))
.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
const policyPaths = [
"config/security/dependency-baseline.approval.json",
"config/security/dependency-baseline.json",
"config/security/dependency-change-evidence.json",
"config/security/dependency-policy.json",
"config/security/secret-scan-policy.json",
"config/security/vulnerability-exceptions.json",
"config/security/vulnerability-policy.json",
"schemas/artifacts/build-manifest.schema.json",
"schemas/artifacts/dependency-inventory.schema.json",
"schemas/artifacts/supply-chain-verification.schema.json",
"scripts/contracts/release-artifacts.ts",
"scripts/create-release-candidate.ts",
"scripts/generate-supply-chain.ts",
"scripts/lib/build-manifest-outputs.ts",
"scripts/lib/json-schema.ts",
"scripts/lib/local-policy-evidence.ts",
"scripts/lib/local-release-evidence.ts",
"scripts/lib/release-candidate.ts",
"scripts/lib/release-input-evidence.ts",
"scripts/lib/release-runtime-coherence.ts",
"scripts/lib/repository-file-inventory.ts",
"scripts/lib/secret-scan-evaluator.ts",
"scripts/lib/secret-scan-policy.ts",
"scripts/lib/supply-chain.ts",
"scripts/lib/validated-json-artifact.ts",
"src/contracts/release-artifacts.ts",
];
const sbomRow = evidenceInputs.find(
({ path: memberPath }) => memberPath === "artifacts/release/sbom.cdx.json",
)!;
const policyInputs = policyPaths.map((policyPath) => ({
path: policyPath,
bytes: 2,
sha256: digest(`policy:${policyPath}`),
}));
const verifierPaths = new Set([
"scripts/contracts/release-artifacts.ts",
"scripts/create-release-candidate.ts",
"scripts/generate-supply-chain.ts",
"scripts/lib/build-manifest-outputs.ts",
"scripts/lib/json-schema.ts",
"scripts/lib/local-policy-evidence.ts",
"scripts/lib/local-release-evidence.ts",
"scripts/lib/release-candidate.ts",
"scripts/lib/release-input-evidence.ts",
"scripts/lib/release-runtime-coherence.ts",
"scripts/lib/repository-file-inventory.ts",
"scripts/lib/secret-scan-evaluator.ts",
"scripts/lib/secret-scan-policy.ts",
"scripts/lib/supply-chain.ts",
"scripts/lib/validated-json-artifact.ts",
"src/contracts/release-artifacts.ts",
]);
const assessment = localEvidenceAssessmentArtifactSchema.parse({
...passingAssessment(),
verifier: {
id: "clean-architecture-frontend-template/local-evidence-verifier",
version: "1",
sourceSha256: supplyChainDigest(
policyInputs.filter(({ path: policyPath }) => verifierPaths.has(policyPath)),
),
},
source: { revision: sourceRevision, sourceSetSha256 },
candidate: {
distSha256: candidateDist,
lockfileSha256: evidenceInputs.find(({ path: memberPath }) => memberPath === "pnpm-lock.yaml")!
.sha256,
sbomSha256: sbomRow.sha256,
},
policyInputs,
evidenceInputs,
});
const assessmentBytes = Buffer.from(`${JSON.stringify(assessment)}\n`);
members.set(LOCAL_EVIDENCE_ASSESSMENT_PATH, assessmentBytes);
for (const [memberPath, bytes] of members) {
await mkdir(path.dirname(path.join(root, memberPath)), { recursive: true });
await writeFile(path.join(root, memberPath), bytes);
}
const files = [...members.entries()]
.map(([memberPath, bytes]) => ({
path: memberPath,
bytes: bytes.byteLength,
sha256: digestBytes(bytes),
}))
.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
const manifest: ReleaseCandidateManifest = {
schemaVersion: 1,
distSha256: assessment.candidate.distSha256,
lockfileSha256: assessment.candidate.lockfileSha256,
bundleSha256: supplyChainDigest(files),
files,
};
await mkdir(path.join(root, "artifacts/release"), { recursive: true });
await writeFile(
path.join(root, "artifacts/release/release-candidate.json"),
`${JSON.stringify(manifest)}\n`,
);
return { root, manifest, assessmentSha256: digestBytes(assessmentBytes) };
}