709 lines
26 KiB
TypeScript
709 lines
26 KiB
TypeScript
import { generateKeyPairSync } from "node:crypto";
|
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { EventEmitter } from "node:events";
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
evaluatePromotionEvidence,
|
|
providerPublicKeyFingerprint,
|
|
validateProviderEvidence,
|
|
} from "../../scripts/lib/provider-evidence.ts";
|
|
import { readProviderTrust } from "../../scripts/lib/provider-trust.ts";
|
|
import { superviseProviderEvidence } from "../../scripts/lib/provider-supervisor.ts";
|
|
import { runProviderProcess } from "../../scripts/lib/provider-process-runner.ts";
|
|
import { runStageVerifiedPromotionCli } from "../../scripts/lib/stage-verified-promotion-cli.ts";
|
|
import type { ReleaseCandidateManifest } from "../../scripts/lib/release-candidate.ts";
|
|
import {
|
|
digest,
|
|
providerExpectedContext,
|
|
providerUnsigned,
|
|
signedProviderV2,
|
|
trust,
|
|
} from "./security-followup-fixture.ts";
|
|
|
|
describe("security provider evidence contracts", () => {
|
|
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(
|
|
{
|
|
source: expected.source,
|
|
candidate: expected.candidate,
|
|
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) },
|
|
secretScanAttestation: expected.secretScanAttestation,
|
|
findings: [],
|
|
},
|
|
"vulnerability-key",
|
|
vulnerabilityKeys.publicKey,
|
|
vulnerabilityKeys.privateKey,
|
|
);
|
|
const provenance = signedProviderV2(
|
|
{
|
|
source: expected.source,
|
|
candidate: expected.candidate,
|
|
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("rejects a signed vulnerability PASS when the captured SARIF attestation differs", () => {
|
|
const keys = generateKeyPairSync("ed25519");
|
|
const expected = providerExpectedContext();
|
|
const secretScanAttestation = {
|
|
status: "PASS" as const,
|
|
localEvidenceAssessmentSha256: digest("assessment"),
|
|
sourceSetSha256: expected.source.sourceSetSha256,
|
|
policySha256: digest("secret policy"),
|
|
sarifSha256: digest("real sarif"),
|
|
scanInputSha256: digest("scan input"),
|
|
};
|
|
const report = signedProviderV2(
|
|
{
|
|
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) },
|
|
source: expected.source,
|
|
candidate: expected.candidate,
|
|
secretScanAttestation,
|
|
findings: [],
|
|
},
|
|
"vulnerability-key",
|
|
keys.publicKey,
|
|
keys.privateKey,
|
|
);
|
|
const validated = validateProviderEvidence({
|
|
kind: "vulnerability",
|
|
value: report,
|
|
expected: {
|
|
...expected,
|
|
vulnerabilityInvocationNonce: "1".repeat(64),
|
|
provenanceInvocationNonce: "2".repeat(64),
|
|
secretScanAttestation: {
|
|
...secretScanAttestation,
|
|
sarifSha256: digest("forged empty sarif"),
|
|
},
|
|
},
|
|
trust: trust("vulnerability-key", keys.publicKey),
|
|
nowEpochMs: () => Date.parse("2026-08-02T01:00:00.000Z"),
|
|
});
|
|
expect(validated.status).toBe("FAIL_UNVERIFIED");
|
|
expect(validated.failures).toContain(
|
|
"vulnerability report secret scan attestation mismatch",
|
|
);
|
|
const forged = structuredClone(report);
|
|
forged.secretScanAttestation.sarifSha256 = digest("forged empty sarif");
|
|
const forgedValidation = validateProviderEvidence({
|
|
kind: "vulnerability",
|
|
value: forged,
|
|
expected: {
|
|
...expected,
|
|
vulnerabilityInvocationNonce: "1".repeat(64),
|
|
provenanceInvocationNonce: "2".repeat(64),
|
|
secretScanAttestation: forged.secretScanAttestation,
|
|
},
|
|
trust: trust("vulnerability-key", keys.publicKey),
|
|
nowEpochMs: () => Date.parse("2026-08-02T01:00:00.000Z"),
|
|
});
|
|
expect(forgedValidation.failures).toContain(
|
|
"vulnerability report signature verification failed",
|
|
);
|
|
|
|
const provenanceKeys = generateKeyPairSync("ed25519");
|
|
const provenance = signedProviderV2(
|
|
{
|
|
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) },
|
|
source: expected.source,
|
|
candidate: expected.candidate,
|
|
subject: { name: "dist", digest: { sha256: expected.candidate.distSha256 } },
|
|
},
|
|
"provenance-key",
|
|
provenanceKeys.publicKey,
|
|
provenanceKeys.privateKey,
|
|
);
|
|
const evaluated = evaluatePromotionEvidence({
|
|
expected: {
|
|
...expected,
|
|
vulnerabilityInvocationNonce: "1".repeat(64),
|
|
provenanceInvocationNonce: "2".repeat(64),
|
|
secretScanAttestation: {
|
|
...secretScanAttestation,
|
|
sarifSha256: digest("forged empty sarif"),
|
|
},
|
|
},
|
|
localStatus: "PASS",
|
|
vulnerabilityReport: report,
|
|
provenanceAttestation: provenance,
|
|
vulnerabilityTrust: trust("vulnerability-key", keys.publicKey),
|
|
provenanceTrust: trust("provenance-key", provenanceKeys.publicKey),
|
|
nowEpochMs: () => Date.parse("2026-08-02T01:00:00.000Z"),
|
|
});
|
|
expect(evaluated.vulnerabilityStatus).toBe("FAIL_UNVERIFIED");
|
|
expect(evaluated.failures).toContain(
|
|
"vulnerability report secret scan attestation 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"),
|
|
secretScan: {
|
|
policySha256: digest("provider secret policy"),
|
|
sarifSha256: digest("provider secret sarif"),
|
|
scanInputSha256: digest("provider secret input"),
|
|
},
|
|
},
|
|
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 });
|
|
});
|
|
|
|
it("samples provider freshness after report capture instead of reusing issuance time", async () => {
|
|
const keys = generateKeyPairSync("ed25519");
|
|
const expected = providerExpectedContext();
|
|
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 issuedSample = Date.parse("2026-08-02T01:00:00.000Z");
|
|
const validationSample = Date.parse("2026-08-02T02:00:00.001Z");
|
|
const samples = [issuedSample, validationSample];
|
|
let issuedAt = "";
|
|
await expect(
|
|
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 }) => {
|
|
issuedAt = environment.PROVIDER_ISSUED_AT!;
|
|
},
|
|
captureReport: async () => Buffer.from("{}\n"),
|
|
},
|
|
{
|
|
captureArchive: async () => ({
|
|
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"),
|
|
secretScan: {
|
|
policySha256: digest("provider secret policy"),
|
|
sarifSha256: digest("provider secret sarif"),
|
|
scanInputSha256: digest("provider secret input"),
|
|
},
|
|
},
|
|
failures: [],
|
|
}),
|
|
validateUpload: (async (input: any) => {
|
|
expect(input.nowEpochMs()).toBe(validationSample);
|
|
throw new Error("provider report expired during execution");
|
|
}) as any,
|
|
randomBytes: () => Buffer.alloc(32, 0x33),
|
|
nowEpochMs: () => samples.shift()!,
|
|
},
|
|
),
|
|
).rejects.toThrow(/expired during execution/u);
|
|
expect(issuedAt).toBe("2026-08-02T01:00:00.000Z");
|
|
});
|
|
|
|
it("kills a timed-out provider but settles only after the child closes", async () => {
|
|
const child = new EventEmitter() as EventEmitter & {
|
|
kill(signal: NodeJS.Signals): boolean;
|
|
};
|
|
let killedWith: NodeJS.Signals | undefined;
|
|
child.kill = (signal) => {
|
|
killedWith = signal;
|
|
return true;
|
|
};
|
|
let fireTimeout: (() => void) | undefined;
|
|
let settled = false;
|
|
const running = runProviderProcess(
|
|
{ executable: "/usr/bin/bwrap", arguments: [], environment: {}, timeoutMs: 1 },
|
|
{
|
|
spawnChild: () => child as any,
|
|
setTimer: (callback) => {
|
|
fireTimeout = callback;
|
|
return 1 as any;
|
|
},
|
|
clearTimer: () => undefined,
|
|
},
|
|
).finally(() => {
|
|
settled = true;
|
|
});
|
|
fireTimeout?.();
|
|
await Promise.resolve();
|
|
expect(killedWith).toBe("SIGKILL");
|
|
expect(settled).toBe(false);
|
|
child.emit("close", null, "SIGKILL");
|
|
await expect(running).rejects.toThrow(/timed out/u);
|
|
expect(settled).toBe(true);
|
|
});
|
|
|
|
it("captures process-group kill errors, attempts child fallback, and settles after close", async () => {
|
|
const child = new EventEmitter() as EventEmitter & {
|
|
pid: number;
|
|
kill(signal: NodeJS.Signals): boolean;
|
|
};
|
|
child.pid = 12_346;
|
|
let fallbackSignal: NodeJS.Signals | undefined;
|
|
child.kill = (signal) => {
|
|
fallbackSignal = signal;
|
|
return true;
|
|
};
|
|
let fireTimeout: (() => void) | undefined;
|
|
const running = runProviderProcess(
|
|
{ executable: "/usr/bin/bwrap", arguments: [], environment: {}, timeoutMs: 1 },
|
|
{
|
|
spawnChild: () => child as any,
|
|
setTimer: (callback) => {
|
|
fireTimeout = callback;
|
|
return 1 as any;
|
|
},
|
|
clearTimer: () => undefined,
|
|
killProcessGroup: () => {
|
|
throw Object.assign(new Error("group kill denied"), { code: "EPERM" });
|
|
},
|
|
},
|
|
);
|
|
expect(() => fireTimeout?.()).not.toThrow();
|
|
expect(fallbackSignal).toBe("SIGKILL");
|
|
child.emit("close", null, "SIGKILL");
|
|
await expect(running).rejects.toThrow(/timed out.*kill failed.*close/u);
|
|
});
|
|
|
|
it.each(["open failure", "partial write failure"])(
|
|
"cleans finalized staging from memory when GITHUB_OUTPUT has a %s",
|
|
async (failureKind) => {
|
|
const finalized = {
|
|
stagingRoot: "/runner/promotion-run-1-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
cleanupToken: "promotion-run-1-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
runnerTempIdentity: { dev: 10, ino: 20 },
|
|
stagingIdentity: { dev: 30, ino: 40 },
|
|
files: [],
|
|
} as const;
|
|
let cleanupInput: unknown;
|
|
let appendCalls = 0;
|
|
const environment = {
|
|
CANDIDATE_ARCHIVE_PATH: "candidate.tar.gz",
|
|
CANDIDATE_ARCHIVE_SHA256: "a".repeat(64),
|
|
VULNERABILITY_REPORT_PATH: "vulnerability.json",
|
|
PROVENANCE_ATTESTATION_PATH: "provenance.json",
|
|
VULNERABILITY_PUBLIC_KEY_PATH: "vulnerability.pem",
|
|
VULNERABILITY_KEY_ID: "vulnerability-key",
|
|
PROVENANCE_PUBLIC_KEY_PATH: "provenance.pem",
|
|
PROVENANCE_KEY_ID: "provenance-key",
|
|
CI_RUN_ID: "run",
|
|
CI_RUN_ATTEMPT: "1",
|
|
VITE_COMMIT_SHA: "b".repeat(40),
|
|
VULNERABILITY_INVOCATION_NONCE: "c".repeat(64),
|
|
PROVENANCE_INVOCATION_NONCE: "d".repeat(64),
|
|
RUNNER_TEMP: "/runner",
|
|
GITHUB_OUTPUT: "/runner/github-output",
|
|
};
|
|
await expect(
|
|
runStageVerifiedPromotionCli(environment, {
|
|
cwd: () => "/workspace",
|
|
finalize: async () => finalized as any,
|
|
appendOutput: async () => {
|
|
appendCalls += 1;
|
|
if (failureKind === "partial write failure") {
|
|
// The output sink accepted an unspecified prefix before rejecting.
|
|
}
|
|
throw new Error(failureKind);
|
|
},
|
|
cleanup: async (input) => {
|
|
cleanupInput = input;
|
|
},
|
|
writeStdout: () => undefined,
|
|
}),
|
|
).rejects.toThrow(new RegExp(failureKind, "u"));
|
|
expect(appendCalls).toBe(1);
|
|
expect(cleanupInput).toEqual({
|
|
runnerTempRoot: "/runner",
|
|
stagingRoot: finalized.stagingRoot,
|
|
cleanupToken: finalized.cleanupToken,
|
|
runnerTempIdentity: finalized.runnerTempIdentity,
|
|
stagingIdentity: finalized.stagingIdentity,
|
|
});
|
|
},
|
|
);
|
|
|
|
});
|