256 lines
7.7 KiB
TypeScript
256 lines
7.7 KiB
TypeScript
import { createHash, generateKeyPairSync, sign } from "node:crypto";
|
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
|
|
import { providerEvidenceSignaturePayload } from "./lib/provider-evidence.ts";
|
|
import { verifyPromotionInputs } from "./lib/promotion-verifier.ts";
|
|
import {
|
|
createReleaseCandidateManifest,
|
|
RELEASE_CANDIDATE_EVIDENCE_PATHS,
|
|
RELEASE_CANDIDATE_MANIFEST_PATH,
|
|
releaseCandidateManifestSchema,
|
|
} from "./lib/release-candidate.ts";
|
|
|
|
const fixtureRoot = await mkdtemp(
|
|
path.join(tmpdir(), "supply-chain-provider-fixture-"),
|
|
);
|
|
try {
|
|
const repositoryRoot = process.cwd();
|
|
const actualCandidate = releaseCandidateManifestSchema.parse(
|
|
JSON.parse(
|
|
await readFile(
|
|
path.join(repositoryRoot, RELEASE_CANDIDATE_MANIFEST_PATH),
|
|
"utf8",
|
|
),
|
|
) as unknown,
|
|
);
|
|
const actualProviderEnvironment = absoluteProviderEnvironment(
|
|
fixtureRoot,
|
|
await writeProviderEnvironment(
|
|
fixtureRoot,
|
|
"actual",
|
|
actualCandidate.distSha256,
|
|
actualCandidate.lockfileSha256,
|
|
),
|
|
);
|
|
const actualDefaultVerifier = await verifyPromotionInputs({
|
|
environment: actualProviderEnvironment,
|
|
});
|
|
|
|
const rawLockfile = "lockfileVersion: '9.0'\n";
|
|
const lockfileSha256 = createHash("sha256")
|
|
.update(rawLockfile)
|
|
.digest("hex");
|
|
await mkdir(path.join(fixtureRoot, "dist"), { recursive: true });
|
|
await writeFile(path.join(fixtureRoot, "dist/app.js"), "immutable\n");
|
|
await writeFile(path.join(fixtureRoot, "pnpm-lock.yaml"), rawLockfile);
|
|
for (const file of RELEASE_CANDIDATE_EVIDENCE_PATHS) {
|
|
if (file === "pnpm-lock.yaml") continue;
|
|
await mkdir(path.dirname(path.join(fixtureRoot, file)), {
|
|
recursive: true,
|
|
});
|
|
const value =
|
|
file === "artifacts/release/dependency-inventory.json"
|
|
? { lockfileSha256 }
|
|
: { fixture: file };
|
|
await writeFile(
|
|
path.join(fixtureRoot, file),
|
|
`${JSON.stringify(value)}\n`,
|
|
);
|
|
}
|
|
const candidate = await createReleaseCandidateManifest(fixtureRoot);
|
|
await writeFile(
|
|
path.join(fixtureRoot, RELEASE_CANDIDATE_MANIFEST_PATH),
|
|
`${JSON.stringify(candidate)}\n`,
|
|
);
|
|
|
|
const validEnvironment = await writeProviderEnvironment(
|
|
fixtureRoot,
|
|
"valid",
|
|
candidate.distSha256,
|
|
candidate.lockfileSha256,
|
|
);
|
|
const wrongEnvironment = await writeProviderEnvironment(
|
|
fixtureRoot,
|
|
"wrong",
|
|
"3".repeat(64),
|
|
candidate.lockfileSha256,
|
|
);
|
|
const acceptLocalEvidence = async () => ({
|
|
status: "PASS" as const,
|
|
failures: [] as const,
|
|
});
|
|
const fixtures = {
|
|
absent: await verifyPromotionInputs({
|
|
repositoryRoot: fixtureRoot,
|
|
environment: {},
|
|
verifyLocalEvidence: acceptLocalEvidence,
|
|
}),
|
|
validImmutable: await verifyPromotionInputs({
|
|
repositoryRoot: fixtureRoot,
|
|
environment: validEnvironment,
|
|
verifyLocalEvidence: acceptLocalEvidence,
|
|
}),
|
|
wrongDigest: await verifyPromotionInputs({
|
|
repositoryRoot: fixtureRoot,
|
|
environment: wrongEnvironment,
|
|
verifyLocalEvidence: acceptLocalEvidence,
|
|
}),
|
|
postAttestationMutation: null as Awaited<
|
|
ReturnType<typeof verifyPromotionInputs>
|
|
> | null,
|
|
};
|
|
await writeFile(path.join(fixtureRoot, "dist/app.js"), "mutated\n");
|
|
fixtures.postAttestationMutation = await verifyPromotionInputs({
|
|
repositoryRoot: fixtureRoot,
|
|
environment: validEnvironment,
|
|
verifyLocalEvidence: acceptLocalEvidence,
|
|
});
|
|
|
|
const passed =
|
|
actualDefaultVerifier.status === "PASS" &&
|
|
fixtures.validImmutable.status === "PASS" &&
|
|
fixtures.absent.status === "FAIL_UNVERIFIED" &&
|
|
fixtures.wrongDigest.status === "FAIL_UNVERIFIED" &&
|
|
fixtures.postAttestationMutation.status === "FAIL_UNVERIFIED";
|
|
await mkdir("artifacts/security", { recursive: true });
|
|
await writeFile(
|
|
"artifacts/security/supply-chain-provider-fixtures.json",
|
|
`${JSON.stringify(
|
|
{
|
|
schemaVersion: 1,
|
|
actualDefaultVerifier: {
|
|
status: actualDefaultVerifier.status,
|
|
failures: actualDefaultVerifier.failures,
|
|
},
|
|
fixtures: Object.fromEntries(
|
|
Object.entries(fixtures).map(([name, result]) => [
|
|
name,
|
|
{ status: result?.status, failures: result?.failures },
|
|
]),
|
|
),
|
|
passingFixtureCount: Object.values(fixtures).filter(
|
|
(result) => result?.status === "PASS",
|
|
).length,
|
|
status: passed ? "PASS" : "FAIL",
|
|
},
|
|
null,
|
|
2,
|
|
)}\n`,
|
|
);
|
|
if (!passed) {
|
|
process.stderr.write(
|
|
"Supply-chain provider fixtures failed closed incorrectly\n",
|
|
);
|
|
process.exitCode = 1;
|
|
} else {
|
|
process.stdout.write(
|
|
"Supply-chain provider fixtures: actual default verifier and valid immutable fixture PASS\n",
|
|
);
|
|
}
|
|
} finally {
|
|
await rm(fixtureRoot, { recursive: true, force: true });
|
|
}
|
|
|
|
function absoluteProviderEnvironment(
|
|
repositoryRoot: string,
|
|
environment: NodeJS.ProcessEnv,
|
|
): NodeJS.ProcessEnv {
|
|
const absolute = { ...environment };
|
|
for (const key of [
|
|
"VULNERABILITY_REPORT_PATH",
|
|
"PROVENANCE_ATTESTATION_PATH",
|
|
"VULNERABILITY_PUBLIC_KEY_PATH",
|
|
"PROVENANCE_PUBLIC_KEY_PATH",
|
|
] as const) {
|
|
const value = absolute[key];
|
|
if (value) absolute[key] = path.join(repositoryRoot, value);
|
|
}
|
|
return absolute;
|
|
}
|
|
|
|
async function writeProviderEnvironment(
|
|
repositoryRoot: string,
|
|
name: string,
|
|
distDigest: string,
|
|
lockfileSha256: string,
|
|
): Promise<NodeJS.ProcessEnv> {
|
|
const vulnerabilityKeys = generateKeyPairSync("ed25519");
|
|
const provenanceKeys = generateKeyPairSync("ed25519");
|
|
const directory = `provider/${name}`;
|
|
await mkdir(path.join(repositoryRoot, directory), { recursive: true });
|
|
const vulnerability = signedEvidence(
|
|
{
|
|
schemaVersion: 1,
|
|
provider: "fixture-vulnerability-provider",
|
|
generatedAt: "2026-08-01T00:00:00.000Z",
|
|
scannedLockfileSha256: lockfileSha256,
|
|
scannedDistSha256: distDigest,
|
|
findings: [],
|
|
},
|
|
"fixture-vulnerability-key",
|
|
vulnerabilityKeys.privateKey,
|
|
);
|
|
const provenance = signedEvidence(
|
|
{
|
|
schemaVersion: 1,
|
|
provider: "fixture-provenance-provider",
|
|
signer: "fixture-workload-identity",
|
|
generatedAt: "2026-08-01T00:00:00.000Z",
|
|
subject: { name: "dist", digest: { sha256: distDigest } },
|
|
},
|
|
"fixture-provenance-key",
|
|
provenanceKeys.privateKey,
|
|
);
|
|
await Promise.all([
|
|
writeFile(
|
|
path.join(repositoryRoot, directory, "vulnerability.json"),
|
|
`${JSON.stringify(vulnerability)}\n`,
|
|
),
|
|
writeFile(
|
|
path.join(repositoryRoot, directory, "provenance.json"),
|
|
`${JSON.stringify(provenance)}\n`,
|
|
),
|
|
writeFile(
|
|
path.join(repositoryRoot, directory, "vulnerability.pem"),
|
|
vulnerabilityKeys.publicKey
|
|
.export({ type: "spki", format: "pem" })
|
|
.toString(),
|
|
),
|
|
writeFile(
|
|
path.join(repositoryRoot, directory, "provenance.pem"),
|
|
provenanceKeys.publicKey
|
|
.export({ type: "spki", format: "pem" })
|
|
.toString(),
|
|
),
|
|
]);
|
|
return {
|
|
VULNERABILITY_REPORT_PATH: `${directory}/vulnerability.json`,
|
|
PROVENANCE_ATTESTATION_PATH: `${directory}/provenance.json`,
|
|
VULNERABILITY_PUBLIC_KEY_PATH: `${directory}/vulnerability.pem`,
|
|
VULNERABILITY_KEY_ID: "fixture-vulnerability-key",
|
|
PROVENANCE_PUBLIC_KEY_PATH: `${directory}/provenance.pem`,
|
|
PROVENANCE_KEY_ID: "fixture-provenance-key",
|
|
};
|
|
}
|
|
|
|
function signedEvidence(
|
|
value: Record<string, unknown>,
|
|
keyId: string,
|
|
privateKey: ReturnType<typeof generateKeyPairSync>["privateKey"],
|
|
) {
|
|
return {
|
|
...value,
|
|
signature: {
|
|
algorithm: "Ed25519",
|
|
keyId,
|
|
value: sign(
|
|
null,
|
|
providerEvidenceSignaturePayload(value),
|
|
privateKey,
|
|
).toString("base64"),
|
|
},
|
|
};
|
|
}
|