Files
clean-architecture-frontend…/scripts/check-supply-chain-provider-fixtures.ts
T

376 lines
13 KiB
TypeScript

import { spawnSync } from "node:child_process";
import {
createHash,
generateKeyPairSync,
sign,
type KeyObject,
} from "node:crypto";
import {
cp,
mkdir,
mkdtemp,
readFile,
readdir,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import {
localEvidenceAssessmentArtifactSchema,
supplyChainProviderFixturesArtifactSchema,
} from "./contracts/release-artifacts.ts";
import { PROMOTED_FILE_NAMES } from "./contracts/promotion-artifacts.ts";
import {
captureCiCandidateArchive,
} from "./lib/ci-candidate-archive.ts";
import {
cleanupFinalizedPromotion,
finalizeVerifiedPromotion,
} from "./lib/promotion-stager.ts";
import { verifyExactPromotionBundle } from "./lib/exact-promotion-bundle.ts";
import {
providerEvidenceSignaturePayload,
providerPublicKeyFingerprint,
} from "./lib/provider-evidence.ts";
import {
LOCAL_EVIDENCE_ASSESSMENT_PATH,
RELEASE_CANDIDATE_EVIDENCE_PATHS,
RELEASE_CANDIDATE_MANIFEST_PATH,
releaseCandidateManifestSchema,
} from "./lib/release-candidate.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
const repositoryRoot = process.cwd();
const fixtureRoot = await mkdtemp(path.join(tmpdir(), "provider-exact-five-fixture-"));
try {
await cp(repositoryRoot, fixtureRoot, {
recursive: true,
filter: (source) => {
const relative = path.relative(repositoryRoot, source);
if (!relative) return true;
const first = relative.split(path.sep)[0];
return ![".release", "artifacts", "dist", "node_modules"].includes(first ?? "");
},
});
await cp(path.join(repositoryRoot, "artifacts"), path.join(fixtureRoot, "artifacts"), {
recursive: true,
});
await rm(path.join(fixtureRoot, "artifacts/release"), { recursive: true, force: true });
await symlink(path.join(repositoryRoot, "node_modules"), path.join(fixtureRoot, "node_modules"), "dir");
const git = spawnSync("git", ["show", "-s", "--format=%H%n%ct", "HEAD"], {
cwd: repositoryRoot,
encoding: "utf8",
});
if (git.status !== 0) throw new Error(`provider fixture git identity failed: ${git.stderr}`);
const [revision, sourceDateEpoch] = git.stdout.trim().split(/\r?\n/u);
if (!revision || !sourceDateEpoch) throw new Error("provider fixture git identity is incomplete");
const build = spawnSync("corepack", ["pnpm", "build:release-candidate"], {
cwd: fixtureRoot,
encoding: "utf8",
timeout: 120_000,
maxBuffer: 32 * 1024 * 1024,
env: {
...process.env,
CI: "true",
VITE_BUILD_ID: "provider-exact-five-fixture",
VITE_COMMIT_SHA: revision,
RELEASE_ID: "provider-exact-five-fixture",
SOURCE_DATE_EPOCH: sourceDateEpoch,
CI_RUNNER_IMAGE: `fixture@sha256:${"a".repeat(64)}`,
},
});
if (build.status !== 0) throw new Error(`${build.stdout}\n${build.stderr}`);
const manifest = releaseCandidateManifestSchema.parse(
JSON.parse(
await readFile(path.join(fixtureRoot, RELEASE_CANDIDATE_MANIFEST_PATH), "utf8"),
) as unknown,
);
const assessment = localEvidenceAssessmentArtifactSchema.parse(
JSON.parse(
await readFile(path.join(fixtureRoot, LOCAL_EVIDENCE_ASSESSMENT_PATH), "utf8"),
) as unknown,
);
const archivePath = path.join(fixtureRoot, "candidate.tar.gz");
const tar = spawnSync(
"/usr/bin/tar",
[
"--sort=name",
"--mtime=@0",
"--owner=0",
"--group=0",
"--numeric-owner",
"-czf",
archivePath,
"dist",
...RELEASE_CANDIDATE_EVIDENCE_PATHS,
RELEASE_CANDIDATE_MANIFEST_PATH,
],
{ cwd: fixtureRoot, encoding: "utf8" },
);
if (tar.status !== 0) throw new Error(`provider fixture real tar failed: ${tar.stderr}`);
const archiveBytes = await readFile(archivePath);
const archiveSha256 = sha256(archiveBytes);
const now = Date.parse("2026-08-02T01:00:00.000Z");
const vulnerabilityKeys = generateKeyPairSync("ed25519");
const provenanceKeys = generateKeyPairSync("ed25519");
const vulnerabilityKeyId = "fixture-vulnerability-key";
const provenanceKeyId = "fixture-provenance-key";
const vulnerabilityNonce = "1".repeat(64);
const provenanceNonce = "2".repeat(64);
const context = {
run: { id: "fixture-run", attempt: 1 },
source: {
revision: assessment.source.revision,
sourceSetSha256: assessment.source.sourceSetSha256,
},
candidate: {
archiveSha256,
bundleSha256: manifest.bundleSha256,
distSha256: manifest.distSha256,
lockfileSha256: manifest.lockfileSha256,
},
} as const;
const vulnerability = signedEvidence(
{
schemaVersion: 2,
evidenceType: "vulnerability-report",
provider: "fixture-vulnerability-provider",
issuedAt: new Date(now).toISOString(),
expiresAt: new Date(now + 60 * 60 * 1_000).toISOString(),
run: { ...context.run, invocationNonce: vulnerabilityNonce },
source: context.source,
candidate: context.candidate,
secretScanAttestation: {
status: "PASS",
localEvidenceAssessmentSha256: sha256(
await readFile(path.join(fixtureRoot, LOCAL_EVIDENCE_ASSESSMENT_PATH)),
),
sourceSetSha256: assessment.source.sourceSetSha256,
policySha256: assessment.secretScan.policySha256,
sarifSha256: assessment.secretScan.sarifSha256,
scanInputSha256: assessment.secretScan.scanInputSha256,
},
findings: [],
},
vulnerabilityKeyId,
vulnerabilityKeys.publicKey,
vulnerabilityKeys.privateKey,
);
const provenance = signedEvidence(
{
schemaVersion: 2,
evidenceType: "provenance-attestation",
provider: "fixture-provenance-provider",
signer: "fixture-workload-identity",
issuedAt: new Date(now).toISOString(),
expiresAt: new Date(now + 60 * 60 * 1_000).toISOString(),
run: { ...context.run, invocationNonce: provenanceNonce },
source: context.source,
candidate: context.candidate,
subject: { name: "dist", digest: { sha256: manifest.distSha256 } },
},
provenanceKeyId,
provenanceKeys.publicKey,
provenanceKeys.privateKey,
);
await Promise.all([
write(fixtureRoot, "provider/vulnerability.json", `${JSON.stringify(vulnerability)}\n`),
write(fixtureRoot, "provider/provenance.json", `${JSON.stringify(provenance)}\n`),
write(
fixtureRoot,
"provider/vulnerability.pem",
vulnerabilityKeys.publicKey.export({ type: "spki", format: "pem" }),
),
write(
fixtureRoot,
"provider/provenance.pem",
provenanceKeys.publicKey.export({ type: "spki", format: "pem" }),
),
]);
const runnerTempRoot = path.join(fixtureRoot, "runner-temp");
await mkdir(runnerTempRoot, { mode: 0o700 });
const finalized = await finalizeVerifiedPromotion(
{
repositoryRoot: fixtureRoot,
archivePath,
expectedArchiveSha256: archiveSha256,
vulnerabilityReportPath: "provider/vulnerability.json",
provenanceAttestationPath: "provider/provenance.json",
vulnerabilityPublicKeyPath: "provider/vulnerability.pem",
vulnerabilityKeyId,
provenancePublicKeyPath: "provider/provenance.pem",
provenanceKeyId,
expectedRun: { id: context.run.id, attempt: 1, sourceRevision: revision },
vulnerabilityInvocationNonce: vulnerabilityNonce,
provenanceInvocationNonce: provenanceNonce,
runnerTempRoot,
},
{
nowEpochMs: () => now,
randomBytes: (bytes) => Buffer.alloc(bytes, 0x4a),
afterCapture: async () => {
await writeFile(path.join(fixtureRoot, "dist/index.html"), "contradictory external tree\n");
},
},
);
const validImmutable =
finalized.files.length === 5 &&
(await readdir(finalized.stagingRoot)).length === 5;
const exactFiles = Object.fromEntries(
await Promise.all(
PROMOTED_FILE_NAMES.map(async (name) => [
name,
await readFile(path.join(finalized.stagingRoot, name)),
] as const),
),
);
const bundleTrust = {
vulnerabilityTrust: {
keyId: vulnerabilityKeyId,
publicKey: vulnerabilityKeys.publicKey,
publicKeyFingerprint: providerPublicKeyFingerprint(vulnerabilityKeys.publicKey),
},
provenanceTrust: {
keyId: provenanceKeyId,
publicKey: provenanceKeys.publicKey,
publicKeyFingerprint: providerPublicKeyFingerprint(provenanceKeys.publicKey),
},
expected: {
run: context.run,
sourceRevision: context.source.revision,
sourceSetSha256: context.source.sourceSetSha256,
archiveSha256: context.candidate.archiveSha256,
bundleSha256: context.candidate.bundleSha256,
distSha256: context.candidate.distSha256,
lockfileSha256: context.candidate.lockfileSha256,
},
nowEpochMs: () => now,
} as const;
const absentFiles = { ...exactFiles } as Partial<typeof exactFiles>;
delete absentFiles["provider-verification.json"];
const absent = await captureRejection(async () => {
await verifyExactPromotionBundle(absentFiles, bundleTrust);
});
const invalidPath = path.join(fixtureRoot, "invalid.tar.gz");
const invalidBytes = Buffer.from("arbitrary non-tar bytes\n");
await writeFile(invalidPath, invalidBytes);
const invalidTar = await captureRejection(async () => {
await finalizeVerifiedPromotion(
{
repositoryRoot: fixtureRoot,
archivePath: invalidPath,
expectedArchiveSha256: sha256(invalidBytes),
vulnerabilityReportPath: "provider/vulnerability.json",
provenanceAttestationPath: "provider/provenance.json",
vulnerabilityPublicKeyPath: "provider/vulnerability.pem",
vulnerabilityKeyId,
provenancePublicKeyPath: "provider/provenance.pem",
provenanceKeyId,
expectedRun: { id: context.run.id, attempt: 1, sourceRevision: revision },
vulnerabilityInvocationNonce: vulnerabilityNonce,
provenanceInvocationNonce: provenanceNonce,
runnerTempRoot,
},
{ nowEpochMs: () => now },
);
});
const wrongDigest = await captureRejection(async () => {
await captureCiCandidateArchive({ archivePath, expectedSha256: "0".repeat(64) });
});
const passed =
validImmutable && absent.rejected && invalidTar.rejected && wrongDigest.rejected;
await cleanupFinalizedPromotion({
runnerTempRoot,
stagingRoot: finalized.stagingRoot,
cleanupToken: finalized.cleanupToken,
runnerTempIdentity: finalized.runnerTempIdentity,
stagingIdentity: finalized.stagingIdentity,
});
await mkdir(path.join(repositoryRoot, "artifacts/security"), { recursive: true });
await writeValidatedJsonArtifact({
path: path.join(repositoryRoot, "artifacts/security/supply-chain-provider-fixtures.json"),
schema: supplyChainProviderFixturesArtifactSchema,
value: {
schemaVersion: 1,
actualDefaultVerifier: {
status: validImmutable ? "PASS" : "FAIL_UNVERIFIED",
failures: validImmutable ? [] : ["canonical exact-five finalizer failed"],
},
fixtures: {
absent: {
status: absent.rejected ? "FAIL_UNVERIFIED" : "PASS",
failures: [absent.failure],
},
validImmutable: { status: validImmutable ? "PASS" : "FAIL_UNVERIFIED", failures: [] },
wrongDigest: {
status: wrongDigest.rejected ? "FAIL_UNVERIFIED" : "PASS",
failures: [wrongDigest.failure],
},
invalidTar: {
status: invalidTar.rejected ? "FAIL_UNVERIFIED" : "PASS",
failures: [invalidTar.failure],
},
},
externalTreeCanary: {
status: validImmutable ? "PASS" : "FAIL_UNVERIFIED",
failures: validImmutable ? [] : ["external tree canary influenced captured archive"],
},
passingFixtureCount: validImmutable ? 1 : 0,
status: passed ? "PASS" : "FAIL",
},
});
if (!passed) throw new Error("canonical exact-five provider fixtures failed closed incorrectly");
process.stdout.write("Supply-chain provider real-tar signed exact-five fixtures: PASS\n");
} finally {
await rm(fixtureRoot, { recursive: true, force: true });
}
function signedEvidence(
value: Record<string, unknown>,
keyId: string,
publicKey: KeyObject,
privateKey: KeyObject,
) {
return {
...value,
signature: {
algorithm: "Ed25519",
keyId,
publicKeyFingerprint: providerPublicKeyFingerprint(publicKey),
value: sign(
null,
providerEvidenceSignaturePayload(value),
privateKey,
).toString("base64"),
},
};
}
async function write(root: string, relative: string, value: string | Buffer): Promise<void> {
const absolute = path.join(root, relative);
await mkdir(path.dirname(absolute), { recursive: true });
await writeFile(absolute, value);
}
async function captureRejection(
operation: () => Promise<void>,
): Promise<Readonly<{ rejected: boolean; failure: string }>> {
try {
await operation();
return Object.freeze({ rejected: false, failure: "fixture unexpectedly passed" });
} catch (error) {
return Object.freeze({
rejected: true,
failure: error instanceof Error ? error.message : String(error),
});
}
}
function sha256(bytes: Buffer): string {
return createHash("sha256").update(bytes).digest("hex");
}