refactor: adapter 구현중..

This commit is contained in:
DongHyeonka
2026-08-13 16:02:21 +09:00
parent 30ceac23c1
commit 4dc033cf33
72 changed files with 13370 additions and 1549 deletions
+9 -5
View File
@@ -1,10 +1,13 @@
import { parseAsync } from "@babel/core";
import { spawnSync } from "node:child_process";
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import { mkdir, readFile, readdir } from "node:fs/promises";
import { dirname, extname, isAbsolute, relative, resolve, sep } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { createRequire } from "node:module";
import { architectureDependencyReportArtifactSchema } from "./contracts/release-artifacts.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
type PathRule = Readonly<{ path?: string; pathNot?: string }>;
type ArchitectureRule = Readonly<{
name: string;
@@ -156,10 +159,11 @@ dependencyReport.staticImportGraph = {
typeScriptOnlySourcePolicy: typeScriptOnlyPolicy,
};
await writeFile(
qualityArtifact,
`${JSON.stringify(dependencyReport, null, 2)}\n`,
);
await writeValidatedJsonArtifact({
path: qualityArtifact,
schema: architectureDependencyReportArtifactSchema,
value: dependencyReport,
});
let architectureFailed = false;
+14 -10
View File
@@ -1,10 +1,13 @@
import { readFile, writeFile } from "node:fs/promises";
import { readFile } from "node:fs/promises";
import { evaluateBundleBudget } from "../src/application/policies/performance-budgets.ts";
import {
bundleOutputInventoryArtifactSchema,
bundlePerformanceArtifactSchema,
} from "./contracts/release-artifacts.ts";
import { classifyViteJavascript } from "./lib/classify-vite-bundle.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
type BundleOutput = { path: string; gzipBytes: number };
type BundleReport = { outputs: BundleOutput[]; [key: string]: unknown };
type ViteManifest = Record<
string,
{ file: string; isEntry?: boolean; imports?: string[] }
@@ -14,9 +17,9 @@ type BundleBudgets = {
lazyChunkGzipBytes: number;
};
const report = JSON.parse(
await readFile("artifacts/performance/bundle.json", "utf8"),
) as BundleReport;
const report = bundleOutputInventoryArtifactSchema.parse(
JSON.parse(await readFile("artifacts/performance/bundle.json", "utf8")) as unknown,
);
const viteManifest = JSON.parse(
await readFile("dist/.vite/manifest.json", "utf8"),
) as ViteManifest;
@@ -87,10 +90,11 @@ const completedReport = {
passed,
};
await writeFile(
"artifacts/performance/bundle.json",
`${JSON.stringify(completedReport, null, 2)}\n`,
);
await writeValidatedJsonArtifact({
path: "artifacts/performance/bundle.json",
schema: bundlePerformanceArtifactSchema,
value: completedReport,
});
if (!passed) {
process.stderr.write(
`Bundle budget or manifest integrity failed: ${[
+10 -2
View File
@@ -14,7 +14,10 @@ import { generateCiWorkflow, renderCiWorkflow } from "./generate-ci-workflow.ts"
import {
ciContractReportSchema,
} from "./lib/ci-contract-report.ts";
import { validatePackageScriptGraph } from "./lib/package-script-graph.ts";
import {
validateInstallScriptPolicy,
validatePackageScriptGraph,
} from "./lib/package-script-graph.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
const removalFixtureMode = process.argv.includes("--reduced-removal-fixture");
@@ -35,7 +38,6 @@ if (!/^\d+\.\d+\.\d+$/u.test(nodeVersion)) {
for (const script of [
"build:release-candidate",
"verify:local-evidence",
"verify:provider-evidence",
"verify:promotion",
"generate:ci-workflow",
"check:ci-workflow",
@@ -54,6 +56,12 @@ if (/\b(?:build|rebuild)(?::[\w-]+)?\b/u.test(packageScripts["verify:promotion"]
failures.push("verify:promotion must not build or rebuild candidate bytes");
}
failures.push(...validatePackageScriptGraph(packageScripts, "check:ci"));
failures.push(
...validateInstallScriptPolicy(
packageScripts,
[...new Set(contract.commands.map(({ script }) => script))],
),
);
const immutable = index.gates.get("FE-GATE-015");
const immutableCommands = immutable?.commandIds.map((id) => index.commands.get(id)?.script);
+19 -20
View File
@@ -1,6 +1,8 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { mkdir, readFile } from "node:fs/promises";
import { classifyObjectSchemaChange } from "../src/application/policies/compatibility.ts";
import { compatibilityFixturesArtifactSchema } from "./contracts/release-artifacts.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
type CompatibilitySchema = Readonly<{
required?: readonly string[];
@@ -32,25 +34,22 @@ for (const [family, cases] of Object.entries(fixtures.families)) {
}
await mkdir("artifacts/release", { recursive: true });
await writeFile(
"artifacts/release/compatibility.json",
`${JSON.stringify(
{
schemaVersion: 1,
generatedAt: new Date().toISOString(),
rules: [
"additive changes preserve required fields",
"breaking changes require version bump and migration, discard, fallback, or rollback",
"config and API major versions must match",
"incompatible persisted cache is discarded by default",
"rollback uses a coherent compatibility tuple",
],
results,
},
null,
2,
)}\n`,
);
await writeValidatedJsonArtifact({
path: "artifacts/release/compatibility.json",
schema: compatibilityFixturesArtifactSchema,
value: {
schemaVersion: 1,
generatedAt: new Date().toISOString(),
rules: [
"additive changes preserve required fields",
"breaking changes require version bump and migration, discard, fallback, or rollback",
"config and API major versions must match",
"incompatible persisted cache is discarded by default",
"rollback uses a coherent compatibility tuple",
],
results,
},
});
if (results.some((result) => !result.passed)) {
process.stderr.write("Compatibility fixture classification failed.\n");
+11 -7
View File
@@ -1,4 +1,7 @@
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import { mkdir, readFile, readdir } from "node:fs/promises";
import { designSystemReportArtifactSchema } from "./contracts/release-artifacts.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
import path from "node:path";
import { REQUIRED_COMPONENT_TOKENS, REQUIRED_PRIMITIVE_TOKENS, REQUIRED_SEMANTIC_TOKENS } from "../src/presentation/design-system/tokens/token-contract.ts";
@@ -118,7 +121,7 @@ for (const file of sources) {
}
}
const report = {
const report = designSystemReportArtifactSchema.parse({
schemaVersion: 1,
mode: fixtureMode ? "negative-fixture" : "source",
checkedTokenCount:
@@ -127,14 +130,15 @@ const report = {
REQUIRED_COMPONENT_TOKENS.length,
failures,
passed: failures.length === 0,
};
});
await mkdir("artifacts/quality", { recursive: true });
await writeFile(
fixtureMode
await writeValidatedJsonArtifact({
path: fixtureMode
? "artifacts/quality/design-system-fixture.json"
: "artifacts/quality/design-system.json",
`${JSON.stringify(report, null, 2)}\n`,
);
schema: designSystemReportArtifactSchema,
value: report,
});
if (failures.length > 0) {
process.stderr.write(`Design system contract failed:\n${failures.join("\n")}\n`);
+11 -7
View File
@@ -1,4 +1,7 @@
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import { mkdir, readFile, readdir } from "node:fs/promises";
import { diagnosticsReportArtifactSchema } from "./contracts/release-artifacts.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
import path from "node:path";
import { DIAGNOSTIC_EVENT_REGISTRY } from "../src/contracts/diagnostics.ts";
@@ -89,7 +92,7 @@ for (const file of sources) {
}
}
const report = {
const report = diagnosticsReportArtifactSchema.parse({
schemaVersion: 1,
mode: fixtureMode ? "negative-fixture" : "source",
telemetryEventCount: Object.keys(TELEMETRY_REGISTRY).length,
@@ -97,14 +100,15 @@ const report = {
checkedFiles: sources.length,
failures,
passed: failures.length === 0,
};
});
await mkdir("artifacts/quality", { recursive: true });
await writeFile(
fixtureMode
await writeValidatedJsonArtifact({
path: fixtureMode
? "artifacts/quality/diagnostics-fixture.json"
: "artifacts/quality/diagnostics.json",
`${JSON.stringify(report, null, 2)}\n`,
);
schema: diagnosticsReportArtifactSchema,
value: report,
});
if (failures.length > 0) {
process.stderr.write(
`Diagnostics contract failed:\n${failures.join("\n")}\n`,
+11 -7
View File
@@ -1,4 +1,7 @@
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import { mkdir, readFile, readdir } from "node:fs/promises";
import { i18nReportArtifactSchema } from "./contracts/release-artifacts.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
import path from "node:path";
import { EN_MESSAGES, KO_MESSAGES, MESSAGE_CATALOGS } from "../src/presentation/i18n/catalog.ts";
@@ -77,7 +80,7 @@ for (const file of sources) {
}
}
const report = {
const report = i18nReportArtifactSchema.parse({
schemaVersion: 1,
mode: fixtureMode ? "negative-fixture" : "source",
localeCount: Object.keys(MESSAGE_CATALOGS).length + 1,
@@ -85,14 +88,15 @@ const report = {
checkedFiles: sources.length,
failures,
passed: failures.length === 0,
};
});
await mkdir("artifacts/quality", { recursive: true });
await writeFile(
fixtureMode
await writeValidatedJsonArtifact({
path: fixtureMode
? "artifacts/quality/i18n-fixture.json"
: "artifacts/quality/i18n.json",
`${JSON.stringify(report, null, 2)}\n`,
);
schema: i18nReportArtifactSchema,
value: report,
});
if (failures.length > 0) {
process.stderr.write(`I18n contract failed:\n${failures.join("\n")}\n`);
+9 -6
View File
@@ -1,11 +1,13 @@
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { measureOptionalRecipeBundle } from "./lib/optional-recipe-bundle.ts";
import { optionalRecipeFixturesArtifactSchema } from "./contracts/release-artifacts.ts";
import {
scanOptionalRecipeSources,
scanProductionBundle,
validateRecipeCatalog,
} from "./lib/optional-recipes.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
type ReferenceRuntimeRecipe = Readonly<{
id: string;
@@ -176,17 +178,18 @@ const results = [
),
},
];
const report = {
const report = optionalRecipeFixturesArtifactSchema.parse({
schemaVersion: 1,
results,
bundleBudgetFixtures,
passed: results.every(({ passed }) => passed),
};
});
await mkdir("artifacts/quality", { recursive: true });
await writeFile(
"artifacts/quality/optional-recipe-fixtures.json",
`${JSON.stringify(report, null, 2)}\n`,
);
await writeValidatedJsonArtifact({
path: "artifacts/quality/optional-recipe-fixtures.json",
schema: optionalRecipeFixturesArtifactSchema,
value: report,
});
if (!report.passed) {
process.stderr.write(
`Optional recipe negative fixtures failed: ${results
+10 -4
View File
@@ -1,5 +1,6 @@
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
import { mkdir, readFile, stat } from "node:fs/promises";
import { optionalRecipesArtifactSchema } from "./contracts/release-artifacts.ts";
import {
measureOptionalRecipeBundle,
type OptionalRecipeBundleMeasurement,
@@ -10,6 +11,7 @@ import {
validateRecipeCatalog,
} from "./lib/optional-recipes.ts";
import { assertMatchesJsonSchema } from "./lib/json-schema.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
type GateViolation = Readonly<{
ruleId: string;
@@ -141,7 +143,7 @@ const violations: GateViolation[] = [
...referenceRuntimeBundleConfigurationViolations,
...referenceRuntimeBundleMeasurementViolations,
];
const report = {
const report = optionalRecipesArtifactSchema.parse({
schemaVersion: 1,
decisionId: "VD-10",
selectedCapabilities: [],
@@ -169,9 +171,13 @@ const report = {
: "NOT_BUILT",
violations,
passed: violations.length === 0,
};
});
await mkdir("artifacts/quality", { recursive: true });
await writeFile(artifactPath, `${JSON.stringify(report, null, 2)}\n`);
await writeValidatedJsonArtifact({
path: artifactPath,
schema: optionalRecipesArtifactSchema,
value: report,
});
if (violations.length > 0) {
process.stderr.write(
+9 -6
View File
@@ -1,14 +1,16 @@
import { mkdir, writeFile } from "node:fs/promises";
import { mkdir } from "node:fs/promises";
import path from "node:path";
import { realtimeBoundariesArtifactSchema } from "./contracts/release-artifacts.ts";
import { scanRealtimeBoundaries } from "./lib/realtime-boundaries.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
const sourceRoot = argument("--source-root") ?? "src";
const artifact =
argument("--artifact") ??
"artifacts/quality/realtime-boundaries.json";
const violations = await scanRealtimeBoundaries(sourceRoot);
const report = Object.freeze({
const report = realtimeBoundariesArtifactSchema.parse({
schemaVersion: 1,
sourceRoot,
violations,
@@ -16,10 +18,11 @@ const report = Object.freeze({
});
await mkdir(path.dirname(artifact), { recursive: true });
await writeFile(
artifact,
`${JSON.stringify(report, null, 2)}\n`,
);
await writeValidatedJsonArtifact({
path: artifact,
schema: realtimeBoundariesArtifactSchema,
value: report,
});
if (violations.length > 0) {
for (const violation of violations) {
@@ -1,10 +1,12 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { mkdir, readFile } from "node:fs/promises";
import { registryCompatibilityFixturesArtifactSchema } from "./contracts/release-artifacts.ts";
import {
diffRegistrySnapshots,
validateBreakingEvidence,
verifyRegistryBaselineApproval,
} from "./lib/registry-compatibility.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
const fixtures = JSON.parse(
await readFile(
@@ -50,10 +52,11 @@ results.push({
});
await mkdir("artifacts/quality", { recursive: true });
await writeFile(
"artifacts/quality/registry-compatibility-fixtures.json",
`${JSON.stringify({ schemaVersion: 1, results }, null, 2)}\n`,
);
await writeValidatedJsonArtifact({
path: "artifacts/quality/registry-compatibility-fixtures.json",
schema: registryCompatibilityFixturesArtifactSchema,
value: { schemaVersion: 1, results },
});
if (results.some((result) => !result.passed)) {
process.stderr.write("Registry compatibility fixture failed.\n");
process.exit(1);
+8 -5
View File
@@ -1,5 +1,6 @@
import { mkdir, writeFile } from "node:fs/promises";
import { mkdir } from "node:fs/promises";
import { supplyChainFixturesArtifactSchema } from "./contracts/release-artifacts.ts";
import {
diffDependencyInventories,
isValidSha512Integrity,
@@ -9,6 +10,7 @@ import {
validateVulnerabilityReport,
verifySupplyChainCoherence,
} from "./lib/supply-chain.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
const integrity = `sha512-${Buffer.alloc(64, 1).toString("base64")}`;
const baseDependency = {
@@ -137,10 +139,11 @@ const results = [
},
];
await mkdir("artifacts/security", { recursive: true });
await writeFile(
"artifacts/security/supply-chain-fixtures.json",
`${JSON.stringify({ schemaVersion: 1, results }, null, 2)}\n`,
);
await writeValidatedJsonArtifact({
path: "artifacts/security/supply-chain-fixtures.json",
schema: supplyChainFixturesArtifactSchema,
value: { schemaVersion: 1, results },
});
if (results.some((result) => !result.passed)) {
process.stderr.write("Supply-chain negative fixture failed.\n");
process.exit(1);
+307 -260
View File
@@ -1,248 +1,161 @@
import { createHash, generateKeyPairSync, sign } from "node:crypto";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
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 { localEvidenceAssessmentArtifactSchema } from "./contracts/release-artifacts.ts";
import { verifyPromotionInputs } from "./lib/promotion-verifier.ts";
import {
createReleaseCandidateManifest,
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 NOW = Date.parse("2026-08-02T01:00:00.000Z");
const FIXTURE_SOURCE = Object.freeze({
revision: "a".repeat(40),
sourceSetSha256: "b".repeat(64),
});
const FIXTURE_LOCAL_IDENTITY = Object.freeze({
sourceRevision: FIXTURE_SOURCE.revision,
sourceSetSha256: FIXTURE_SOURCE.sourceSetSha256,
assessmentSha256: "c".repeat(64),
});
const fixtureRoot = await mkdtemp(
path.join(tmpdir(), "supply-chain-provider-fixture-"),
);
const repositoryRoot = process.cwd();
const fixtureRoot = await mkdtemp(path.join(tmpdir(), "provider-exact-five-fixture-"));
try {
const repositoryRoot = process.cwd();
const actualCandidate = releaseCandidateManifestSchema.parse(
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(repositoryRoot, RELEASE_CANDIDATE_MANIFEST_PATH),
"utf8",
),
await readFile(path.join(fixtureRoot, RELEASE_CANDIDATE_MANIFEST_PATH), "utf8"),
) as unknown,
);
const actualAssessment = localEvidenceAssessmentArtifactSchema.parse(
const assessment = localEvidenceAssessmentArtifactSchema.parse(
JSON.parse(
await readFile(path.join(repositoryRoot, LOCAL_EVIDENCE_ASSESSMENT_PATH), "utf8"),
await readFile(path.join(fixtureRoot, LOCAL_EVIDENCE_ASSESSMENT_PATH), "utf8"),
) as unknown,
);
const actualProviderEnvironment = absoluteProviderEnvironment(
fixtureRoot,
await writeProviderEnvironment(
fixtureRoot,
"actual",
actualCandidate,
{
revision: actualAssessment.source.revision,
sourceSetSha256: actualAssessment.source.sourceSetSha256,
},
),
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" },
);
const actualDefaultVerifier = await verifyPromotionInputs({
artifactType: "provider-verification",
environment: actualProviderEnvironment,
nowEpochMs: () => NOW,
});
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,
FIXTURE_SOURCE,
);
const wrongEnvironment = await writeProviderEnvironment(
fixtureRoot,
"wrong",
candidate,
FIXTURE_SOURCE,
{ distSha256: "3".repeat(64) },
);
const acceptLocalEvidence = async () => ({
status: "PASS" as const,
identity: FIXTURE_LOCAL_IDENTITY,
failures: [] as const,
});
const fixtures = {
absent: await verifyPromotionInputs({
artifactType: "provider-verification",
repositoryRoot: fixtureRoot,
environment: {},
verifyLocalEvidence: acceptLocalEvidence,
nowEpochMs: () => NOW,
}),
validImmutable: await verifyPromotionInputs({
artifactType: "provider-verification",
repositoryRoot: fixtureRoot,
environment: validEnvironment,
verifyLocalEvidence: acceptLocalEvidence,
nowEpochMs: () => NOW,
}),
wrongDigest: await verifyPromotionInputs({
artifactType: "provider-verification",
repositoryRoot: fixtureRoot,
environment: wrongEnvironment,
verifyLocalEvidence: acceptLocalEvidence,
nowEpochMs: () => NOW,
}),
postAttestationMutation: null as Awaited<
ReturnType<typeof verifyPromotionInputs>
> | null,
};
await writeFile(path.join(fixtureRoot, "dist/app.js"), "mutated\n");
fixtures.postAttestationMutation = await verifyPromotionInputs({
artifactType: "provider-verification",
repositoryRoot: fixtureRoot,
environment: validEnvironment,
verifyLocalEvidence: acceptLocalEvidence,
nowEpochMs: () => NOW,
});
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 [
"CANDIDATE_ARCHIVE_PATH",
"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,
candidate: Awaited<ReturnType<typeof createReleaseCandidateManifest>>,
source: Readonly<{ revision: string; sourceSetSha256: string }>,
overrides: Readonly<{ distSha256?: string }> = {},
): Promise<NodeJS.ProcessEnv> {
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 directory = `provider/${name}`;
const archiveBytes = `fixture archive ${name}\n`;
const archiveSha256 = createHash("sha256").update(archiveBytes).digest("hex");
const candidateIdentity = {
archiveSha256,
bundleSha256: candidate.bundleSha256,
distSha256: overrides.distSha256 ?? candidate.distSha256,
lockfileSha256: candidate.lockfileSha256,
};
const sourceIdentity = {
revision: source.revision,
sourceSetSha256: source.sourceSetSha256,
};
await mkdir(path.join(repositoryRoot, directory), { recursive: true });
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: "2026-08-02T01:00:00.000Z",
expiresAt: "2026-08-02T02:00:00.000Z",
run: { id: "fixture-run", attempt: 1, invocationNonce: "1".repeat(64) },
source: sourceIdentity,
candidate: candidateIdentity,
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: [],
},
"fixture-vulnerability-key",
vulnerabilityKeyId,
vulnerabilityKeys.publicKey,
vulnerabilityKeys.privateKey,
);
@@ -252,65 +165,175 @@ async function writeProviderEnvironment(
evidenceType: "provenance-attestation",
provider: "fixture-provenance-provider",
signer: "fixture-workload-identity",
issuedAt: "2026-08-02T01:00:00.000Z",
expiresAt: "2026-08-02T02:00:00.000Z",
run: { id: "fixture-run", attempt: 1, invocationNonce: "2".repeat(64) },
source: sourceIdentity,
candidate: candidateIdentity,
subject: { name: "dist", digest: { sha256: candidateIdentity.distSha256 } },
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 } },
},
"fixture-provenance-key",
provenanceKeyId,
provenanceKeys.publicKey,
provenanceKeys.privateKey,
);
await Promise.all([
writeFile(
path.join(repositoryRoot, directory, "candidate.tar.gz"),
archiveBytes,
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" }),
),
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(),
write(
fixtureRoot,
"provider/provenance.pem",
provenanceKeys.publicKey.export({ type: "spki", format: "pem" }),
),
]);
return {
CANDIDATE_ARCHIVE_PATH: `${directory}/candidate.tar.gz`,
CANDIDATE_ARCHIVE_SHA256: archiveSha256,
CI_RUN_ID: "fixture-run",
CI_RUN_ATTEMPT: "1",
EXPECTED_SOURCE_REVISION: source.revision,
VULNERABILITY_INVOCATION_NONCE: "1".repeat(64),
PROVENANCE_INVOCATION_NONCE: "2".repeat(64),
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",
};
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: ReturnType<typeof generateKeyPairSync>["publicKey"],
privateKey: ReturnType<typeof generateKeyPairSync>["privateKey"],
publicKey: KeyObject,
privateKey: KeyObject,
) {
return {
...value,
@@ -326,3 +349,27 @@ function signedEvidence(
},
};
}
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");
}
+4
View File
@@ -21,5 +21,9 @@ await cleanupFinalizedPromotion({
dev: requiredIdentity("PROMOTION_RUNNER_TEMP_DEV"),
ino: requiredIdentity("PROMOTION_RUNNER_TEMP_INO"),
},
stagingIdentity: {
dev: requiredIdentity("PROMOTION_STAGING_DEV"),
ino: requiredIdentity("PROMOTION_STAGING_INO"),
},
});
process.stdout.write("Promotion staging cleanup: PASS\n");
+123 -17
View File
@@ -9,7 +9,11 @@ import {
RELEASE_CANDIDATE_EVIDENCE_PATHS,
RELEASE_CANDIDATE_MANIFEST_PATH,
} from "../lib/release-candidate.ts";
import { validatePackageScriptGraph } from "../lib/package-script-graph.ts";
import {
validateInstallScriptPolicy,
validateNpmScopeEnvironment,
validatePackageScriptGraph,
} from "../lib/package-script-graph.ts";
import { PROMOTED_UPLOAD_PATHS } from "./promotion-artifacts.ts";
const ciActionRegistrationSchema = z
@@ -201,7 +205,22 @@ const artifactSchemaSchema = z.discriminatedUnion("kind", [
kind: z.literal("json"),
maxBytes: z.number().int().min(1).max(268_435_456),
executableSchemaId: z.enum([
"generic-json-object",
"automated-a11y",
"manual-a11y",
"architecture-dependency-report",
"design-system-contract",
"i18n-contract",
"diagnostics-contract",
"realtime-boundaries",
"optional-recipes",
"optional-recipe-fixtures",
"registry-compatibility-fixtures",
"reproducible-build",
"supply-chain-fixtures",
"supply-chain-provider-fixtures",
"compatibility-fixtures",
"documentation-review",
"hosting-headers",
"coverage-summary-v8",
"risk-coverage-v3",
"build-manifest",
@@ -318,12 +337,6 @@ const uploadStep = z
const downloadStep = z
.object({ kind: z.literal("download"), transferId: id, path: workflowPath })
.strict();
const validateCandidateArchiveStep = z
.object({
kind: z.literal("validate-candidate-archive"),
archivePath: workflowPath,
})
.strict();
const extractStep = z
.object({
kind: z.literal("extract"),
@@ -360,7 +373,6 @@ const jobStepSchema = z.discriminatedUnion("kind", [
archiveCandidateStep,
uploadStep,
downloadStep,
validateCandidateArchiveStep,
extractStep,
providerStep,
validateProviderStep,
@@ -374,7 +386,7 @@ const jobSchema = z
displayName: workflowValue,
kind: z.enum(["gate-matrix", "gate-single", "immutable", "provider", "promotion"]),
needs: z.array(id).max(32),
condition: z.enum(["always", "merge", "release", "production", "field", "documentation"]),
condition: z.enum(["always", "needs-success", "merge", "release", "production", "field", "documentation"]),
timeoutMinutes: z.number().int().positive(),
gateIds: z.array(id).max(64),
browserGateIds: z.array(id).max(64),
@@ -455,7 +467,43 @@ function canonicalGateShapeSha256(gates: CiGateContract["gates"]): string {
return createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
}
export function parseCiGateContract(value: unknown): CiGateContract {
function canonicalAuthorityBaselineFailures(contract: CiGateContract): string[] {
const failures: string[] = [];
const commandReferenceCount = contract.gates.reduce(
(total, gate) => total + gate.commandIds.length,
0,
);
if (contract.gates.length !== 26) {
failures.push(`gate authority baseline must contain exactly 26 gates; received ${contract.gates.length}`);
}
if (contract.commands.length !== 81 || commandReferenceCount !== 93) {
failures.push(
`command authority baseline must contain exactly 81 definitions and 93 references; received ${contract.commands.length} definitions and ${commandReferenceCount} references`,
);
}
if (contract.artifacts.length !== 105) {
failures.push(`artifact authority baseline must contain exactly 105 artifacts; received ${contract.artifacts.length}`);
}
if (contract.stages.length !== 5) {
failures.push(`stage authority baseline must contain exactly 5 stages; received ${contract.stages.length}`);
}
const expectedRetentionClasses = [
{ id: "merge-cycle", policy: "at least through pull-request readiness decision" },
{ id: "release-coherence", policy: "at least until the next release is promoted" },
{ id: "prod-drill", policy: "at least until the next production promotion decision" },
{ id: "field", policy: "through the 28-day window and aggregation" },
{ id: "documentation", policy: "through documentation readiness review" },
];
if (JSON.stringify(contract.retention.classes) !== JSON.stringify(expectedRetentionClasses)) {
failures.push("retention registry must contain exactly the five canonical retention classes");
}
return failures;
}
export function parseCiGateContract(
value: unknown,
options: LoadCiGateContractOptions = {},
): CiGateContract {
const result = ciGateContractSchema.safeParse(value);
if (!result.success) {
const diagnostic = result.error.issues
@@ -463,6 +511,12 @@ export function parseCiGateContract(value: unknown): CiGateContract {
.join("\n");
throw new TypeError(`CI gate contract invalid:\n${diagnostic}`);
}
if ((options.mode ?? "canonical") === "canonical") {
const failures = canonicalAuthorityBaselineFailures(result.data);
if (failures.length > 0) {
throw new TypeError(`CI gate contract invalid:\n${failures.map((failure) => `root: ${failure}`).join("\n")}`);
}
}
return result.data;
}
@@ -470,12 +524,12 @@ export async function loadCiGateContract(
root = process.cwd(),
options: LoadCiGateContractOptions = {},
): Promise<CiGateContract> {
const mode = options.mode ?? "canonical";
const [rawContract, rawPackage] = await Promise.all([
readFile(path.join(root, "config/ci/gates.json"), "utf8"),
readFile(path.join(root, "package.json"), "utf8"),
]);
const contract = parseCiGateContract(JSON.parse(rawContract));
const mode = options.mode ?? "canonical";
const contract = parseCiGateContract(JSON.parse(rawContract), { mode });
if (
mode === "canonical" &&
canonicalGateShapeSha256(contract.gates) !== CANONICAL_GATE_SHAPE_SHA256
@@ -510,13 +564,57 @@ export async function loadCiGateContract(
throw new TypeError(`canonical check:ci dependency drift: ${script}`);
}
}
const graphFailures = validatePackageScriptGraph(packageDocument.scripts, "check:ci");
const contractEntryScripts = [
...new Set(contract.commands.map((command) => command.script)),
];
const graphFailures = [
...validatePackageScriptGraph(packageDocument.scripts, "check:ci"),
...contractEntryScripts.flatMap((script) =>
validatePackageScriptGraph(packageDocument.scripts, script)
),
];
if (graphFailures.length > 0) {
throw new TypeError(`CI package script graph invalid:\n${graphFailures.join("\n")}`);
throw new TypeError(
`CI package script graph invalid:\n${[...new Set(graphFailures)].join("\n")}`,
);
}
const installPolicyFailures = validateInstallScriptPolicy(
packageDocument.scripts,
contractEntryScripts,
);
if (installPolicyFailures.length > 0) {
throw new TypeError(
`CI package script install policy invalid:\n${installPolicyFailures.join("\n")}`,
);
}
return contract;
}
export async function withCiGatePreflight<Result>(
root: string,
gateId: string | undefined,
execute: (context: Readonly<{
contract: CiGateContract;
contractIndex: CiGateContractIndex;
gateId: string;
gate: CiGate;
}>) => Promise<Result> | Result,
): Promise<Result> {
const npmScopeEnvironmentFailures = validateNpmScopeEnvironment(process.env);
if (npmScopeEnvironmentFailures.length > 0) {
throw new TypeError(
`CI runner npm scope environment invalid:\n${npmScopeEnvironmentFailures.join("\n")}`,
);
}
const contract = await loadCiGateContract(root);
const contractIndex = indexCiGateContract(contract);
const gate = gateId ? contractIndex.gates.get(gateId) : undefined;
if (!gateId || !gate) {
throw new TypeError("CI gate id must be FE-GATE-001..FE-GATE-026");
}
return execute({ contract, contractIndex, gateId, gate });
}
export function indexCiGateContract(contract: CiGateContract): CiGateContractIndex {
return Object.freeze({
commands: new Map(contract.commands.map((entry) => [entry.id, entry])),
@@ -625,6 +723,14 @@ function validateContractSemantics(
issue(`unknown retention class ${gate.retentionClassId} for ${gate.id}`);
}
}
const referencedRetentionClasses = new Set(
contract.gates.map(({ retentionClassId }) => retentionClassId),
);
for (const retentionClass of contract.retention.classes) {
if (!referencedRetentionClasses.has(retentionClass.id)) {
issue(`orphan retention class: ${retentionClass.id}`);
}
}
const referencedCommands = new Set(contract.gates.flatMap(({ commandIds }) => commandIds));
for (const command of contract.commands) {
if (!referencedCommands.has(command.id)) issue(`orphan command: ${command.id}`);
@@ -751,7 +857,7 @@ function validateContractSemantics(
immutable_build: ["immutable", ["release_gate"], "release"],
vulnerability_provider: ["provider", ["immutable_build"], "always"],
provenance_provider: ["provider", ["immutable_build"], "always"],
promotion: ["promotion", ["immutable_build", "vulnerability_provider", "provenance_provider"], "always"],
promotion: ["promotion", ["immutable_build", "vulnerability_provider", "provenance_provider"], "needs-success"],
production_gate: ["gate-matrix", ["promotion"], "production"],
field_gate: ["gate-single", ["production_gate"], "field"],
documentation_gate: ["gate-single", [], "documentation"],
@@ -1026,7 +1132,7 @@ function validateJobStepKinds(
"gate-matrix": new Set(["checkout", "setup-node", "frozen-install", "browser-install", "run-gate", "upload"]),
"gate-single": new Set(["checkout", "setup-node", "frozen-install", "run-gate", "upload"]),
immutable: new Set(["checkout", "setup-node", "frozen-install", "run-gate", "archive-candidate", "upload"]),
provider: new Set(["checkout", "setup-node", "frozen-install", "download", "validate-candidate-archive", "extract", "run-provider", "validate-provider-evidence", "upload"]),
provider: new Set(["checkout", "setup-node", "frozen-install", "download", "extract", "run-provider", "validate-provider-evidence", "upload"]),
promotion: new Set(["checkout", "setup-node", "frozen-install", "download", "verify-promotion", "upload", "cleanup-promotion"]),
};
for (const step of job.steps) {
+853 -11
View File
@@ -2,6 +2,8 @@ import { z } from "zod";
export * from "../../src/contracts/release-artifacts.ts";
import { MANUAL_A11Y_ROUTE_IDS } from "../lib/manual-a11y-evidence.ts";
const nonEmptyString = z.string().min(1);
const timestamp = z.iso.datetime();
const sha256 = z.string().regex(/^[a-f0-9]{64}$/u);
@@ -85,6 +87,13 @@ export const localEvidenceAssessmentArtifactSchema = z
candidate: z
.object({ distSha256: sha256, lockfileSha256: sha256, sbomSha256: sha256 })
.strict(),
secretScan: z
.object({
policySha256: sha256,
sarifSha256: sha256,
scanInputSha256: sha256,
})
.strict(),
policyInputs: z.array(assessmentInputRowSchema).min(1).max(256),
evidenceInputs: z.array(assessmentInputRowSchema).min(1).max(4_096),
checks: z
@@ -308,20 +317,162 @@ const outputDigestSchema = z
})
.strict();
export const bundlePerformanceArtifactSchema = z
const bundleOutputInventoryShape = {
schemaVersion: z.literal(1),
generatedAt: timestamp,
context: z
.object({
nodeVersion: nonEmptyString,
packageManager: nonEmptyString,
runnerImage: nonEmptyString,
})
.strict(),
outputs: z.array(outputDigestSchema).min(1),
} as const;
function addUniqueBundleOutputIssues(
artifact: Readonly<{ outputs: readonly Readonly<{ path: string }>[] }>,
context: z.RefinementCtx,
): void {
const paths = artifact.outputs.map(({ path }) => path);
if (new Set(paths).size !== paths.length) {
context.addIssue({
code: "custom",
path: ["outputs"],
message: "output paths must be unique",
});
}
}
export const bundleOutputInventoryArtifactSchema = z
.object(bundleOutputInventoryShape)
.strict()
.superRefine(addUniqueBundleOutputIssues);
const bundleMeasurementSchema = z
.object({ path: nonEmptyString, gzipBytes: z.int().nonnegative() })
.strict();
const bundleClassificationSchema = z
.object({
schemaVersion: z.literal(1),
generatedAt: timestamp,
context: z
.object({
nodeVersion: nonEmptyString,
packageManager: nonEmptyString,
runnerImage: nonEmptyString,
})
.strict(),
outputs: z.array(outputDigestSchema).min(1),
initialFiles: z.array(nonEmptyString),
lazyFiles: z.array(nonEmptyString),
missingImports: z.array(nonEmptyString),
})
.strict();
const bundleThresholdsSchema = z
.object({
initialJsGzipBytes: z.int().positive(),
lazyChunkGzipBytes: z.int().positive(),
})
.strict();
const bundleBudgetResultSchema = z
.object({
initialPassed: z.boolean(),
lazyResults: z.array(
bundleMeasurementSchema.extend({
threshold: z.int().positive(),
passed: z.boolean(),
}),
),
passed: z.boolean(),
})
.strict();
export const bundlePerformanceArtifactSchema = z
.object({
...bundleOutputInventoryShape,
measurements: z
.object({
initialJsGzipBytes: z.int().nonnegative(),
lazyChunks: z.array(bundleMeasurementSchema),
})
.strict(),
classification: bundleClassificationSchema,
missingOutputs: z.array(nonEmptyString),
thresholds: bundleThresholdsSchema,
results: bundleBudgetResultSchema,
fixtures: z.tuple([
z.object({ name: z.literal("initial-js-over-budget"), passed: z.boolean() }).strict(),
z.object({ name: z.literal("lazy-chunk-over-budget"), passed: z.boolean() }).strict(),
]),
passed: z.boolean(),
})
.strict()
.superRefine((artifact, context) => {
addUniqueBundleOutputIssues(artifact, context);
const issue = (path: PropertyKey[], message: string) =>
context.addIssue({ code: "custom", path, message });
const uniqueSorted = (values: readonly string[]) =>
new Set(values).size === values.length &&
JSON.stringify(values) === JSON.stringify([...values].sort());
for (const [field, values] of [
["initialFiles", artifact.classification.initialFiles],
["lazyFiles", artifact.classification.lazyFiles],
["missingImports", artifact.classification.missingImports],
["missingOutputs", artifact.missingOutputs],
] as const) {
if (!uniqueSorted(values)) {
issue(
field === "missingOutputs" ? [field] : ["classification", field],
"paths must be unique and sorted",
);
}
}
const initial = new Set(artifact.classification.initialFiles);
if (artifact.classification.lazyFiles.some((file) => initial.has(file))) {
issue(["classification"], "initial and lazy files must be disjoint");
}
const outputs = new Map(
artifact.outputs.map((output) => [output.path.replace(/^dist\//u, ""), output]),
);
const expectedMissing = [
...artifact.classification.initialFiles,
...artifact.classification.lazyFiles,
].filter((file) => !outputs.has(file)).sort();
if (JSON.stringify(artifact.missingOutputs) !== JSON.stringify(expectedMissing)) {
issue(["missingOutputs"], "must equal classified JavaScript outputs not found in inventory");
}
const expectedInitialBytes = artifact.classification.initialFiles.reduce(
(total, file) => total + (outputs.get(file)?.gzipBytes ?? 0),
0,
);
if (artifact.measurements.initialJsGzipBytes !== expectedInitialBytes) {
issue(["measurements", "initialJsGzipBytes"], "must equal classified initial output bytes");
}
const expectedLazyChunks = artifact.classification.lazyFiles.map((file) => ({
path: file,
gzipBytes: outputs.get(file)?.gzipBytes ?? 0,
}));
if (JSON.stringify(artifact.measurements.lazyChunks) !== JSON.stringify(expectedLazyChunks)) {
issue(["measurements", "lazyChunks"], "must equal classified lazy output bytes");
}
const expectedInitialPassed =
artifact.measurements.initialJsGzipBytes <= artifact.thresholds.initialJsGzipBytes;
if (artifact.results.initialPassed !== expectedInitialPassed) {
issue(["results", "initialPassed"], "must agree with initial threshold");
}
const expectedLazyResults = artifact.measurements.lazyChunks.map((chunk) => ({
...chunk,
threshold: artifact.thresholds.lazyChunkGzipBytes,
passed: chunk.gzipBytes <= artifact.thresholds.lazyChunkGzipBytes,
}));
if (JSON.stringify(artifact.results.lazyResults) !== JSON.stringify(expectedLazyResults)) {
issue(["results", "lazyResults"], "must agree with lazy measurements and threshold");
}
const expectedBudgetPassed =
expectedInitialPassed && expectedLazyResults.every(({ passed }) => passed);
if (artifact.results.passed !== expectedBudgetPassed) {
issue(["results", "passed"], "must agree with budget results");
}
const expectedPassed =
expectedBudgetPassed &&
artifact.fixtures.every(({ passed }) => passed) &&
artifact.classification.missingImports.length === 0 &&
artifact.missingOutputs.length === 0;
if (artifact.passed !== expectedPassed) {
issue(["passed"], "must agree with budgets, fixtures, and manifest integrity");
}
});
const cyclonedxComponentSchema = z
.object({
@@ -571,3 +722,694 @@ export const runbookRecordArtifactSchema = z
passed: z.boolean(),
})
.strict();
const failureList = z.array(nonEmptyString).max(4_096);
const sourceOrFixtureMode = z.enum(["source", "negative-fixture"]);
const namedBooleanResultSchema = z
.object({ id: nonEmptyString, passed: z.boolean() })
.strict();
function addPassedFailureInvariant(
artifact: Readonly<{ passed: boolean; failures: readonly string[] }>,
context: z.RefinementCtx,
): void {
if (artifact.passed !== (artifact.failures.length === 0)) {
context.addIssue({
code: "custom",
path: ["passed"],
message: "passed must agree with failures",
});
}
}
function addUniqueStringIssues(
values: readonly string[],
path: PropertyKey[],
context: z.RefinementCtx,
): void {
if (new Set(values).size !== values.length) {
context.addIssue({ code: "custom", path, message: "must not contain duplicates" });
}
}
export const automatedA11yArtifactSchema = z
.object({
schemaVersion: z.literal(1),
generatedAt: timestamp,
scope: z.array(nonEmptyString).min(1).max(128),
threshold: z.object({ critical: z.literal(0), serious: z.literal(0) }).strict(),
automatedStatus: z.literal("passed"),
manualReview: z.literal("see artifacts/tests/a11y-manual/report.json"),
})
.strict()
.superRefine((artifact, context) => {
addUniqueStringIssues(artifact.scope, ["scope"], context);
if (JSON.stringify(artifact.scope) !== JSON.stringify(MANUAL_A11Y_ROUTE_IDS)) {
context.addIssue({ code: "custom", path: ["scope"], message: "must match the installed route registry" });
}
});
const manualA11yResultSchema = z
.object({
routeId: nonEmptyString,
path: nonEmptyString,
reviewer: z.string().nullable(),
reviewedAt: z.string().nullable(),
releaseId: z.string().nullable(),
failures: failureList,
passed: z.boolean(),
})
.strict()
.superRefine((result, context) => {
if (result.path !== `artifacts/tests/a11y-manual/${result.routeId}.md`) {
context.addIssue({ code: "custom", path: ["path"], message: "path must match routeId" });
}
const hasIdentity = Boolean(
result.reviewer &&
result.releaseId &&
result.reviewedAt &&
Number.isFinite(Date.parse(result.reviewedAt)),
);
if (result.passed !== (result.failures.length === 0 && hasIdentity)) {
context.addIssue({
code: "custom",
path: ["passed"],
message: "passed must agree with failures and review identity",
});
}
});
export const manualA11yReportArtifactSchema = z
.object({
schemaVersion: z.literal(1),
generatedAt: timestamp,
scope: z.array(nonEmptyString).min(1).max(128),
results: z.array(manualA11yResultSchema).min(1).max(128),
coherentRelease: z.boolean(),
passed: z.boolean(),
})
.strict()
.superRefine((artifact, context) => {
const routeIds = artifact.results.map(({ routeId }) => routeId);
addUniqueStringIssues(artifact.scope, ["scope"], context);
addUniqueStringIssues(routeIds, ["results"], context);
if (JSON.stringify(artifact.scope) !== JSON.stringify(MANUAL_A11Y_ROUTE_IDS)) {
context.addIssue({ code: "custom", path: ["scope"], message: "must match the installed route registry" });
}
if (JSON.stringify(routeIds) !== JSON.stringify(artifact.scope)) {
context.addIssue({ code: "custom", path: ["results"], message: "result routeIds must match scope" });
}
const releaseIds = artifact.results.map(({ releaseId }) => releaseId);
const coherentRelease =
releaseIds.every((releaseId): releaseId is string => Boolean(releaseId)) &&
new Set(releaseIds).size === 1;
if (artifact.coherentRelease !== coherentRelease) {
context.addIssue({ code: "custom", path: ["coherentRelease"], message: "must represent one non-empty releaseId" });
}
if (
artifact.passed !==
(coherentRelease && artifact.results.every(({ passed }) => passed))
) {
context.addIssue({ code: "custom", path: ["passed"], message: "must agree with route results" });
}
});
const architectureDependencySchema = z
.object({
source: nonEmptyString,
target: nonEmptyString,
specifier: nonEmptyString,
kind: z.enum(["local", "external"]),
})
.strict();
const architectureUnresolvedSchema = z
.object({ source: nonEmptyString, specifier: nonEmptyString, reason: nonEmptyString })
.strict();
const architectureViolationSchema = z
.object({
rule: nonEmptyString,
severity: nonEmptyString,
source: nonEmptyString,
target: nonEmptyString,
cycle: z.array(nonEmptyString).optional(),
})
.strict();
const staticImportGraphSchema = z
.object({
analyzer: z.literal("babel-parser-node-resolver"),
modules: z.array(nonEmptyString),
dependencies: z.array(architectureDependencySchema),
unresolved: z.array(architectureUnresolvedSchema),
parseFailures: z.array(
z.object({ source: nonEmptyString, reason: nonEmptyString }).strict(),
),
cycles: z.array(z.array(nonEmptyString).min(1)),
violations: z.array(architectureViolationSchema),
summary: z
.object({
modules: z.int().nonnegative(),
typescriptModules: z.int().nonnegative(),
dependencies: z.int().nonnegative(),
localDependencies: z.int().nonnegative(),
unresolved: z.int().nonnegative(),
parseFailures: z.int().nonnegative(),
cycles: z.int().nonnegative(),
errors: z.int().nonnegative(),
typeScriptOnlyPolicyPassed: z.boolean(),
nonTypeScriptExecutableSources: z.int().nonnegative(),
})
.strict(),
fixtureChecks: z
.object({ passed: z.boolean(), checks: z.array(nonEmptyString), failures: failureList })
.strict(),
typeScriptOnlySourcePolicy: z
.object({
checkedRoots: z.array(nonEmptyString).min(1),
exceptionsAllowed: z.literal(false),
violations: z.array(nonEmptyString),
passed: z.boolean(),
})
.strict(),
})
.strict()
.superRefine((graph, context) => {
const counts = [
["modules", graph.modules.length],
["dependencies", graph.dependencies.length],
["localDependencies", graph.dependencies.filter(({ kind }) => kind === "local").length],
["unresolved", graph.unresolved.length],
["parseFailures", graph.parseFailures.length],
["cycles", graph.cycles.length],
["errors", graph.violations.filter(({ severity }) => severity === "error").length],
["nonTypeScriptExecutableSources", graph.typeScriptOnlySourcePolicy.violations.length],
] as const;
for (const [field, expected] of counts) {
if (graph.summary[field] !== expected) {
context.addIssue({ code: "custom", path: ["summary", field], message: "count does not match evidence rows" });
}
}
if (graph.summary.typescriptModules > graph.summary.modules) {
context.addIssue({ code: "custom", path: ["summary", "typescriptModules"], message: "cannot exceed modules" });
}
if (graph.fixtureChecks.passed !== (graph.fixtureChecks.failures.length === 0)) {
context.addIssue({ code: "custom", path: ["fixtureChecks", "passed"], message: "must agree with failures" });
}
if (
graph.typeScriptOnlySourcePolicy.passed !==
(graph.typeScriptOnlySourcePolicy.violations.length === 0) ||
graph.summary.typeScriptOnlyPolicyPassed !== graph.typeScriptOnlySourcePolicy.passed
) {
context.addIssue({ code: "custom", path: ["typeScriptOnlySourcePolicy", "passed"], message: "must agree with violations and summary" });
}
});
const dependencyCruiserSummarySchema = z
.object({
violations: z.array(jsonObject),
error: z.int().nonnegative(),
warn: z.int().nonnegative(),
info: z.int().nonnegative(),
ignore: z.int().nonnegative(),
totalCruised: z.int().nonnegative(),
totalDependenciesCruised: z.int().nonnegative(),
})
.catchall(z.json());
export const architectureDependencyReportArtifactSchema = z.union([
z
.object({
modules: z.array(jsonObject),
summary: dependencyCruiserSummarySchema,
staticImportGraph: staticImportGraphSchema,
})
.strict(),
z
.object({
summary: z.object({ errors: z.literal(1) }).strict(),
dependencyCruiserOutput: z.string(),
staticImportGraph: staticImportGraphSchema,
})
.strict(),
]);
export const designSystemReportArtifactSchema = z
.object({
schemaVersion: z.literal(1),
mode: sourceOrFixtureMode,
checkedTokenCount: z.int().positive(),
failures: failureList,
passed: z.boolean(),
})
.strict()
.superRefine(addPassedFailureInvariant);
export const i18nReportArtifactSchema = z
.object({
schemaVersion: z.literal(1),
mode: sourceOrFixtureMode,
localeCount: z.int().positive(),
messageKeyCount: z.int().positive(),
checkedFiles: z.int().nonnegative(),
failures: failureList,
passed: z.boolean(),
})
.strict()
.superRefine(addPassedFailureInvariant);
export const diagnosticsReportArtifactSchema = z
.object({
schemaVersion: z.literal(1),
mode: sourceOrFixtureMode,
telemetryEventCount: z.int().positive(),
diagnosticEventCount: z.int().positive(),
checkedFiles: z.int().nonnegative(),
failures: failureList,
passed: z.boolean(),
})
.strict()
.superRefine(addPassedFailureInvariant);
export const realtimeBoundariesArtifactSchema = z
.object({
schemaVersion: z.literal(1),
sourceRoot: nonEmptyString,
violations: z.array(
z
.object({
ruleId: z.enum([
"NATIVE_REALTIME_API_OUTSIDE_ADAPTER",
"PRESENTATION_INTERVAL_OWNER",
"UNSELECTED_REALTIME_RUNTIME_COMPOSED",
]),
file: nonEmptyString,
line: z.int().positive(),
})
.strict(),
),
passed: z.boolean(),
})
.strict()
.superRefine((artifact, context) => {
if (artifact.passed !== (artifact.violations.length === 0)) {
context.addIssue({ code: "custom", path: ["passed"], message: "must agree with violations" });
}
const keys = artifact.violations.map(({ ruleId, file, line }) => `${file}\0${line}\0${ruleId}`);
addUniqueStringIssues(keys, ["violations"], context);
});
const optionalRecipeBundleOutputSchema = z
.object({
fileName: nonEmptyString,
bytes: z.int().nonnegative(),
gzipBytes: z.int().nonnegative(),
sha256,
})
.strict();
const optionalRecipeBundleMeasurementSchema = z
.object({
recipeId: nonEmptyString,
sourceRoots: z.array(nonEmptyString).min(1),
sourceFileCount: z.int().positive(),
toolchain: z
.object({
bundler: z.literal("vite"),
viteVersion: nonEmptyString,
mode: z.literal("production"),
target: z.literal("es2022"),
format: z.literal("es"),
minifier: z.literal("esbuild"),
treeshake: z.literal(false),
compression: z.literal("node-zlib-gzip"),
})
.strict(),
outputs: z.array(optionalRecipeBundleOutputSchema).min(1),
bytes: z.int().nonnegative(),
gzipBytes: z.int().nonnegative(),
bundleBudgetGzipBytes: z.int().positive(),
remainingGzipBytes: z.int(),
sha256,
passed: z.boolean(),
})
.strict()
.superRefine((measurement, context) => {
if (measurement.bytes !== measurement.outputs.reduce((total, output) => total + output.bytes, 0)) {
context.addIssue({ code: "custom", path: ["bytes"], message: "must equal output bytes" });
}
if (measurement.gzipBytes !== measurement.outputs.reduce((total, output) => total + output.gzipBytes, 0)) {
context.addIssue({ code: "custom", path: ["gzipBytes"], message: "must equal output gzip bytes" });
}
if (measurement.remainingGzipBytes !== measurement.bundleBudgetGzipBytes - measurement.gzipBytes) {
context.addIssue({ code: "custom", path: ["remainingGzipBytes"], message: "must equal budget minus gzip bytes" });
}
if (measurement.passed !== (measurement.gzipBytes <= measurement.bundleBudgetGzipBytes)) {
context.addIssue({ code: "custom", path: ["passed"], message: "must agree with bundle budget" });
}
});
const optionalRecipeReferenceRuntimeSchema = z
.object({
status: z.literal("AVAILABLE_NOT_COMPOSED"),
coveredCapabilities: z.array(nonEmptyString).min(1),
sourceRoots: z.array(nonEmptyString).min(1),
conformanceScripts: z.array(nonEmptyString).min(1),
productionComposition: z.literal(false),
})
.strict();
const optionalRecipeViolationSchema = z
.object({ ruleId: nonEmptyString, path: nonEmptyString, detail: nonEmptyString.optional() })
.strict();
export const optionalRecipesArtifactSchema = z
.object({
schemaVersion: z.literal(1),
decisionId: z.literal("VD-10"),
selectedCapabilities: z.array(nonEmptyString).max(0),
referenceRuntimes: z.array(
z.object({ id: nonEmptyString, referenceRuntime: optionalRecipeReferenceRuntimeSchema }).strict(),
).min(1),
recipeCount: z.int().nonnegative(),
productionRuntimeDependencies: z.array(nonEmptyString).nullable(),
referenceRuntimeBundleBudgets: z.array(optionalRecipeBundleMeasurementSchema).min(1),
bundleStatus: z.enum(["PASS", "FAIL", "NOT_BUILT"]),
violations: z.array(optionalRecipeViolationSchema),
passed: z.boolean(),
})
.strict()
.superRefine((artifact, context) => {
if (artifact.recipeCount < artifact.referenceRuntimes.length) {
context.addIssue({ code: "custom", path: ["recipeCount"], message: "cannot be smaller than reference runtimes" });
}
if (artifact.passed !== (artifact.violations.length === 0)) {
context.addIssue({ code: "custom", path: ["passed"], message: "must agree with violations" });
}
const runtimeIds = artifact.referenceRuntimes.map(({ id }) => id).sort();
const budgetIds = artifact.referenceRuntimeBundleBudgets.map(({ recipeId }) => recipeId).sort();
addUniqueStringIssues(runtimeIds, ["referenceRuntimes"], context);
addUniqueStringIssues(budgetIds, ["referenceRuntimeBundleBudgets"], context);
if (JSON.stringify(runtimeIds) !== JSON.stringify(budgetIds)) {
context.addIssue({ code: "custom", path: ["referenceRuntimeBundleBudgets"], message: "must cover every reference runtime" });
}
});
const OPTIONAL_RECIPE_FIXTURE_IDS = [
"cleanup-omission",
"unselected-runtime-dependency",
"server-state-policy",
"vendor-direct-import",
"credential-leak",
"server-state-source-duplication",
"production-imports-recipe",
"reference-runtime-not-composed",
"reference-runtime-not-bundled",
"reference-runtime-module-not-bundled",
"reference-runtime-bundle-over-budget",
] as const;
const OPTIONAL_RECIPE_BUDGET_FIXTURE_IDS = [
"file-transfer",
"offline-indexeddb",
"realtime",
"service-worker-pwa",
] as const;
export const optionalRecipeFixturesArtifactSchema = z
.object({
schemaVersion: z.literal(1),
results: z.array(
namedBooleanResultSchema.extend({ id: z.enum(OPTIONAL_RECIPE_FIXTURE_IDS) }),
).length(OPTIONAL_RECIPE_FIXTURE_IDS.length),
bundleBudgetFixtures: z.array(
z
.object({
recipeId: z.enum(OPTIONAL_RECIPE_BUDGET_FIXTURE_IDS),
gzipBytes: z.int().nonnegative(),
fixtureBudgetGzipBytes: z.int().positive(),
rejected: z.boolean(),
})
.strict(),
).length(OPTIONAL_RECIPE_BUDGET_FIXTURE_IDS.length),
passed: z.boolean(),
})
.strict()
.superRefine((artifact, context) => {
addUniqueStringIssues(artifact.results.map(({ id }) => id), ["results"], context);
addUniqueStringIssues(artifact.bundleBudgetFixtures.map(({ recipeId }) => recipeId), ["bundleBudgetFixtures"], context);
if (artifact.passed !== artifact.results.every(({ passed }) => passed)) {
context.addIssue({ code: "custom", path: ["passed"], message: "must agree with fixture results" });
}
artifact.bundleBudgetFixtures.forEach((fixture, index) => {
if (fixture.rejected !== (fixture.gzipBytes > fixture.fixtureBudgetGzipBytes)) {
context.addIssue({ code: "custom", path: ["bundleBudgetFixtures", index, "rejected"], message: "must agree with fixture budget" });
}
});
});
const registryCompatibilityValueSchema = z.union([
z.enum(["none", "additive", "behavior-change", "breaking"]),
z.boolean(),
]);
const REGISTRY_COMPATIBILITY_FIXTURE_IDS = [
"ordering-only",
"row-addition",
"behavior-change",
"row-removal",
"field-type-narrowing",
"route-path-change",
"registry-contract-narrowing",
"breaking-evidence-required",
"tampered-baseline-digest",
] as const;
export const registryCompatibilityFixturesArtifactSchema = z
.object({
schemaVersion: z.literal(1),
results: z.array(
z
.object({
id: z.enum(REGISTRY_COMPATIBILITY_FIXTURE_IDS),
expected: registryCompatibilityValueSchema,
actual: registryCompatibilityValueSchema,
passed: z.boolean(),
})
.strict(),
).length(REGISTRY_COMPATIBILITY_FIXTURE_IDS.length),
})
.strict()
.superRefine((artifact, context) => {
addUniqueStringIssues(artifact.results.map(({ id }) => id), ["results"], context);
artifact.results.forEach((result, index) => {
if (result.passed !== (result.actual === result.expected)) {
context.addIssue({ code: "custom", path: ["results", index, "passed"], message: "must agree with expected and actual" });
}
});
});
const buildDigestSchema = z.union([sha256, z.literal("BUILD_FAILED")]);
export const reproducibleBuildArtifactSchema = z
.object({
schemaVersion: z.literal(1),
sourceDateEpoch: z.string().regex(/^\d+$/u),
buildId: nonEmptyString,
commitSha: nonEmptyString,
releaseId: nonEmptyString,
runnerImage: nonEmptyString,
firstDigest: buildDigestSchema,
secondDigest: buildDigestSchema,
restored: z.boolean(),
status: z.enum(["PASS", "FAIL"]),
})
.strict()
.superRefine((artifact, context) => {
const passed =
artifact.restored &&
artifact.firstDigest !== "BUILD_FAILED" &&
artifact.firstDigest === artifact.secondDigest;
if ((artifact.status === "PASS") !== passed) {
context.addIssue({ code: "custom", path: ["status"], message: "must agree with build digests and restoration" });
}
});
const SUPPLY_CHAIN_FIXTURE_IDS = [
"transitive-removal-is-real-diff",
"tampered-integrity-rejected",
"high-risk-self-approval-rejected",
"denied-license-rejected",
"critical-vulnerability-expired-exception-rejected",
"sbom-provenance-mismatch-rejected",
"dependency-ordering-deterministic",
"baseline-digest-tamper-rejected",
"vulnerability-provider-evidence-invalid",
] as const;
export const supplyChainFixturesArtifactSchema = z
.object({
schemaVersion: z.literal(1),
results: z.array(
namedBooleanResultSchema.extend({ id: z.enum(SUPPLY_CHAIN_FIXTURE_IDS) }),
).length(SUPPLY_CHAIN_FIXTURE_IDS.length),
})
.strict()
.superRefine((artifact, context) =>
addUniqueStringIssues(artifact.results.map(({ id }) => id), ["results"], context)
);
const providerFixtureResultSchema = z
.object({ status: z.enum(["PASS", "FAIL_UNVERIFIED"]), failures: failureList })
.strict()
.superRefine((result, context) => {
if ((result.status === "PASS") !== (result.failures.length === 0)) {
context.addIssue({ code: "custom", path: ["status"], message: "must agree with failures" });
}
});
export const supplyChainProviderFixturesArtifactSchema = z
.object({
schemaVersion: z.literal(1),
actualDefaultVerifier: providerFixtureResultSchema,
fixtures: z
.object({
absent: providerFixtureResultSchema,
validImmutable: providerFixtureResultSchema,
wrongDigest: providerFixtureResultSchema,
invalidTar: providerFixtureResultSchema,
})
.strict(),
externalTreeCanary: providerFixtureResultSchema,
passingFixtureCount: z.int().nonnegative(),
status: z.enum(["PASS", "FAIL"]),
})
.strict()
.superRefine((artifact, context) => {
const expectedPass =
artifact.actualDefaultVerifier.status === "PASS" &&
artifact.fixtures.validImmutable.status === "PASS" &&
artifact.externalTreeCanary.status === "PASS" &&
[artifact.fixtures.absent, artifact.fixtures.wrongDigest, artifact.fixtures.invalidTar]
.every(({ status }) => status === "FAIL_UNVERIFIED");
if (artifact.passingFixtureCount !== (artifact.fixtures.validImmutable.status === "PASS" ? 1 : 0)) {
context.addIssue({ code: "custom", path: ["passingFixtureCount"], message: "must count the passing immutable fixture" });
}
if ((artifact.status === "PASS") !== expectedPass) {
context.addIssue({ code: "custom", path: ["status"], message: "must agree with required fixture outcomes" });
}
});
const compatibilityClassificationSchema = z.enum(["additive", "breaking"]);
export const compatibilityFixturesArtifactSchema = z
.object({
schemaVersion: z.literal(1),
generatedAt: timestamp,
rules: z.array(nonEmptyString).length(5),
results: z.array(
z
.object({
family: z.enum(["api", "config", "storage", "release"]),
expected: compatibilityClassificationSchema,
actual: compatibilityClassificationSchema,
passed: z.boolean(),
})
.strict(),
).length(8),
})
.strict()
.superRefine((artifact, context) => {
const keys = artifact.results.map(({ family, expected }) => `${family}\0${expected}`);
addUniqueStringIssues(keys, ["results"], context);
artifact.results.forEach((result, index) => {
if (result.passed !== (result.actual === result.expected)) {
context.addIssue({ code: "custom", path: ["results", index, "passed"], message: "must agree with expected and actual" });
}
});
});
export const documentationReviewArtifactSchema = z
.object({
schemaVersion: z.literal(1),
generatedAt: timestamp,
status: z.literal("PASS_SCOPED"),
reviewer: z.literal("wiki-diagram-reviewer"),
standard: z.literal("rules/diagram-standards.md v2"),
evidenceReport: z
.object({ repoPath: nonEmptyString, canonicalPath: nonEmptyString, canonicalSha256: sha256 })
.strict(),
reportDigestValid: z.boolean(),
results: z.array(
z
.object({
diagram: z.enum(["overview", "staticDelivery"]),
sourcePath: nonEmptyString,
sha256,
sourceReferenced: z.boolean(),
digestReferenced: z.boolean(),
reviewer: z.literal("wiki-diagram-reviewer"),
score: z.number().min(0).max(100),
scorePass: z.boolean(),
passed: z.boolean(),
})
.strict(),
).length(2),
passed: z.boolean(),
})
.strict()
.superRefine((artifact, context) => {
addUniqueStringIssues(artifact.results.map(({ diagram }) => diagram), ["results"], context);
artifact.results.forEach((result, index) => {
const passed = result.sourceReferenced && result.digestReferenced && result.scorePass;
if (result.passed !== passed) {
context.addIssue({ code: "custom", path: ["results", index, "passed"], message: "must agree with review evidence" });
}
});
if (artifact.passed !== (artifact.reportDigestValid && artifact.results.every(({ passed }) => passed))) {
context.addIssue({ code: "custom", path: ["passed"], message: "must agree with report digest and review results" });
}
});
export const hostingHeadersArtifactSchema = z
.object({
schemaVersion: z.literal(1),
generatedAt: timestamp,
mode: z.enum(["live", "invalid-live", "fixture"]),
baseUrl: z.string().nullable(),
providerVerificationRequired: z.boolean(),
results: z.array(
z
.object({
surface: nonEmptyString,
header: nonEmptyString,
expected: z.json(),
observed: z.json().optional(),
reason: nonEmptyString.optional(),
passed: z.boolean(),
})
.strict(),
).min(1),
passed: z.boolean(),
})
.strict()
.superRefine((artifact, context) => {
const keys = artifact.results.map(({ surface, header }) => `${surface}\0${header}`);
addUniqueStringIssues(keys, ["results"], context);
const requiredKeys = [
...["index", "runtimeConfig", "releaseManifest"].flatMap((surface) =>
[
"cache-control",
"content-type",
"content-security-policy",
"strict-transport-security",
"x-frame-options",
"referrer-policy",
"x-content-type-options",
"permissions-policy",
].map((header) => `${surface}\0${header}`)
),
"hashedAsset\0cache-control",
"hashedAsset\0content-type",
"sourceMap\0public",
"serviceWorker\0enabled",
];
for (const requiredKey of requiredKeys) {
if (!keys.includes(requiredKey)) {
context.addIssue({ code: "custom", path: ["results"], message: `missing required probe: ${requiredKey}` });
}
}
if (artifact.providerVerificationRequired !== (artifact.mode !== "live")) {
context.addIssue({ code: "custom", path: ["providerVerificationRequired"], message: "must agree with hosting mode" });
}
if ((artifact.mode === "live") !== (artifact.baseUrl !== null)) {
context.addIssue({ code: "custom", path: ["baseUrl"], message: "must be present only for live mode" });
}
if (artifact.passed !== artifact.results.every(({ passed }) => passed)) {
context.addIssue({ code: "custom", path: ["passed"], message: "must agree with probe results" });
}
});
+6 -6
View File
@@ -44,6 +44,7 @@ export type CiWorkflowFileSystem = Readonly<{
readFile(target: string): Promise<Buffer>;
open(target: string, flags: number, mode: number): Promise<{
writeFile(content: string, encoding: "utf8"): Promise<unknown>;
chmod(mode: number): Promise<unknown>;
sync(): Promise<unknown>;
close(): Promise<unknown>;
}>;
@@ -226,11 +227,6 @@ function renderStep(
` name: ${yamlScalar(requiredTransferName(transfers, step.transferId))}`,
` path: ${yamlScalar(step.path)}`,
];
case "validate-candidate-archive":
return [
" - name: Validate immutable candidate before extraction",
` run: node scripts/verify-ci-candidate-archive.ts --archive ${shellDoubleQuoted(step.archivePath)}`,
];
case "extract":
return [
" - name: Verify and extract the candidate through one inode-bound operation",
@@ -263,8 +259,10 @@ function renderStep(
` PROMOTION_CLEANUP_TOKEN: \${{ steps.${step.finalizerStepId}.outputs.cleanup_token }}`,
` PROMOTION_RUNNER_TEMP_DEV: \${{ steps.${step.finalizerStepId}.outputs.runner_temp_dev }}`,
` PROMOTION_RUNNER_TEMP_INO: \${{ steps.${step.finalizerStepId}.outputs.runner_temp_ino }}`,
` PROMOTION_STAGING_DEV: \${{ steps.${step.finalizerStepId}.outputs.staging_dev }}`,
` PROMOTION_STAGING_INO: \${{ steps.${step.finalizerStepId}.outputs.staging_ino }}`,
" run: |",
' if [ -n "$PROMOTION_STAGING_ROOT" ] && [ -n "$PROMOTION_CLEANUP_TOKEN" ]; then',
' if [ -n "$PROMOTION_STAGING_ROOT" ] && [ -n "$PROMOTION_CLEANUP_TOKEN" ] && [ -n "$PROMOTION_RUNNER_TEMP_DEV" ] && [ -n "$PROMOTION_RUNNER_TEMP_INO" ] && [ -n "$PROMOTION_STAGING_DEV" ] && [ -n "$PROMOTION_STAGING_INO" ]; then',
" node scripts/cleanup-verified-promotion.ts",
" fi",
];
@@ -296,6 +294,7 @@ function requiredStepActionUses(stepKind: string): string {
function renderCondition(condition: CiWorkflowJob["condition"]): string | null {
const expressions: Record<CiWorkflowJob["condition"], string | null> = {
always: null,
"needs-success": null,
merge: "${{ gitea.event_name != 'workflow_dispatch' || inputs.stage != 'documentation' }}",
release: "${{ startsWith(gitea.ref, 'refs/tags/v') || (gitea.event_name == 'workflow_dispatch' && (inputs.stage == 'release' || inputs.stage == 'production' || inputs.stage == 'field')) }}",
production: "${{ gitea.event_name == 'workflow_dispatch' && (inputs.stage == 'production' || inputs.stage == 'field') }}",
@@ -412,6 +411,7 @@ export function createCiWorkflowGenerator(
let failure: unknown;
try {
await handle.writeFile(expected.toString("utf8"), "utf8");
await handle.chmod(0o644);
await handle.sync();
} catch (error) {
failure = error;
+2 -2
View File
@@ -7,7 +7,7 @@ import {
} from "node:fs/promises";
import {
bundlePerformanceArtifactSchema,
bundleOutputInventoryArtifactSchema,
buildManifestArtifactSchema,
dependencyDiffArtifactSchema,
dependencyInventoryArtifactSchema,
@@ -327,7 +327,7 @@ await mkdir("artifacts/release", { recursive: true });
await mkdir("artifacts/security", { recursive: true });
await writeValidatedJsonArtifact({
path: "artifacts/performance/bundle.json",
schema: bundlePerformanceArtifactSchema,
schema: bundleOutputInventoryArtifactSchema,
value: bundleReport,
});
await writeValidatedJsonArtifact({
+298 -11
View File
@@ -10,21 +10,37 @@ import type {
} from "../contracts/ci-gates.ts";
import { ciContractReportSchema } from "./ci-contract-report.ts";
import {
architectureDependencyReportArtifactSchema,
automatedA11yArtifactSchema,
buildManifestArtifactSchema,
bundlePerformanceArtifactSchema,
compatibilityFixturesArtifactSchema,
dependencyDiffArtifactSchema,
dependencyInventoryArtifactSchema,
designSystemReportArtifactSchema,
diagnosticsReportArtifactSchema,
documentationReviewArtifactSchema,
fieldWebVitalsArtifactSchema,
hostingHeadersArtifactSchema,
i18nReportArtifactSchema,
jsonSchemaDocumentArtifactSchema,
labPerformanceArtifactSchema,
licenseReportArtifactSchema,
manualA11yReportArtifactSchema,
moduleInventoryArtifactSchema,
optionalRecipeFixturesArtifactSchema,
optionalRecipesArtifactSchema,
provenanceArtifactSchema,
realtimeBoundariesArtifactSchema,
registryGovernanceRunArtifactSchema,
registryCompatibilityFixturesArtifactSchema,
registrySnapshotArtifactSchema,
releaseVerificationArtifactSchema,
reproducibleBuildArtifactSchema,
runbookRecordArtifactSchema,
sbomArtifactSchema,
supplyChainFixturesArtifactSchema,
supplyChainProviderFixturesArtifactSchema,
supplyChainVerificationArtifactSchema,
vulnerabilityReportArtifactSchema,
} from "../contracts/release-artifacts.ts";
@@ -40,10 +56,6 @@ import { normalizeRepositoryRelativePath } from "./repository-file-inventory.ts"
import { secretScanSarifSchema } from "./secret-scan-evaluator.ts";
import { testEvidenceReportSchema } from "./test-evidence-artifact.ts";
const jsonObjectSchema = z.record(z.string(), z.json()).refine(
(value) => Object.keys(value).length > 0,
"generic JSON artifact must be a non-empty object",
);
const coverageCounterSchema = z
.object({
total: z.number().int().nonnegative(),
@@ -184,7 +196,22 @@ type ExecutableJsonSchemaId = Extract<
>["executableSchemaId"];
const executableJsonSchemas: Readonly<Record<ExecutableJsonSchemaId, ZodType>> = Object.freeze({
"generic-json-object": jsonObjectSchema,
"automated-a11y": automatedA11yArtifactSchema,
"manual-a11y": manualA11yReportArtifactSchema,
"architecture-dependency-report": architectureDependencyReportArtifactSchema,
"design-system-contract": designSystemReportArtifactSchema,
"i18n-contract": i18nReportArtifactSchema,
"diagnostics-contract": diagnosticsReportArtifactSchema,
"realtime-boundaries": realtimeBoundariesArtifactSchema,
"optional-recipes": optionalRecipesArtifactSchema,
"optional-recipe-fixtures": optionalRecipeFixturesArtifactSchema,
"registry-compatibility-fixtures": registryCompatibilityFixturesArtifactSchema,
"reproducible-build": reproducibleBuildArtifactSchema,
"supply-chain-fixtures": supplyChainFixturesArtifactSchema,
"supply-chain-provider-fixtures": supplyChainProviderFixturesArtifactSchema,
"compatibility-fixtures": compatibilityFixturesArtifactSchema,
"documentation-review": documentationReviewArtifactSchema,
"hosting-headers": hostingHeadersArtifactSchema,
"coverage-summary-v8": coverageSummarySchema,
"risk-coverage-v3": riskCoverageArtifactSchema,
"build-manifest": buildManifestArtifactSchema,
@@ -213,6 +240,12 @@ const executableJsonSchemas: Readonly<Record<ExecutableJsonSchemaId, ZodType>> =
"ci-contract-report": ciContractReportSchema,
});
export function hasCiArtifactSemanticValidator(
schema: CiGateArtifactSchema,
): boolean {
return schema.kind !== "json" || schema.executableSchemaId in executableJsonSchemas;
}
type ReadHandle = Readonly<{
stat(): Promise<Stats>;
read(
@@ -253,9 +286,7 @@ export async function validateCiArtifact(
if (!/^#|\[[^\]]+\]|\S/u.test(text)) throw new TypeError(`invalid Markdown artifact: ${relative}`);
return;
case "html":
if (!/^\s*(?:<!doctype\s+html\s*>\s*)?<html\b[^>]*>[\s\S]*<\/html\s*>\s*$/iu.test(text)) {
throw new TypeError(`invalid HTML artifact: ${relative}`);
}
assertWellFormedHtml(text, relative);
return;
case "junit":
assertWellFormedJUnitXml(text, relative);
@@ -355,51 +386,85 @@ async function readHandleBounded(
return captured.subarray(0, offset);
}
const MAX_DOCUMENT_DEPTH = 256;
const MAX_DOCUMENT_UNITS = 100_000;
function assertWellFormedJUnitXml(source: string, relative: string): void {
const invalid = () => new TypeError(`invalid JUnit artifact: ${relative}`);
if (/<!DOCTYPE\b|<!ENTITY\b/iu.test(source)) throw invalid();
if (!hasOnlyXmlCharacters(source)) throw invalid();
const stack: string[] = [];
let root: string | undefined;
let rootClosed = false;
let declarationSeen = false;
let units = 0;
let cursor = 0;
while (cursor < source.length) {
const open = source.indexOf("<", cursor);
const text = source.slice(cursor, open < 0 ? source.length : open);
if (stack.length === 0 && text.trim()) throw invalid();
if ((text.includes("]]>") || !hasValidXmlEntities(text)) && text.length > 0) throw invalid();
if (text.length > 0 && ++units > MAX_DOCUMENT_UNITS) throw invalid();
if (open < 0) break;
if (source.startsWith("<!--", open)) {
const close = source.indexOf("-->", open + 4);
if (close < 0 || source.slice(open + 4, close).includes("--")) throw invalid();
if (++units > MAX_DOCUMENT_UNITS) throw invalid();
cursor = close + 3;
continue;
}
if (source.startsWith("<![CDATA[", open)) {
const close = source.indexOf("]]>", open + 9);
if (stack.length === 0 || close < 0) throw invalid();
if (++units > MAX_DOCUMENT_UNITS) throw invalid();
cursor = close + 3;
continue;
}
if (source.startsWith("<?", open)) {
const close = source.indexOf("?>", open + 2);
if (root || close < 0) throw invalid();
const processingInstruction = source.slice(open, close + 2);
const match = /^<\?([A-Za-z_][\w:.-]*)(?:\s+[\s\S]*?)?\?>$/u.exec(
processingInstruction,
);
if (!match) throw invalid();
if (match[1]!.toLowerCase() === "xml") {
if (
declarationSeen ||
source.slice(0, open).trim() ||
!/^<\?xml\s+version\s*=\s*(["'])1\.0\1(?:\s+encoding\s*=\s*(["'])UTF-8\2)?\s*\?>$/u.test(
processingInstruction,
)
) {
throw invalid();
}
declarationSeen = true;
}
if (++units > MAX_DOCUMENT_UNITS) throw invalid();
cursor = close + 2;
continue;
}
const close = source.indexOf(">", open + 1);
if (source.startsWith("<!", open)) throw invalid();
const close = markupEnd(source, open + 1);
if (close < 0) throw invalid();
const tag = source.slice(open, close + 1);
const closing = /^<\/([A-Za-z_][\w:.-]*)\s*>$/u.exec(tag);
if (closing) {
if (stack.pop() !== closing[1]) throw invalid();
if (stack.length === 0) rootClosed = true;
if (++units > MAX_DOCUMENT_UNITS) throw invalid();
cursor = close + 1;
continue;
}
const opening = /^<([A-Za-z_][\w:.-]*)([\s\S]*?)(\/?)>$/u.exec(tag);
if (!opening || rootClosed || !hasValidXmlAttributes(opening[2] ?? "")) throw invalid();
root ??= opening[1];
if (opening[3] !== "/") stack.push(opening[1]!);
if (opening[3] !== "/") {
if (stack.length >= MAX_DOCUMENT_DEPTH) throw invalid();
stack.push(opening[1]!);
}
else if (stack.length === 0) rootClosed = true;
if (++units > MAX_DOCUMENT_UNITS) throw invalid();
cursor = close + 1;
}
if (stack.length > 0 || !rootClosed || (root !== "testsuite" && root !== "testsuites")) {
@@ -412,14 +477,236 @@ function hasValidXmlAttributes(source: string): boolean {
const names = new Set<string>();
while (remaining.length > 0) {
if (!remaining.trim()) return true;
const match = /^\s+([A-Za-z_:][\w:.-]*)\s*=\s*(?:"[^"<]*"|'[^'<]*')/u.exec(remaining);
const match = /^\s+([A-Za-z_:][\w:.-]*)\s*=\s*(?:"([^"<]*)"|'([^'<]*)')/u.exec(remaining);
if (!match || names.has(match[1]!)) return false;
if (!hasValidXmlEntities(match[2] ?? match[3] ?? "")) return false;
names.add(match[1]!);
remaining = remaining.slice(match[0].length);
}
return true;
}
function hasOnlyXmlCharacters(source: string): boolean {
for (const character of source) {
const codePoint = character.codePointAt(0)!;
if (
codePoint !== 0x09 &&
codePoint !== 0x0a &&
codePoint !== 0x0d &&
(codePoint < 0x20 ||
(codePoint > 0xd7ff && codePoint < 0xe000) ||
(codePoint > 0xfffd && codePoint < 0x10000) ||
codePoint > 0x10ffff)
) {
return false;
}
}
return true;
}
function hasValidXmlEntities(source: string): boolean {
let cursor = 0;
while (cursor < source.length) {
const ampersand = source.indexOf("&", cursor);
if (ampersand < 0) return true;
const semicolon = source.indexOf(";", ampersand + 1);
if (semicolon < 0) return false;
const entity = source.slice(ampersand + 1, semicolon);
if (!["amp", "lt", "gt", "apos", "quot"].includes(entity)) {
const decimal = /^#([0-9]+)$/u.exec(entity);
const hexadecimal = /^#x([a-fA-F0-9]+)$/u.exec(entity);
if (!decimal && !hexadecimal) return false;
const codePoint = Number.parseInt((decimal ?? hexadecimal)![1]!, decimal ? 10 : 16);
if (
!Number.isSafeInteger(codePoint) ||
(codePoint !== 0x09 &&
codePoint !== 0x0a &&
codePoint !== 0x0d &&
(codePoint < 0x20 ||
(codePoint > 0xd7ff && codePoint < 0xe000) ||
(codePoint > 0xfffd && codePoint < 0x10000) ||
codePoint > 0x10ffff))
) {
return false;
}
}
cursor = semicolon + 1;
}
return true;
}
const HTML_VOID_ELEMENTS = new Set([
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"param",
"source",
"track",
"wbr",
]);
const HTML_RAW_TEXT_ELEMENTS = new Set(["script", "style", "textarea", "title"]);
function assertWellFormedHtml(source: string, relative: string): void {
const invalid = () => new TypeError(`invalid HTML artifact: ${relative}`);
if (/<!ENTITY\b|<!DOCTYPE\s+html\s+[^>]*\[/iu.test(source)) throw invalid();
const stack: string[] = [];
let cursor = 0;
let units = 0;
let doctypeSeen = false;
let rootSeen = false;
let rootClosed = false;
let playwrightPayloadSeen = false;
while (cursor < source.length) {
const rawElement = stack.at(-1);
if (rawElement && HTML_RAW_TEXT_ELEMENTS.has(rawElement)) {
const closingStart = source.toLowerCase().indexOf(`</${rawElement}`, cursor);
if (closingStart < 0) throw invalid();
if (++units > MAX_DOCUMENT_UNITS) throw invalid();
cursor = closingStart;
}
const open = source.indexOf("<", cursor);
const text = source.slice(cursor, open < 0 ? source.length : open);
if (stack.length === 0 && text.trim()) throw invalid();
if (text.length > 0 && ++units > MAX_DOCUMENT_UNITS) throw invalid();
if (open < 0) break;
if (source.startsWith("<!--", open)) {
const close = source.indexOf("-->", open + 4);
if (close < 0 || source.slice(open + 4, close).includes("--")) throw invalid();
if (++units > MAX_DOCUMENT_UNITS) throw invalid();
cursor = close + 3;
continue;
}
const declarationEnd = source.indexOf(">", open + 2);
if (source.slice(open, open + 9).toLowerCase() === "<!doctype") {
if (
declarationEnd < 0 ||
doctypeSeen ||
rootSeen ||
source.slice(open, declarationEnd + 1).toLowerCase() !== "<!doctype html>"
) {
throw invalid();
}
doctypeSeen = true;
if (++units > MAX_DOCUMENT_UNITS) throw invalid();
cursor = declarationEnd + 1;
continue;
}
if (source.startsWith("<!", open) || source.startsWith("<?", open)) throw invalid();
const close = markupEnd(source, open + 1);
if (close < 0) throw invalid();
const tag = source.slice(open, close + 1);
const closing = /^<\/([A-Za-z][A-Za-z0-9:-]*)\s*>$/u.exec(tag);
if (closing) {
const name = closing[1]!.toLowerCase();
if (stack.pop() !== name) throw invalid();
if (stack.length === 0) {
if (name === "html") rootClosed = true;
else if (name === "template" && playwrightPayloadSeen) {
// Playwright emits its base64 report template after </html>; HTML5
// reparents this token into the document body. It is the sole
// permitted generated-report sidecar and does not create a new root.
} else {
throw invalid();
}
}
if (++units > MAX_DOCUMENT_UNITS) throw invalid();
cursor = close + 1;
continue;
}
const opening = /^<([A-Za-z][A-Za-z0-9:-]*)([\s\S]*?)(\/?)>$/u.exec(tag);
if (!opening) throw invalid();
const name = opening[1]!.toLowerCase();
const attributes = parseHtmlAttributes(opening[2] ?? "");
if (!attributes || name.includes(":")) throw invalid();
if (!rootSeen) {
if (name !== "html" || opening[3] === "/") throw invalid();
rootSeen = true;
} else if (name === "html") {
throw invalid();
}
if (rootClosed && stack.length === 0) {
if (
playwrightPayloadSeen ||
name !== "template" ||
attributes.get("id") !== "playwrightReportBase64" ||
opening[3] === "/"
) {
throw invalid();
}
playwrightPayloadSeen = true;
}
if (!HTML_VOID_ELEMENTS.has(name) && opening[3] !== "/") {
if (stack.length >= MAX_DOCUMENT_DEPTH) throw invalid();
stack.push(name);
} else if (stack.length === 0 && name === "html") {
rootClosed = true;
}
if (++units > MAX_DOCUMENT_UNITS) throw invalid();
cursor = close + 1;
}
if (stack.length > 0 || !rootSeen || !rootClosed) throw invalid();
}
function markupEnd(source: string, start: number): number {
let quote: "\"" | "'" | undefined;
for (let index = start; index < source.length; index += 1) {
const character = source[index];
if (quote) {
if (character === quote) quote = undefined;
} else if (character === "\"" || character === "'") {
quote = character;
} else if (character === ">") {
return index;
}
}
return -1;
}
function parseHtmlAttributes(source: string): ReadonlyMap<string, string> | null {
const attributes = new Map<string, string>();
let cursor = 0;
while (cursor < source.length) {
const whitespace = /^\s+/u.exec(source.slice(cursor));
if (!whitespace) return source.slice(cursor).trim() ? null : attributes;
cursor += whitespace[0].length;
if (cursor >= source.length) return attributes;
const nameMatch = /^[A-Za-z_:][A-Za-z0-9:._-]*/u.exec(source.slice(cursor));
if (!nameMatch) return null;
const name = nameMatch[0].toLowerCase();
if (attributes.has(name) || name.includes(":")) return null;
cursor += nameMatch[0].length;
const spacing = /^\s*/u.exec(source.slice(cursor))![0];
cursor += spacing.length;
let value = "";
if (source[cursor] === "=") {
cursor += 1;
cursor += /^\s*/u.exec(source.slice(cursor))![0].length;
const quote = source[cursor];
if (quote === "\"" || quote === "'") {
const end = source.indexOf(quote, cursor + 1);
if (end < 0) return null;
value = source.slice(cursor + 1, end);
if (value.includes("<")) return null;
cursor = end + 1;
} else {
const unquoted = /^[^\s"'`=<>]+/u.exec(source.slice(cursor));
if (!unquoted) return null;
value = unquoted[0];
cursor += value.length;
}
}
attributes.set(name, value);
}
return attributes;
}
function assertSameIdentity(before: Stats, after: Stats, relative: string): void {
if (
!Number.isSafeInteger(before.dev) ||
+353
View File
@@ -0,0 +1,353 @@
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import path from "node:path";
import {
PROMOTED_FILE_NAMES,
type PromotedFileName,
} from "../contracts/promotion-artifacts.ts";
import {
PROMOTION_VERIFIER_ID,
PROMOTION_VERIFIER_VERSION,
assertDistinctProviderTrust,
evaluatePromotionEvidence,
providerVerificationArtifactSchema,
provenanceProviderAttestationSchema,
vulnerabilityProviderReportSchema,
trustPolicySha256,
type ProviderTrust,
} from "./provider-evidence.ts";
import { verifyCapturedCiCandidateArchive } from "./ci-candidate-archive.ts";
import { LOCAL_EVIDENCE_ASSESSMENT_PATH } from "./release-candidate.ts";
import { verifyArchivedLocalEvidence } from "./local-release-evidence.ts";
export type ExactPromotionBundle = Readonly<
Partial<Record<PromotedFileName, Buffer>>
>;
export type ExactPromotionExpectedContext = Readonly<{
run: Readonly<{ id: string; attempt: number }>;
sourceRevision: string;
archiveSha256: string;
sourceSetSha256?: string;
bundleSha256?: string;
distSha256?: string;
lockfileSha256?: string;
}>;
export async function verifyExactPromotionBundle(
files: ExactPromotionBundle,
options: Readonly<{
vulnerabilityTrust: ProviderTrust;
provenanceTrust: ProviderTrust;
expected: ExactPromotionExpectedContext;
nowEpochMs?: () => number;
}>,
): Promise<Readonly<{ status: "PASS" }>> {
assertDistinctProviderTrust(options);
assertExternalExpectedContext(options.expected);
const names = Object.keys(files).sort(asciiCompare);
const expectedNames = [...PROMOTED_FILE_NAMES].sort(asciiCompare);
if (JSON.stringify(names) !== JSON.stringify(expectedNames)) {
throw new Error("promotion bundle must contain the exact five canonical files");
}
for (const name of PROMOTED_FILE_NAMES) {
if (!Buffer.isBuffer(files[name])) {
throw new TypeError(`promotion bundle file is missing or not captured bytes: ${name}`);
}
}
const archiveBytes = files["release-candidate.tar.gz"]!;
const vulnerabilityBytes = files["vulnerability-report.json"]!;
const provenanceBytes = files["provenance-attestation.json"]!;
const providerBytes = files["provider-verification.json"]!;
const promotionBytes = files["promotion-verification.json"]!;
const vulnerability = vulnerabilityProviderReportSchema.parse(parseJson(
vulnerabilityBytes,
"vulnerability report",
));
const provenance = provenanceProviderAttestationSchema.parse(parseJson(
provenanceBytes,
"provenance attestation",
));
const provider = providerVerificationArtifactSchema.parse(parseJson(
providerBytes,
"provider verification",
));
const promotion = providerVerificationArtifactSchema.parse(parseJson(
promotionBytes,
"promotion verification",
));
if (
provider.artifactType !== "provider-verification" ||
promotion.artifactType !== "promotion-verification"
) {
throw new Error("promotion verification artifact role mismatch");
}
for (const [label, record] of [
["provider", provider],
["promotion", promotion],
] as const) {
if (
record.verifier.id !== PROMOTION_VERIFIER_ID ||
record.verifier.version !== PROMOTION_VERIFIER_VERSION
) {
throw new Error(`${label} verification literal verifier identity mismatch`);
}
if (record.status !== "PASS" || record.failures.length !== 0) {
throw new Error(`${label} verification must be PASS without failures`);
}
}
if (
provider.vulnerabilityStatus !== "PASS" ||
provider.provenanceAttestationStatus !== "PASS"
) {
throw new Error("provider verification subordinate statuses must both be PASS");
}
if (promotion.localEvidenceStatus !== "PASS") {
throw new Error("promotion local evidence subordinate status must be PASS");
}
assertEqual("shared verifiedAt", provider.verifiedAt, promotion.verifiedAt);
assertEqual("shared run", provider.run, promotion.run);
assertEqual("shared source", provider.source, promotion.source);
assertEqual("shared candidate", provider.candidate, promotion.candidate);
assertEqual(
"shared provider evidence",
provider.providerEvidence,
promotion.providerEvidence,
);
assertEqual(
"shared trust policy",
provider.trustPolicySha256,
promotion.trustPolicySha256,
);
assertEqual("external expected run", provider.run, options.expected.run);
assertEqual(
"external expected source revision",
provider.source.revision,
options.expected.sourceRevision,
);
assertEqual(
"external expected archive digest",
provider.candidate.archiveSha256,
options.expected.archiveSha256,
);
for (const [label, actual, expected] of [
["source set", provider.source.sourceSetSha256, options.expected.sourceSetSha256],
["bundle", provider.candidate.bundleSha256, options.expected.bundleSha256],
["dist", provider.candidate.distSha256, options.expected.distSha256],
["lockfile", provider.candidate.lockfileSha256, options.expected.lockfileSha256],
] as const) {
if (expected !== undefined) {
assertEqual(`external expected ${label} digest`, actual, expected);
}
}
const anchoredTrustPolicySha256 = trustPolicySha256(options);
if (provider.trustPolicySha256 !== anchoredTrustPolicySha256) {
throw new Error("verification trust policy does not match anchored provider keys");
}
if (promotion.providerVerificationSha256 !== sha256(providerBytes)) {
throw new Error("promotion provider verification byte hash mismatch");
}
if (
provider.candidate.archiveSha256 !== sha256(archiveBytes) ||
provider.providerEvidence.vulnerabilityReportSha256 !== sha256(vulnerabilityBytes) ||
provider.providerEvidence.provenanceAttestationSha256 !== sha256(provenanceBytes)
) {
if (provider.candidate.archiveSha256 !== sha256(archiveBytes)) {
throw new Error("candidate archive actual digest mismatch");
}
if (
provider.providerEvidence.vulnerabilityReportSha256 !==
sha256(vulnerabilityBytes)
) {
throw new Error("vulnerability report actual digest mismatch");
}
throw new Error("provenance attestation actual digest mismatch");
}
for (const [label, evidence, nonce, keyId, fingerprint] of [
[
"vulnerability",
vulnerability,
provider.providerEvidence.vulnerabilityInvocationNonce,
provider.providerEvidence.vulnerabilityKeyId,
provider.providerEvidence.vulnerabilityKeyFingerprint,
],
[
"provenance",
provenance,
provider.providerEvidence.provenanceInvocationNonce,
provider.providerEvidence.provenanceKeyId,
provider.providerEvidence.provenanceKeyFingerprint,
],
] as const) {
assertEqual(`${label} run`, { id: evidence.run.id, attempt: evidence.run.attempt }, provider.run);
assertEqual(`${label} source`, evidence.source, provider.source);
assertEqual(`${label} candidate`, evidence.candidate, provider.candidate);
if (
evidence.run.invocationNonce !== nonce ||
evidence.signature.keyId !== keyId ||
evidence.signature.publicKeyFingerprint !== fingerprint
) {
throw new Error(`${label} provider evidence nonce or trust role mismatch`);
}
}
if (provenance.subject.digest.sha256 !== provider.candidate.distSha256) {
throw new Error("provenance subject dist digest mismatch");
}
if (vulnerability.findings.length !== 0) {
throw new Error("vulnerability report is not PASS");
}
assertEqual(
"signed secret scan attestation",
vulnerability.secretScanAttestation,
provider.providerEvidence.secretScanAttestation,
);
let assessmentSha256: string | null = null;
const localIdentityHolder: {
current: null | Readonly<{
sourceRevision: string;
sourceSetSha256: string;
assessmentSha256: string;
secretScan: Readonly<{
policySha256: string;
sarifSha256: string;
scanInputSha256: string;
}>;
}>;
} = { current: null };
await verifyCapturedCiCandidateArchive(
archiveBytes,
provider.candidate.archiveSha256,
{
verifyExtracted: async (extractionRoot, manifest) => {
assertEqual("archive candidate", {
bundleSha256: manifest.bundleSha256,
distSha256: manifest.distSha256,
lockfileSha256: manifest.lockfileSha256,
}, {
bundleSha256: provider.candidate.bundleSha256,
distSha256: provider.candidate.distSha256,
lockfileSha256: provider.candidate.lockfileSha256,
});
assessmentSha256 = sha256(
await readFile(path.join(extractionRoot, LOCAL_EVIDENCE_ASSESSMENT_PATH)),
);
const local = await verifyArchivedLocalEvidence({
extractionRoot,
expectedManifest: manifest,
});
if (local.status !== "PASS" || !local.identity) {
throw new Error(
`exact-five archived local verification is not PASS: ${local.failures.join(", ")}`,
);
}
localIdentityHolder.current = local.identity;
},
},
);
if (assessmentSha256 !== promotion.localEvidenceAssessmentSha256) {
throw new Error("promotion local evidence assessment actual digest mismatch");
}
if (
!localIdentityHolder.current ||
localIdentityHolder.current.sourceRevision !== provider.source.revision ||
localIdentityHolder.current.sourceSetSha256 !== provider.source.sourceSetSha256 ||
localIdentityHolder.current.assessmentSha256 !== promotion.localEvidenceAssessmentSha256
) {
throw new Error("exact-five archived local identity mismatch");
}
assertEqual("archived secret scan attestation", {
status: "PASS",
localEvidenceAssessmentSha256: localIdentityHolder.current.assessmentSha256,
sourceSetSha256: localIdentityHolder.current.sourceSetSha256,
policySha256: localIdentityHolder.current.secretScan.policySha256,
sarifSha256: localIdentityHolder.current.secretScan.sarifSha256,
scanInputSha256: localIdentityHolder.current.secretScan.scanInputSha256,
}, vulnerability.secretScanAttestation);
const reevaluated = evaluatePromotionEvidence({
expected: {
run: provider.run,
source: provider.source,
candidate: provider.candidate,
vulnerabilityInvocationNonce:
provider.providerEvidence.vulnerabilityInvocationNonce,
provenanceInvocationNonce:
provider.providerEvidence.provenanceInvocationNonce,
secretScanAttestation: provider.providerEvidence.secretScanAttestation,
},
localStatus: "PASS",
vulnerabilityReport: vulnerability,
provenanceAttestation: provenance,
vulnerabilityTrust: options.vulnerabilityTrust,
provenanceTrust: options.provenanceTrust,
nowEpochMs: options.nowEpochMs,
});
if (
reevaluated.status !== "PASS" ||
reevaluated.vulnerabilityStatus !== "PASS" ||
reevaluated.provenanceAttestationStatus !== "PASS"
) {
throw new Error(
`exact-five provider signature/freshness revalidation is not PASS: ${reevaluated.failures.join(", ")}`,
);
}
return Object.freeze({ status: "PASS" as const });
}
function assertExternalExpectedContext(
expected: ExactPromotionExpectedContext,
): void {
if (
!expected ||
typeof expected.run?.id !== "string" ||
expected.run.id.length === 0 ||
!Number.isSafeInteger(expected.run.attempt) ||
expected.run.attempt < 1 ||
!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(expected.sourceRevision) ||
!isSha256(expected.archiveSha256)
) {
throw new TypeError("external expected promotion context is invalid or incomplete");
}
for (const digest of [
expected.sourceSetSha256,
expected.bundleSha256,
expected.distSha256,
expected.lockfileSha256,
]) {
if (digest !== undefined && !isSha256(digest)) {
throw new TypeError("external optional expected promotion digest is invalid");
}
}
}
function isSha256(value: unknown): value is string {
return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value);
}
function parseJson(bytes: Buffer, label: string): unknown {
try {
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) as unknown;
} catch {
throw new TypeError(`${label} is not strict UTF-8 JSON`);
}
}
function assertEqual(label: string, left: unknown, right: unknown): void {
if (JSON.stringify(left) !== JSON.stringify(right)) {
throw new Error(`${label} mismatch`);
}
}
function sha256(bytes: Buffer): string {
return createHash("sha256").update(bytes).digest("hex");
}
function asciiCompare(left: string, right: string): number {
return left < right ? -1 : left > right ? 1 : 0;
}
+200 -35
View File
@@ -31,6 +31,8 @@ import {
import { assertMatchesJsonSchema } from "./json-schema.ts";
import {
LOCAL_EVIDENCE_ASSESSMENT_PATH,
LOCAL_EVIDENCE_POLICY_INPUT_PATHS,
LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS,
RELEASE_CANDIDATE_EVIDENCE_PATHS,
RELEASE_CANDIDATE_MANIFEST_PATH,
collectDistOutputs,
@@ -54,9 +56,12 @@ import {
parseRepositoryFileInventoryPolicy,
} from "./repository-file-inventory.ts";
import {
parseSecretScanPolicy,
secretScanSarifSchema,
evaluateRepositorySecretScan,
verifyStoredSecretScan,
} from "./secret-scan-evaluator.ts";
import { secretScanRules } from "./secret-scan.ts";
import {
isValidSha512Integrity,
parsePnpmLockfilePackages,
@@ -301,37 +306,10 @@ export async function verifyLocalSupplyChainEvidence(
export const LOCAL_EVIDENCE_VERIFIER_ID =
"clean-architecture-frontend-template/local-evidence-verifier";
export const LOCAL_EVIDENCE_VERIFIER_VERSION = "1";
export const LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS = Object.freeze([
"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",
] as const);
export const LOCAL_EVIDENCE_POLICY_INPUT_PATHS = Object.freeze([
"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",
...LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS,
] as const);
export {
LOCAL_EVIDENCE_POLICY_INPUT_PATHS,
LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS,
} from "./release-candidate.ts";
export async function createLocalEvidenceAssessment(
repositoryRoot = process.cwd(),
@@ -360,7 +338,7 @@ export async function createLocalEvidenceAssessment(
files: evidenceInputs,
};
const evaluated = await evaluateProducerLocalChecks(root, candidate);
const [build, release, provenance, supply, sbomDocument, policyInputs] = await Promise.all([
const [build, release, provenance, supply, sbomDocument, policyInputs, secretScan] = await Promise.all([
readJson(root, "artifacts/release/build-manifest.json").then((value) =>
buildManifestArtifactSchema.parse(value),
),
@@ -381,6 +359,7 @@ export async function createLocalEvidenceAssessment(
digestInput(root, policyPath),
),
),
evaluateRepositorySecretScan({ repositoryRoot: root }),
]);
const identityFailures: string[] = [];
if (build.commitSha !== release.commitSha) {
@@ -421,6 +400,15 @@ export async function createLocalEvidenceAssessment(
if (verifierSources.length !== LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS.length) {
throw new Error("local assessment verifier source set is incomplete");
}
const secretPolicy = policyInputs.find(
({ path: policyPath }) => policyPath === "config/security/secret-scan-policy.json",
);
const secretSarif = evidenceInputs.find(
({ path: evidencePath }) => evidencePath === "artifacts/security/scan.sarif",
);
if (!secretPolicy || !secretSarif) {
throw new Error("local assessment secret scan inputs are incomplete");
}
return localEvidenceAssessmentArtifactSchema.parse({
schemaVersion: 1,
artifactType: "local-evidence-assessment",
@@ -440,6 +428,11 @@ export async function createLocalEvidenceAssessment(
lockfileSha256: candidate.lockfileSha256,
sbomSha256: sbom.sha256,
},
secretScan: {
policySha256: secretPolicy.sha256,
sarifSha256: secretSarif.sha256,
scanInputSha256: secretScan.scanInputSha256,
},
policyInputs,
evidenceInputs,
checks,
@@ -458,6 +451,7 @@ type LocalCheckName =
async function evaluateProducerLocalChecks(
root: string,
candidate: ReleaseCandidateManifest,
options: Readonly<{ archived?: boolean }> = {},
): Promise<Readonly<{
checks: Readonly<Record<LocalCheckName, "PASS" | "FAIL">>;
failures: readonly string[];
@@ -490,13 +484,16 @@ async function evaluateProducerLocalChecks(
};
await evaluate("release", async () => {
const [build, release, stored] = await Promise.all([
const [build, release, runtime, stored] = await Promise.all([
readJson(root, "artifacts/release/build-manifest.json").then((value) =>
buildManifestArtifactSchema.parse(value),
),
readJson(root, "dist/release-manifest.json").then((value) =>
releaseManifestArtifactSchema.parse(value),
),
readJson(root, "dist/config.json").then((value) =>
runtimeConfigArtifactSchema.parse(value),
),
readJson(root, "artifacts/release/verification.json").then((value) =>
releaseVerificationArtifactSchema.parse(value),
),
@@ -522,30 +519,104 @@ async function evaluateProducerLocalChecks(
) {
diagnostics.push("stored release verification is not a coherent PASS");
}
if (!runtime.BUILD_ID || !runtime.RELEASE_ID) {
diagnostics.push("runtime release identity is missing");
} else {
const apiContractVersion =
release.schemaVersion === 1 && "API_CONTRACT_VERSION" in runtime
? runtime.API_CONTRACT_VERSION
: undefined;
const coherence = await verifyReleaseRuntimeCoherence({
release,
runtime: {
BUILD_ID: runtime.BUILD_ID,
RELEASE_ID: runtime.RELEASE_ID,
CONFIG_SCHEMA_VERSION: runtime.CONFIG_SCHEMA_VERSION,
...(apiContractVersion === undefined
? {}
: { API_CONTRACT_VERSION: apiContractVersion }),
},
contractPackages: EXPECTED_CONTRACT_SET_PACKAGES,
});
diagnostics.push(...coherence.mismatches.map((item) => `runtime:${item}`));
}
diagnostics.push(...(await verifyBuildManifestOutputs(build, { repositoryRoot: root })));
return diagnostics;
});
await evaluate("supplyChain", async () => {
const [supply, coherence] = await Promise.all([
const [inventory, sbom, provenance, supply, coherence, lockfileBytes] = await Promise.all([
readJson(root, "artifacts/release/dependency-inventory.json").then((value) =>
dependencyInventoryArtifactSchema.parse(value),
),
readJson(root, "artifacts/release/sbom.cdx.json").then((value) =>
sbomArtifactSchema.parse(value),
),
readJson(root, "artifacts/release/provenance.json").then((value) =>
provenanceArtifactSchema.parse(value),
),
readJson(root, "artifacts/security/supply-chain-verification.json").then((value) =>
supplyChainVerificationArtifactSchema.parse(value),
),
readJson(root, "artifacts/security/supply-chain-coherence.json").then((value) =>
supplyChainCoherenceReportSchema.parse(value),
),
readFile(path.join(root, "pnpm-lock.yaml")),
]);
const diagnostics: string[] = [];
const lockfileSha256 = createHash("sha256").update(lockfileBytes).digest("hex");
const outputs = await collectDistOutputs(root);
const currentDistSha256 = distSha256(outputs);
const sbomSha256 = supplyChainDigest(sbom);
const independentlyCoherent = verifySupplyChainCoherence(
sbom,
inventory,
provenance,
currentDistSha256,
);
if (
supply.localStatus !== "PASS" ||
supply.failures.length > 0 ||
supply.distSha256 !== candidate.distSha256 ||
supply.lockfileSha256 !== candidate.lockfileSha256 ||
supply.sbomSha256 !== sbomSha256 ||
supply.sourceSetSha256 !== provenance.predicate.materials.sourceSetSha256 ||
inventory.lockfileSha256 !== lockfileSha256 ||
candidate.lockfileSha256 !== lockfileSha256 ||
candidate.distSha256 !== currentDistSha256 ||
coherence.status !== "PASS" ||
coherence.failures.length > 0 ||
coherence.distSha256 !== candidate.distSha256 ||
coherence.lockfileSha256 !== candidate.lockfileSha256
coherence.lockfileSha256 !== candidate.lockfileSha256 ||
coherence.sbomSha256 !== sbomSha256 ||
coherence.dependencyCount !== inventory.dependencies.length ||
independentlyCoherent.failures.length > 0
) {
diagnostics.push("stored supply-chain evidence is not a coherent PASS");
}
diagnostics.push(...verifyLocalSupplyChainDefaults(supply));
diagnostics.push(
...verifyStoredDistChecksums(
outputs,
await readFile(path.join(root, "artifacts/release/checksums.txt"), "utf8"),
),
);
const lockRows = parsePnpmLockfilePackages(lockfileBytes.toString("utf8"));
const inventoryByIdentity = new Map(
inventory.dependencies.map((entry) => [`${entry.name}@${entry.version}`, entry] as const),
);
if (lockRows.length !== inventory.dependencies.length) {
diagnostics.push("transitive dependency count differs from lockfile");
}
for (const lockRow of lockRows) {
const dependency = inventoryByIdentity.get(`${lockRow.name}@${lockRow.version}`);
if (
!dependency ||
dependency.integrity !== lockRow.integrity ||
!isValidSha512Integrity(lockRow.integrity)
) {
diagnostics.push(`lockfile inventory integrity mismatch: ${lockRow.name}@${lockRow.version}`);
}
}
return diagnostics;
});
await evaluate("dependencyPolicy", async () => {
@@ -596,6 +667,25 @@ async function evaluateProducerLocalChecks(
);
});
await evaluate("secretScan", async () => {
if (options.archived) {
const policy = parseSecretScanPolicy(
await readJson(root, "config/security/secret-scan-policy.json"),
);
const sarif = secretScanSarifSchema.parse(
await readJson(root, "artifacts/security/scan.sarif"),
);
const diagnostics: string[] = [];
if (
policy.trackedRoots.length === 0 ||
policy.generatedRoots.length === 0 ||
sarif.runs[0]!.results.length > 0 ||
JSON.stringify(sarif.runs[0]!.tool.driver.rules.map(({ id }) => id)) !==
JSON.stringify(secretScanRules().map(({ id }) => id))
) {
diagnostics.push("archived secret scan is not an independently valid PASS");
}
return diagnostics;
}
const evaluation = await evaluateRepositorySecretScan({ repositoryRoot: root });
return verifyStoredSecretScan(
evaluation,
@@ -640,6 +730,11 @@ export async function verifyArchivedLocalEvidence(input: Readonly<{
sourceRevision: string;
sourceSetSha256: string;
assessmentSha256: string;
secretScan: Readonly<{
policySha256: string;
sarifSha256: string;
scanInputSha256: string;
}>;
}>;
failures: readonly string[];
}>> {
@@ -696,6 +791,25 @@ export async function verifyArchivedLocalEvidence(input: Readonly<{
if (JSON.stringify(policyPaths) !== JSON.stringify(LOCAL_EVIDENCE_POLICY_INPUT_PATHS)) {
failures.push("local assessment policyInputs exact set mismatch");
}
for (const policyInput of assessment.policyInputs) {
try {
const bytes = await readFile(path.join(extractionRoot, policyInput.path));
const member = extractedManifest.files.find(
({ path: memberPath }) => memberPath === policyInput.path,
);
if (
!member ||
member.bytes !== policyInput.bytes ||
member.sha256 !== policyInput.sha256 ||
bytes.byteLength !== policyInput.bytes ||
createHash("sha256").update(bytes).digest("hex") !== policyInput.sha256
) {
failures.push(`archived policy input binding mismatch: ${policyInput.path}`);
}
} catch {
failures.push(`archived policy input is missing or invalid: ${policyInput.path}`);
}
}
const verifierSources = assessment.policyInputs.filter(({ path: policyPath }) =>
(LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS as readonly string[]).includes(policyPath),
);
@@ -715,6 +829,12 @@ export async function verifyArchivedLocalEvidence(input: Readonly<{
const sbom = extractedManifest.files.find(
({ path: memberPath }) => memberPath === "artifacts/release/sbom.cdx.json",
);
const secretPolicy = extractedManifest.files.find(
({ path: memberPath }) => memberPath === "config/security/secret-scan-policy.json",
);
const secretSarif = extractedManifest.files.find(
({ path: memberPath }) => memberPath === "artifacts/security/scan.sarif",
);
if (
assessment.candidate.distSha256 !== extractedManifest.distSha256 ||
assessment.candidate.lockfileSha256 !== extractedManifest.lockfileSha256 ||
@@ -723,10 +843,54 @@ export async function verifyArchivedLocalEvidence(input: Readonly<{
) {
failures.push("local assessment candidate digest binding mismatch");
}
if (
!secretPolicy ||
!secretSarif ||
assessment.secretScan.policySha256 !== secretPolicy.sha256 ||
assessment.secretScan.sarifSha256 !== secretSarif.sha256
) {
failures.push("local assessment secret scan artifact binding mismatch");
}
if (assessment.status !== "PASS" || Object.values(assessment.checks).includes("FAIL")) {
failures.push("local evidence assessment is not PASS");
}
try {
const [supply, coherence] = await Promise.all([
readJson(extractionRoot, "artifacts/security/supply-chain-verification.json").then(
(value) => supplyChainVerificationArtifactSchema.parse(value),
),
readJson(extractionRoot, "artifacts/security/supply-chain-coherence.json").then(
(value) => supplyChainCoherenceReportSchema.parse(value),
),
]);
if (
supply.localStatus !== "PASS" ||
supply.failures.length > 0 ||
coherence.status !== "PASS" ||
coherence.failures.length > 0
) {
failures.push("archived supply-chain subordinate evidence is not PASS");
}
} catch {
failures.push("archived supply-chain subordinate evidence is missing or invalid");
}
const independent = await evaluateProducerLocalChecks(
extractionRoot,
extractedManifest,
{ archived: true },
);
if (
independent.failures.length > 0 ||
JSON.stringify(independent.checks) !== JSON.stringify(assessment.checks)
) {
failures.push(
...independent.failures.map((failure) => `archived local check:${failure}`),
);
failures.push("archived local checks do not independently reproduce assessment PASS");
}
const identities = await readArchivedIdentities(extractionRoot, failures);
if (
identities.buildRevision !== assessment.source.revision ||
@@ -751,6 +915,7 @@ export async function verifyArchivedLocalEvidence(input: Readonly<{
sourceRevision: passingAssessment.source.revision,
sourceSetSha256: passingAssessment.source.sourceSetSha256,
assessmentSha256,
secretScan: passingAssessment.secretScan,
})
: null,
failures: uniqueFailures,
+956 -16
View File
@@ -1,5 +1,138 @@
const packageScriptInvocation = /\b(?:(?:corepack\s+)?pnpm(?:\s+--?[A-Za-z][A-Za-z-]*(?:=[^\s;&|]+)?)*(?:\s+run)?|npm\s+run)\s+([A-Za-z0-9:_-]+)/gu;
const pnpmNonScriptCommands = new Set(["dlx", "exec", "install"]);
type PackageManager = "pnpm" | "npm" | "yarn";
type ManagerParseResult = Readonly<{
dependencies: readonly string[];
unsafeLifecycle: boolean;
unsupportedManagerSyntax: boolean;
}>;
type SuppressionState = {
effective: boolean | undefined;
contradictory: boolean;
malformed: boolean;
};
type ShellNpmScopeEnvironmentState = {
autoExport: boolean;
forbidden: boolean;
uncertain: boolean;
};
type ShellCommandPrefix = Readonly<{
assignments: readonly Readonly<{
dynamicName: boolean;
name: string | null;
}>[];
commandIndex: number;
uncertain: boolean;
}>;
type EnvironmentCommandPrefix = Readonly<{
assignmentNames: readonly string[];
uncertain: boolean;
}>;
type TokenizedShellSegment = Readonly<{
tokens: readonly string[];
expansionTokens: readonly boolean[];
}>;
const managerNames = new Set<PackageManager>(["pnpm", "npm", "yarn"]);
const managerOptionsWithValue = new Set([
"-C", "--cache", "--cache-folder", "--config-dir", "--cwd", "--dir", "--filter",
"--global-dir", "--globalconfig", "--home", "--lockfile-dir", "--mutex", "--prefix",
"--registry", "--store-dir", "--userconfig", "--workspace", "--workspace-dir",
]);
const managerBooleanOptions = new Set([
"--color", "--global", "--no-color", "--offline", "--prefer-offline", "--silent",
"--use-stderr", "--verbose", "-g", "-s",
]);
const npmScriptDispatchBooleanOptions = new Set([
"--foreground-scripts", "--if-present", "--ignore-scripts",
]);
const npmScriptDispatchScopeOptions = new Set([
"--prefix", "--workspace", "--workspaces",
]);
const npmDispatchScopeEnvironmentNames = new Set([
"npm_config_globalconfig", "npm_config_prefix", "npm_config_userconfig",
"npm_config_workspace", "npm_config_workspaces",
]);
const npmIndirectConfigAuthorityOptions = new Set([
"--globalconfig", "--userconfig",
]);
const manifestScopeOptions: Readonly<Record<PackageManager, ReadonlySet<string>>> = {
pnpm: new Set(["-C", "--dir", "--filter", "--workspace-dir"]),
npm: new Set(["--prefix", "--workspace"]),
yarn: new Set(["--cwd"]),
};
const lifecycleMutationCommands = new Set([
"add", "ci", "dedupe", "i", "install", "link", "pack", "prune", "publish",
"rebuild", "remove", "rm", "uninstall", "unlink", "up", "update", "upgrade",
]);
const managerBuiltinAliases: Readonly<
Record<PackageManager, ReadonlyMap<string, string>>
> = {
pnpm: new Map([["ln", "link"]]),
npm: new Map(),
yarn: new Map(),
};
const unsupportedBuiltinDispatchers: Readonly<
Record<PackageManager, ReadonlySet<string>>
> = {
pnpm: new Set(["dlx", "exec"]),
npm: new Set(["exec"]),
yarn: new Set(["dlx", "exec", "workspace", "workspaces"]),
};
const lifecycleBooleanOptions: Readonly<
Record<PackageManager, ReadonlySet<string>>
> = {
pnpm: new Set([
"--dry-run", "--force", "--frozen-lockfile", "--lockfile-only",
"--no-optional", "--prefer-frozen-lockfile", "--recursive",
"--workspace-root", "-D", "-P", "-r", "-w",
]),
npm: new Set([
"--audit", "--dry-run", "--force", "--foreground-scripts", "--fund",
"--package-lock-only",
]),
yarn: new Set([
"--check-cache", "--frozen-lockfile", "--ignore-engines",
"--ignore-optional", "--immutable", "--immutable-cache", "--inline-builds",
"--no-lockfile", "--non-interactive", "--pure-lockfile",
]),
};
const lifecycleOptionsWithValue: Readonly<
Record<PackageManager, ReadonlySet<string>>
> = {
pnpm: new Set(["--child-concurrency", "--modules-dir", "--reporter"]),
npm: new Set(["--include", "--install-strategy", "--omit"]),
yarn: new Set(["--mode", "--modules-folder", "--production"]),
};
const knownBuiltinCommands: Readonly<Record<PackageManager, ReadonlySet<string>>> = {
pnpm: new Set([
"audit", "config", "deploy", "dlx", "exec", "fetch", "help", "list", "ls",
"outdated", "root", "server", "setup", "store", "view", "why",
]),
npm: new Set([
"access", "audit", "bugs", "cache", "completion", "config", "diff", "docs",
"doctor", "exec", "explore", "fund", "help", "help-search", "hook", "init",
"list", "login", "logout", "ls", "org", "outdated", "owner", "ping", "pkg",
"prefix", "profile", "query", "repo", "root", "search", "star", "stars",
"team", "token", "unstar", "version", "view", "whoami",
]),
yarn: new Set([
"cache", "config", "constraints", "dedupe", "dlx", "exec", "help", "info",
"npm", "plugin", "set", "stage", "version", "why",
]),
};
const exactBareSafeBuiltinCommands: Readonly<
Record<PackageManager, ReadonlySet<string>>
> = {
pnpm: new Set(["audit"]),
npm: new Set(["audit"]),
yarn: new Set(),
};
const npmImplicitScripts = new Set(["restart", "start", "stop", "test"]);
export function validatePackageScriptGraph(
scripts: Readonly<Record<string, string>>,
@@ -27,21 +160,17 @@ export function validatePackageScriptGraph(
if (/\bscripts\/run-ci-gate(?:\.[cm]?[jt]s)?\b/u.test(command)) {
failures.push(`${scriptName} must not invoke the CI gate runner`);
}
if (/\bci:gate\b/u.test(command)) {
failures.push(`${scriptName} must not invoke ci:gate`);
const parsed = parseManagerCommands(command, scripts);
if (parsed.unsupportedManagerSyntax) {
failures.push(`package script manager invocation is not safely parseable: ${scriptName}`);
}
packageScriptInvocation.lastIndex = 0;
const dependencies = Array.from(
command.matchAll(packageScriptInvocation),
(match) => match[1]!,
).filter((dependency) => !pnpmNonScriptCommands.has(dependency));
for (const dependency of dependencies) {
if (dependency !== "ci:gate") {
if (!(dependency in scripts)) {
failures.push(`package script missing: ${scriptName} -> ${dependency}`);
} else {
visit(dependency);
}
for (const dependency of parsed.dependencies) {
if (dependency === "ci:gate") {
failures.push(`${scriptName} must not invoke ci:gate`);
} else if (!(dependency in scripts)) {
failures.push(`package script missing: ${scriptName} -> ${dependency}`);
} else {
visit(dependency);
}
}
stack.pop();
@@ -52,3 +181,814 @@ export function validatePackageScriptGraph(
visit(entryScript);
return [...new Set(failures)];
}
export function validateInstallScriptPolicy(
scripts: Readonly<Record<string, string>>,
entryScripts: readonly string[],
): string[] {
const failures: string[] = [];
const visited = new Set<string>();
const visit = (scriptName: string): void => {
if (visited.has(scriptName)) return;
visited.add(scriptName);
const command = scripts[scriptName];
if (command === undefined) {
failures.push(`package script missing: ${scriptName}`);
return;
}
const parsed = parseManagerCommands(command, scripts);
if (parsed.unsafeLifecycle || parsed.unsupportedManagerSyntax) {
failures.push(
`install-bearing package script must use --ignore-scripts: ${scriptName}`,
);
}
for (const dependency of parsed.dependencies) {
if (dependency !== "ci:gate") visit(dependency);
}
};
for (const entryScript of entryScripts) visit(entryScript);
return [...new Set(failures)];
}
export function validateNpmScopeEnvironment(
environment: Readonly<Record<string, string | undefined>>,
): string[] {
return Object.keys(environment)
.filter((name) => npmDispatchScopeEnvironmentNames.has(name.toLowerCase()))
.map((name) => `npm scope environment is not allowed: ${name}`);
}
function parseManagerCommands(
command: string,
scripts: Readonly<Record<string, string>>,
): ManagerParseResult {
const tokenized = tokenizeShellSegments(command);
const dependencies: string[] = [];
let unsafeLifecycle = false;
let unsupportedManagerSyntax = false;
if (!tokenized) {
return Object.freeze({
dependencies: Object.freeze([]),
unsafeLifecycle: false,
unsupportedManagerSyntax: containsManagerReference(command),
});
}
unsupportedManagerSyntax ||= tokenized.unsupportedControl && containsManagerReference(command);
const npmScopeEnvironmentState: ShellNpmScopeEnvironmentState = {
autoExport: false,
forbidden: false,
uncertain: false,
};
for (const segment of tokenized.segments) {
const { tokens, expansionTokens } = segment;
updateShellNpmScopeEnvironmentState(segment, npmScopeEnvironmentState);
for (let index = 0; index < tokens.length; index += 1) {
const token = tokens[index]!;
if (token === "corepack") {
if (hasUnsafeManagerCommandPrefix(tokens, expansionTokens, index)) {
unsupportedManagerSyntax = true;
break;
}
const wrapped = tokens[index + 1];
if (!wrapped || !isPackageManager(wrapped)) {
unsupportedManagerSyntax = true;
break;
}
const parsed = parseManagerInvocation(
wrapped,
tokens,
expansionTokens,
index + 2,
scripts,
hasUnsafeNpmScopeEnvironment(
npmScopeEnvironmentState,
tokens,
expansionTokens,
index,
),
);
dependencies.push(...parsed.dependencies);
unsafeLifecycle ||= parsed.unsafeLifecycle;
unsupportedManagerSyntax ||= parsed.unsupportedManagerSyntax;
break;
}
if (isPackageManager(token)) {
if (hasUnsafeManagerCommandPrefix(tokens, expansionTokens, index)) {
unsupportedManagerSyntax = true;
break;
}
const parsed = parseManagerInvocation(
token,
tokens,
expansionTokens,
index + 1,
scripts,
hasUnsafeNpmScopeEnvironment(
npmScopeEnvironmentState,
tokens,
expansionTokens,
index,
),
);
dependencies.push(...parsed.dependencies);
unsafeLifecycle ||= parsed.unsafeLifecycle;
unsupportedManagerSyntax ||= parsed.unsupportedManagerSyntax;
break;
}
if (containsManagerReference(token)) {
unsupportedManagerSyntax = true;
break;
}
}
}
return Object.freeze({
dependencies: Object.freeze([...new Set(dependencies)]),
unsafeLifecycle,
unsupportedManagerSyntax,
});
}
function parseManagerInvocation(
manager: PackageManager,
tokens: readonly string[],
expansionTokens: readonly boolean[],
start: number,
scripts: Readonly<Record<string, string>>,
hasNpmScopeEnvironment: boolean,
): ManagerParseResult {
let cursor = start;
let changesManifestScope = false;
let hasNpmConfigAuthority = false;
let consumedManagerSyntax = false;
const suppression: SuppressionState = {
effective: undefined,
contradictory: false,
malformed: false,
};
while (cursor < tokens.length && tokens[cursor]!.startsWith("-")) {
consumedManagerSyntax = true;
const option = tokens[cursor]!;
const parsedSuppression = consumeSuppressionOption(
manager,
tokens,
cursor,
suppression,
);
if (parsedSuppression.recognized) {
if (parsedSuppression.unsupported) return unsupportedResult();
cursor = parsedSuppression.nextIndex;
continue;
}
const equals = option.indexOf("=");
const name = equals < 0 ? option : option.slice(0, equals);
if (managerOptionsWithValue.has(name)) {
changesManifestScope ||= manifestScopeOptions[manager].has(name);
hasNpmConfigAuthority ||=
manager === "npm" && npmIndirectConfigAuthorityOptions.has(name);
if (equals >= 0) {
if (option.slice(equals + 1).length === 0) return unsupportedResult();
} else {
cursor += 1;
if (cursor >= tokens.length || tokens[cursor]!.startsWith("-")) {
return unsupportedResult();
}
}
} else if (managerBooleanOptions.has(name)) {
if (equals >= 0 && !/^(?:true|false)$/u.test(option.slice(equals + 1))) {
return unsupportedResult();
}
} else if (option !== "--") {
return unsupportedResult();
}
cursor += 1;
}
const subcommand = tokens[cursor];
if (!subcommand) return unsupportedResult();
if (manager === "npm" && (hasNpmScopeEnvironment || hasNpmConfigAuthority)) {
return unsupportedResult();
}
const argumentsAfterCommand = tokens.slice(cursor + 1);
if (subcommand === "run" || subcommand === "run-script") {
const dependency = argumentsAfterCommand[0];
if (!dependency || dependency.startsWith("-")) return unsupportedResult();
if (changesManifestScope) return unsupportedResult();
if (
manager === "npm" &&
(expansionTokens.slice(start, cursor + 2).some(Boolean) ||
!areNpmScriptDispatchArgumentsSupported(
argumentsAfterCommand.slice(1),
expansionTokens.slice(cursor + 2),
suppression,
))
) {
return unsupportedResult();
}
return manager === "npm"
? npmScriptDependencyResult(dependency, scripts, suppression)
: dependencyResult(dependency);
}
const canonicalSubcommand = managerBuiltinAliases[manager].get(subcommand) ?? subcommand;
if (unsupportedBuiltinDispatchers[manager].has(canonicalSubcommand)) {
return unsupportedResult();
}
if (lifecycleMutationCommands.has(canonicalSubcommand)) {
const lifecycleArgumentsSupported = parseLifecycleArguments(
manager,
argumentsAfterCommand,
suppression,
);
return Object.freeze({
dependencies: Object.freeze([]),
unsafeLifecycle:
!lifecycleArgumentsSupported || !hasEffectiveLifecycleSuppression(suppression),
unsupportedManagerSyntax: !lifecycleArgumentsSupported,
});
}
if (knownBuiltinCommands[manager].has(canonicalSubcommand)) {
return exactBareSafeBuiltinCommands[manager].has(canonicalSubcommand) &&
!consumedManagerSyntax &&
argumentsAfterCommand.length === 0
? emptyResult()
: unsupportedResult();
}
const isKnownRootScript = Object.prototype.hasOwnProperty.call(scripts, subcommand);
const supportsImplicit = /^[A-Za-z0-9:_-]+$/u.test(subcommand) && (
((manager === "pnpm" || manager === "yarn") && isKnownRootScript) ||
(manager === "npm" && npmImplicitScripts.has(subcommand))
);
if (supportsImplicit) {
if (changesManifestScope) return unsupportedResult();
if (
manager === "npm" &&
(expansionTokens.slice(start, cursor + 1).some(Boolean) ||
!areNpmScriptDispatchArgumentsSupported(
argumentsAfterCommand,
expansionTokens.slice(cursor + 1),
suppression,
))
) {
return unsupportedResult();
}
return manager === "npm"
? npmScriptDependencyResult(subcommand, scripts, suppression)
: dependencyResult(subcommand);
}
return unsupportedResult();
}
function areNpmScriptDispatchArgumentsSupported(
tokens: readonly string[],
expansionTokens: readonly boolean[],
suppression: SuppressionState,
): boolean {
let index = 0;
while (index < tokens.length) {
const token = tokens[index]!;
if (token === "--") return true;
if (expansionTokens[index]) return false;
if (!token.startsWith("-")) {
index += 1;
continue;
}
const parsedSuppression = consumeSuppressionOption(
"npm",
tokens,
index,
suppression,
);
if (parsedSuppression.recognized) {
if (parsedSuppression.unsupported) return false;
index = parsedSuppression.nextIndex;
continue;
}
const equals = token.indexOf("=");
const name = equals < 0 ? token : token.slice(0, equals);
const isShortWorkspaceOption = token === "-w" || /^-w(?:=)?.+/u.test(token);
if (npmScriptDispatchScopeOptions.has(name) || isShortWorkspaceOption) {
return false;
}
if (!npmScriptDispatchBooleanOptions.has(name)) return false;
if (equals >= 0 && !/^(?:true|false)$/u.test(token.slice(equals + 1))) {
return false;
}
index += 1;
}
return true;
}
function hasUnsafeNpmScopeEnvironment(
state: Readonly<ShellNpmScopeEnvironmentState>,
tokens: readonly string[],
expansionTokens: readonly boolean[],
commandIndex: number,
): boolean {
return state.forbidden || state.uncertain ||
hasUnsafeImmediateNpmScopeEnvironment(tokens, expansionTokens, commandIndex);
}
function hasUnsafeManagerCommandPrefix(
tokens: readonly string[],
expansionTokens: readonly boolean[],
commandIndex: number,
): boolean {
const prefix = parseShellCommandPrefix(tokens, expansionTokens);
if (prefix.uncertain) return true;
let cursor = prefix.commandIndex;
if (cursor === commandIndex) return false;
if (cursor > commandIndex ||
!isEnvironmentCommand(tokens[cursor], expansionTokens[cursor] ?? false)) {
return true;
}
return parseEnvironmentCommandPrefix(
tokens,
expansionTokens,
cursor,
commandIndex,
).uncertain;
}
function hasUnsafeImmediateNpmScopeEnvironment(
tokens: readonly string[],
expansionTokens: readonly boolean[],
commandIndex: number,
): boolean {
const prefix = parseShellCommandPrefix(tokens, expansionTokens);
if (prefix.uncertain || prefix.assignments.some(
({ name }) => name !== null && isNpmScopeEnvironmentName(name),
)) return true;
let cursor = prefix.commandIndex;
if (cursor === commandIndex) return false;
if (!isEnvironmentCommand(tokens[cursor], expansionTokens[cursor] ?? false)) return false;
const environmentPrefix = parseEnvironmentCommandPrefix(
tokens,
expansionTokens,
cursor,
commandIndex,
);
return environmentPrefix.uncertain || environmentPrefix.assignmentNames.some(
(name) => isNpmScopeEnvironmentName(name),
);
}
function parseEnvironmentCommandPrefix(
tokens: readonly string[],
expansionTokens: readonly boolean[],
start: number,
commandIndex: number,
): EnvironmentCommandPrefix {
const assignmentNames: string[] = [];
let cursor = start + 1;
let uncertain = false;
while (cursor < commandIndex && tokens[cursor]!.startsWith("-")) {
const option = tokens[cursor]!;
if (expansionTokens[cursor]) uncertain = true;
if (option === "--") {
cursor += 1;
break;
}
if (option === "-i" || option === "--ignore-environment") {
cursor += 1;
continue;
}
if (option === "-u" || option === "--unset") {
cursor += 1;
if (cursor >= commandIndex || tokens[cursor]!.startsWith("-")) {
uncertain = true;
break;
}
uncertain ||= expansionTokens[cursor] ?? false;
cursor += 1;
continue;
}
if (/^--unset=.+/u.test(option)) {
cursor += 1;
continue;
}
uncertain = true;
cursor += 1;
}
while (cursor < commandIndex) {
const token = tokens[cursor]!;
if (hasDynamicAssignmentName(token, expansionTokens[cursor] ?? false)) {
uncertain = true;
}
const assignmentName = parseEnvironmentAssignmentName(token);
if (assignmentName) assignmentNames.push(assignmentName);
else uncertain = true;
cursor += 1;
}
return Object.freeze({
assignmentNames: Object.freeze(assignmentNames),
uncertain,
});
}
function parseShellCommandPrefix(
tokens: readonly string[],
expansionTokens: readonly boolean[],
): ShellCommandPrefix {
const assignments: Array<{
dynamicName: boolean;
name: string | null;
}> = [];
let cursor = 0;
let uncertain = false;
while (cursor < tokens.length) {
const token = tokens[cursor]!;
const name = parseAssignmentName(token);
const dynamicName = hasDynamicAssignmentName(
token,
expansionTokens[cursor] ?? false,
);
if (!name && !dynamicName) break;
assignments.push({ dynamicName, name });
uncertain ||= dynamicName;
cursor += 1;
}
while (cursor < tokens.length) {
const wrapper = tokens[cursor];
if (expansionTokens[cursor]) {
uncertain = true;
break;
}
if (wrapper !== "command" && wrapper !== "exec") break;
cursor += 1;
while (cursor < tokens.length && tokens[cursor]!.startsWith("-")) {
const option = tokens[cursor]!;
if (option === "--") {
cursor += 1;
break;
}
if (wrapper === "command" && option === "-p") {
cursor += 1;
continue;
}
uncertain = true;
cursor += 1;
if (wrapper === "exec" && option === "-a" && cursor < tokens.length) {
cursor += 1;
}
}
}
if (expansionTokens[cursor]) uncertain = true;
return Object.freeze({
assignments: Object.freeze(assignments.map((assignment) => Object.freeze(assignment))),
commandIndex: cursor,
uncertain,
});
}
function updateShellNpmScopeEnvironmentState(
segment: TokenizedShellSegment,
state: ShellNpmScopeEnvironmentState,
): void {
const { tokens, expansionTokens } = segment;
const prefix = parseShellCommandPrefix(tokens, expansionTokens);
state.uncertain ||= prefix.uncertain;
const command = tokens[prefix.commandIndex];
if (!command) {
for (const assignment of prefix.assignments) {
if (state.autoExport && assignment.name &&
isNpmScopeEnvironmentName(assignment.name)) {
state.forbidden = true;
}
}
return;
}
if (command === "eval" || command === "." || command === "source") {
state.uncertain = true;
return;
}
if (command === "unset" || command === "typeset" || command === "declare" ||
command === "local" || command === "readonly") {
state.uncertain = true;
return;
}
if (command === "set") {
if (tokens.slice(prefix.commandIndex + 1).includes("-a")) state.autoExport = true;
if (tokens.slice(prefix.commandIndex + 1).includes("+a")) {
state.autoExport = false;
state.uncertain = true;
}
return;
}
if (command === "export") {
let cursor = prefix.commandIndex + 1;
for (; cursor < tokens.length; cursor += 1) {
const token = tokens[cursor]!;
if (token === "--") continue;
if (token === "-n" || token.startsWith("-")) {
state.uncertain = true;
continue;
}
const assignmentName = parseAssignmentName(token);
const bareName = /^[A-Za-z_][A-Za-z0-9_]*$/u.test(token) ? token : null;
if (assignmentName || bareName) {
if (isNpmScopeEnvironmentName(assignmentName ?? bareName!)) {
state.forbidden = true;
}
} else if (hasDynamicAssignmentName(token, expansionTokens[cursor] ?? false) ||
expansionTokens[cursor]) {
state.uncertain = true;
}
}
return;
}
}
function isEnvironmentCommand(token: string | undefined, hasExpansion: boolean): boolean {
if (!token || hasExpansion) return false;
return token.split("/").at(-1) === "env";
}
function parseEnvironmentAssignmentName(token: string): string | null {
const equals = token.indexOf("=");
return equals > 0 ? token.slice(0, equals) : null;
}
function isNpmScopeEnvironmentName(name: string): boolean {
return npmDispatchScopeEnvironmentNames.has(name.toLowerCase());
}
function hasDynamicAssignmentName(token: string, hasExpansion: boolean): boolean {
const equals = token.indexOf("=");
return hasExpansion && equals > 0 && parseAssignmentName(token) === null;
}
function parseAssignmentName(token: string): string | null {
return /^([A-Za-z_][A-Za-z0-9_]*)=/u.exec(token)?.[1] ?? null;
}
function parseLifecycleArguments(
manager: PackageManager,
tokens: readonly string[],
suppression: SuppressionState,
): boolean {
let cursor = 0;
while (cursor < tokens.length) {
const token = tokens[cursor]!;
if (!token.startsWith("-")) {
cursor += 1;
continue;
}
const parsedSuppression = consumeSuppressionOption(
manager,
tokens,
cursor,
suppression,
);
if (parsedSuppression.recognized) {
if (parsedSuppression.unsupported) return false;
cursor = parsedSuppression.nextIndex;
continue;
}
const parsedOption = consumeAllowedLifecycleOption(manager, tokens, cursor);
if (parsedOption === null) return false;
cursor = parsedOption;
}
return true;
}
function consumeSuppressionOption(
manager: PackageManager,
tokens: readonly string[],
index: number,
state: SuppressionState,
): Readonly<{ recognized: boolean; unsupported: boolean; nextIndex: number }> {
const token = tokens[index]!;
if (token === "--no-ignore-scripts") {
recordSuppression(state, false);
return { recognized: true, unsupported: false, nextIndex: index + 1 };
}
const equalsForms = ["--ignore-scripts=", "--config.ignore-scripts="] as const;
for (const prefix of equalsForms) {
if (!token.startsWith(prefix)) continue;
if (prefix.startsWith("--config.") && manager !== "pnpm") {
state.malformed = true;
return { recognized: true, unsupported: true, nextIndex: index + 1 };
}
const raw = token.slice(prefix.length);
if (raw !== "true" && raw !== "false") {
state.malformed = true;
return { recognized: true, unsupported: true, nextIndex: index + 1 };
}
recordSuppression(state, raw === "true");
return { recognized: true, unsupported: false, nextIndex: index + 1 };
}
if (token !== "--ignore-scripts" && token !== "--config.ignore-scripts") {
return { recognized: false, unsupported: false, nextIndex: index };
}
if (token === "--config.ignore-scripts" && manager !== "pnpm") {
state.malformed = true;
return { recognized: true, unsupported: true, nextIndex: index + 1 };
}
const next = tokens[index + 1];
if (next === "true" || next === "false") {
const supportsSplitValue = manager === "npm" || manager === "pnpm";
if (!supportsSplitValue) {
state.malformed = true;
return { recognized: true, unsupported: true, nextIndex: index + 2 };
}
recordSuppression(state, next === "true");
return { recognized: true, unsupported: false, nextIndex: index + 2 };
}
recordSuppression(state, true);
return { recognized: true, unsupported: false, nextIndex: index + 1 };
}
function recordSuppression(
state: SuppressionState,
value: boolean,
): void {
if (state.effective !== undefined && state.effective !== value) {
state.contradictory = true;
}
state.effective = value;
}
function hasEffectiveLifecycleSuppression(state: SuppressionState): boolean {
return state.effective === true && !state.contradictory && !state.malformed;
}
function consumeAllowedLifecycleOption(
manager: PackageManager,
tokens: readonly string[],
index: number,
): number | null {
const option = tokens[index]!;
const equals = option.indexOf("=");
const name = equals < 0 ? option : option.slice(0, equals);
const booleanOption =
managerBooleanOptions.has(name) || lifecycleBooleanOptions[manager].has(name);
if (booleanOption) {
if (equals >= 0 && !/^(?:true|false)$/u.test(option.slice(equals + 1))) return null;
return index + 1;
}
const valuedOption =
managerOptionsWithValue.has(name) || lifecycleOptionsWithValue[manager].has(name);
if (!valuedOption) return null;
if (equals >= 0) return option.slice(equals + 1).length > 0 ? index + 1 : null;
const value = tokens[index + 1];
if (!value || value.startsWith("-")) return null;
return index + 2;
}
function dependencyResult(dependency: string): ManagerParseResult {
return dependenciesResult([dependency]);
}
function npmScriptDependencyResult(
dependency: string,
scripts: Readonly<Record<string, string>>,
suppression: SuppressionState,
): ManagerParseResult {
if (hasEffectiveLifecycleSuppression(suppression)) {
return dependenciesResult([dependency]);
}
return dependenciesResult(
[`pre${dependency}`, dependency, `post${dependency}`]
.filter((scriptName) => scriptName === dependency || scriptName in scripts),
);
}
function dependenciesResult(dependencies: readonly string[]): ManagerParseResult {
return Object.freeze({
dependencies: Object.freeze([...dependencies]),
unsafeLifecycle: false,
unsupportedManagerSyntax: false,
});
}
function emptyResult(): ManagerParseResult {
return Object.freeze({
dependencies: Object.freeze([]),
unsafeLifecycle: false,
unsupportedManagerSyntax: false,
});
}
function unsupportedResult(): ManagerParseResult {
return Object.freeze({
dependencies: Object.freeze([]),
unsafeLifecycle: false,
unsupportedManagerSyntax: true,
});
}
function isPackageManager(value: string): value is PackageManager {
return managerNames.has(value as PackageManager);
}
function containsManagerReference(value: string): boolean {
return /(?:^|[^A-Za-z0-9_-])(?:corepack|pnpm|npm|yarn)(?:[^A-Za-z0-9_-]|$)/u
.test(value);
}
function tokenizeShellSegments(command: string): Readonly<{
segments: readonly TokenizedShellSegment[];
unsupportedControl: boolean;
}> | null {
const segments: Array<{ tokens: string[]; expansionTokens: boolean[] }> = [
{ tokens: [], expansionTokens: [] },
];
let token = "";
let tokenHasExpansion = false;
let quote: "'" | '"' | null = null;
let escaping = false;
let unsupportedControl = false;
const pushToken = (): void => {
if (token.length > 0) {
segments.at(-1)!.tokens.push(token);
segments.at(-1)!.expansionTokens.push(tokenHasExpansion);
}
token = "";
tokenHasExpansion = false;
};
const pushSegment = (): void => {
pushToken();
if (segments.at(-1)!.tokens.length > 0) {
segments.push({ tokens: [], expansionTokens: [] });
}
};
for (let index = 0; index < command.length; index += 1) {
const character = command[index]!;
if (escaping) {
token += character;
escaping = false;
continue;
}
if (character === "\\" && quote !== "'") {
escaping = true;
continue;
}
if (quote) {
if (character === quote) quote = null;
else {
if (quote === '"' && character === "$") tokenHasExpansion = true;
token += character;
}
continue;
}
if (character === "'" || character === '"') {
quote = character;
continue;
}
if (character === "`" || (character === "$" && command[index + 1] === "(")) {
unsupportedControl = true;
if (character === "$") tokenHasExpansion = true;
token += character;
continue;
}
if (character === "$" || character === "*" || character === "?" || character === "[") {
tokenHasExpansion = true;
}
if (character === "#") {
unsupportedControl = true;
pushToken();
while (
index + 1 < command.length &&
command[index + 1] !== "\n" &&
command[index + 1] !== "\r"
) {
index += 1;
}
continue;
}
if (character === "<" || character === ">" || character === "(" || character === ")") {
unsupportedControl = true;
token += character;
continue;
}
if (/\s/u.test(character)) {
pushToken();
if (character === "\n" || character === "\r") pushSegment();
continue;
}
if (character === ";" || character === "|" || character === "&") {
pushSegment();
if (command[index + 1] === character) index += 1;
continue;
}
token += character;
}
if (quote || escaping) return null;
pushToken();
return Object.freeze({
segments: Object.freeze(
segments
.filter((segment) => segment.tokens.length > 0)
.map((segment) => Object.freeze({
tokens: Object.freeze(segment.tokens),
expansionTokens: Object.freeze(segment.expansionTokens),
})),
),
unsupportedControl,
});
}
+304 -29
View File
@@ -8,7 +8,9 @@ import {
lstat,
mkdir,
open,
readdir,
rm,
rmdir,
stat,
} from "node:fs/promises";
import path from "node:path";
@@ -19,6 +21,7 @@ import {
} from "../contracts/promotion-artifacts.ts";
import {
evaluatePromotionEvidence,
assertDistinctProviderTrust,
providerPublicKeyFingerprint,
providerVerificationArtifactSchema,
PROMOTION_VERIFIER_ID,
@@ -28,6 +31,7 @@ import {
vulnerabilityProviderReportSchema,
type ProviderTrust,
} from "./provider-evidence.ts";
import { verifyExactPromotionBundle } from "./exact-promotion-bundle.ts";
import {
captureCiCandidateArchive,
withVerifiedCapturedCandidate,
@@ -35,7 +39,8 @@ import {
import { verifyArchivedLocalEvidence } from "./local-release-evidence.ts";
import { readBoundedRegularFile } from "./ci-artifact-validator.ts";
type StagedFile = Readonly<{
export type StagedFile = Readonly<{
name: PromotedFileName;
bytes: Buffer;
sha256: string;
@@ -45,6 +50,7 @@ export type FinalizedPromotion = Readonly<{
stagingRoot: string;
cleanupToken: string;
runnerTempIdentity: Readonly<{ dev: number; ino: number }>;
stagingIdentity: Readonly<{ dev: number; ino: number }>;
files: readonly Readonly<{ name: PromotedFileName; sha256: string }>[];
}>;
@@ -69,6 +75,9 @@ export async function finalizeVerifiedPromotion(input: Readonly<{
afterCapture?: () => Promise<void>;
beforePublish?: () => Promise<void>;
afterStagingWrite?: () => Promise<void>;
afterFileWrite?: (name: PromotedFileName) => Promise<void>;
beforeSeal?: () => Promise<void>;
afterMkdirBeforeOpen?: (stagingRoot: string) => Promise<void>;
}> = {}): Promise<FinalizedPromotion> {
const root = path.resolve(input.repositoryRoot);
const capturedArchive = await (dependencies.captureArchive ?? captureCiCandidateArchive)({
@@ -92,14 +101,14 @@ export async function finalizeVerifiedPromotion(input: Readonly<{
input.provenanceKeyId,
provenanceKeyBytes,
);
assertDistinctProviderTrust({ vulnerabilityTrust, provenanceTrust });
const vulnerabilityReport = vulnerabilityProviderReportSchema.parse(
parseJson(vulnerabilityBytes),
);
const provenanceAttestation = provenanceProviderAttestationSchema.parse(
parseJson(provenanceBytes),
);
const now = (dependencies.nowEpochMs ?? Date.now)();
const verifiedAt = new Date(now).toISOString();
const nowEpochMs = dependencies.nowEpochMs ?? Date.now;
const generated = await withVerifiedCapturedCandidate({
captured: capturedArchive,
@@ -128,6 +137,14 @@ export async function finalizeVerifiedPromotion(input: Readonly<{
distSha256: manifest.distSha256,
lockfileSha256: manifest.lockfileSha256,
},
secretScanAttestation: {
status: "PASS" as const,
localEvidenceAssessmentSha256: local.identity.assessmentSha256,
sourceSetSha256: local.identity.sourceSetSha256,
policySha256: local.identity.secretScan.policySha256,
sarifSha256: local.identity.secretScan.sarifSha256,
scanInputSha256: local.identity.secretScan.scanInputSha256,
},
vulnerabilityInvocationNonce: input.vulnerabilityInvocationNonce,
provenanceInvocationNonce: input.provenanceInvocationNonce,
} as const;
@@ -138,7 +155,7 @@ export async function finalizeVerifiedPromotion(input: Readonly<{
provenanceAttestation,
vulnerabilityTrust,
provenanceTrust,
nowEpochMs: () => now,
nowEpochMs,
});
if (reevaluated.status !== "PASS") {
throw new Error(
@@ -154,8 +171,10 @@ export async function finalizeVerifiedPromotion(input: Readonly<{
vulnerabilityKeyFingerprint: vulnerabilityTrust.publicKeyFingerprint,
provenanceKeyId: provenanceTrust.keyId,
provenanceKeyFingerprint: provenanceTrust.publicKeyFingerprint,
secretScanAttestation: expected.secretScanAttestation,
} as const;
const trustDigest = trustPolicySha256({ vulnerabilityTrust, provenanceTrust });
const verifiedAt = new Date(nowEpochMs()).toISOString();
const common = {
schemaVersion: 3 as const,
verifiedAt,
@@ -188,6 +207,15 @@ export async function finalizeVerifiedPromotion(input: Readonly<{
return Object.freeze({
providerRecordBytes,
promotionRecordBytes: canonicalJsonBytes(promotionRecord),
exactExpected: Object.freeze({
run: expected.run,
sourceRevision: expected.source.revision,
sourceSetSha256: expected.source.sourceSetSha256,
archiveSha256: expected.candidate.archiveSha256,
bundleSha256: expected.candidate.bundleSha256,
distSha256: expected.candidate.distSha256,
lockfileSha256: expected.candidate.lockfileSha256,
}),
});
},
});
@@ -206,12 +234,35 @@ export async function finalizeVerifiedPromotion(input: Readonly<{
throw new Error("promotion exact-five canonical file order drift");
}
await dependencies.beforePublish?.();
return publishPrivateStaging(
await verifyExactPromotionBundle(
Object.fromEntries(stagedFiles.map(({ name, bytes }) => [name, bytes])),
{
vulnerabilityTrust,
provenanceTrust,
expected: generated.exactExpected,
nowEpochMs,
},
);
return publishPrivatePromotionStaging(
input.runnerTempRoot,
input.expectedRun,
stagedFiles,
dependencies.randomBytes ?? cryptoRandomBytes,
dependencies.afterStagingWrite,
dependencies.afterFileWrite,
async (capturedFiles) => {
await verifyExactPromotionBundle(
capturedFiles,
{
vulnerabilityTrust,
provenanceTrust,
expected: generated.exactExpected,
nowEpochMs,
},
);
},
dependencies.afterMkdirBeforeOpen,
dependencies.beforeSeal,
);
}
@@ -222,6 +273,7 @@ export async function cleanupFinalizedPromotion(input: Readonly<{
stagingRoot: string;
cleanupToken: string;
runnerTempIdentity: Readonly<{ dev: number; ino: number }>;
stagingIdentity: Readonly<{ dev: number; ino: number }>;
}>, dependencies: Readonly<{
beforeRemove?: () => Promise<void>;
}> = {}): Promise<void> {
@@ -234,6 +286,10 @@ export async function cleanupFinalizedPromotion(input: Readonly<{
input.runnerTempIdentity.dev <= 0 ||
!Number.isSafeInteger(input.runnerTempIdentity.ino) ||
input.runnerTempIdentity.ino <= 0
|| !Number.isSafeInteger(input.stagingIdentity.dev)
|| input.stagingIdentity.dev <= 0
|| !Number.isSafeInteger(input.stagingIdentity.ino)
|| input.stagingIdentity.ino <= 0
) {
throw new TypeError("promotion cleanup root/token mismatch");
}
@@ -260,10 +316,33 @@ export async function cleanupFinalizedPromotion(input: Readonly<{
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
throw new TypeError("promotion cleanup leaf is unsafe");
}
await dependencies.beforeRemove?.();
const visibleParent = await lstat(parent);
assertRunnerTempIdentity(visibleParent, input.runnerTempIdentity);
await rm(descriptorExpected, { recursive: true, force: true });
assertStagingIdentity(metadata, input.stagingIdentity);
const stagingHandle = await open(
descriptorExpected,
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
);
try {
const stagingDescriptorRoot = `/proc/self/fd/${stagingHandle.fd}`;
assertStagingIdentity(await stat(stagingDescriptorRoot), input.stagingIdentity);
const names = (await readdir(stagingDescriptorRoot)).sort(asciiCompare);
if (
JSON.stringify(names) !==
JSON.stringify([...PROMOTED_FILE_NAMES].sort(asciiCompare))
) {
throw new Error("promotion cleanup leaf does not contain the exact five files");
}
await dependencies.beforeRemove?.();
const visibleParent = await lstat(parent);
assertRunnerTempIdentity(visibleParent, input.runnerTempIdentity);
for (const name of PROMOTED_FILE_NAMES) {
await rm(path.join(stagingDescriptorRoot, name), { force: false });
}
assertStagingIdentity(await stat(stagingDescriptorRoot), input.stagingIdentity);
assertStagingIdentity(await lstat(descriptorExpected), input.stagingIdentity);
await rmdir(descriptorExpected);
} finally {
await stagingHandle.close();
}
const afterParent = await lstat(parent);
assertRunnerTempIdentity(afterParent, input.runnerTempIdentity);
} finally {
@@ -271,13 +350,29 @@ export async function cleanupFinalizedPromotion(input: Readonly<{
}
}
async function publishPrivateStaging(
export async function publishPrivatePromotionStaging(
runnerTempRoot: string,
run: Readonly<{ id: string; attempt: number }>,
files: readonly StagedFile[],
randomBytes: (bytes: number) => Buffer,
afterStagingWrite?: () => Promise<void>,
afterFileWrite?: (name: PromotedFileName) => Promise<void>,
sealStagedFiles?: (files: Readonly<Record<PromotedFileName, Buffer>>) => Promise<void>,
afterMkdirBeforeOpen?: (stagingRoot: string) => Promise<void>,
beforeSeal?: () => Promise<void>,
): Promise<FinalizedPromotion> {
if (
JSON.stringify(files.map(({ name }) => name)) !==
JSON.stringify(PROMOTED_FILE_NAMES) ||
files.some(
({ bytes, sha256: digest }) =>
!Buffer.isBuffer(bytes) ||
!/^[a-f0-9]{64}$/u.test(digest) ||
sha256(bytes) !== digest,
)
) {
throw new TypeError("private promotion staging requires the canonical exact-five bytes");
}
const parentPath = path.resolve(runnerTempRoot);
const before = await lstat(parentPath);
if (!before.isDirectory() || before.isSymbolicLink()) {
@@ -298,14 +393,119 @@ async function publishPrivateStaging(
const descriptorStaging = path.join(descriptorRoot, cleanupToken);
const visibleStaging = path.join(parentPath, cleanupToken);
let ownsStaging = false;
let stagingHandle: Awaited<ReturnType<typeof open>> | undefined;
let createdStagingIdentity: Readonly<{ dev: number; ino: number }> | undefined;
let stagingIdentity: Readonly<{ dev: number; ino: number }> | undefined;
let openedIdentityVerified = false;
const cleanup = async (primaryFailure?: unknown): Promise<void> => {
const cleanupFailures: unknown[] = [];
const attemptCleanup = async (operation: () => Promise<void>): Promise<void> => {
try {
await operation();
} catch (error) {
cleanupFailures.push(error);
}
};
if (ownsStaging && openedIdentityVerified && stagingHandle && stagingIdentity) {
const ownedIdentity = stagingIdentity;
const stagingDescriptorRoot = `/proc/self/fd/${stagingHandle.fd}`;
const removals = await Promise.allSettled(
files.map(({ name }) => rm(path.join(stagingDescriptorRoot, name), { force: true })),
);
cleanupFailures.push(
...removals.flatMap((result) =>
result.status === "rejected" ? [result.reason] : [],
),
);
await attemptCleanup(async () => {
let visible;
try {
visible = await lstat(descriptorStaging);
} catch (error) {
if (hasErrorCode(error, "ENOENT")) return;
throw error;
}
if (
visible.isDirectory() &&
!visible.isSymbolicLink() &&
visible.dev === ownedIdentity.dev &&
visible.ino === ownedIdentity.ino
) {
await rmdir(descriptorStaging);
}
});
}
if (stagingHandle) {
const ownedHandle = stagingHandle;
await attemptCleanup(async () => ownedHandle.close());
}
await attemptCleanup(async () => parentHandle.close());
if (cleanupFailures.length > 0) {
throw new AggregateError(
primaryFailure === undefined
? cleanupFailures
: [primaryFailure, ...cleanupFailures],
primaryFailure instanceof Error
? `${primaryFailure.message}; promotion staging cleanup also failed`
: "promotion staging cleanup failed",
{ cause: cleanupFailures.at(-1) },
);
}
};
let finalizedPromotion: FinalizedPromotion;
try {
const procMetadata = await stat(descriptorRoot);
if (!procMetadata.isDirectory()) throw new Error("descriptor-relative staging is unavailable");
await mkdir(descriptorStaging, { mode: 0o700 });
ownsStaging = true;
const createdStaging = await lstat(descriptorStaging);
if (!createdStaging.isDirectory() || createdStaging.isSymbolicLink()) {
throw new Error("created promotion staging leaf is unsafe");
}
createdStagingIdentity = Object.freeze({
dev: createdStaging.dev,
ino: createdStaging.ino,
});
await afterMkdirBeforeOpen?.(visibleStaging);
const openedHandle = await open(
descriptorStaging,
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
);
let openedStaging;
try {
openedStaging = await openedHandle.stat();
if (!openedStaging.isDirectory()) {
throw new Error("promotion staging descriptor is not a directory");
}
if (
openedStaging.dev !== createdStagingIdentity.dev ||
openedStaging.ino !== createdStagingIdentity.ino
) {
throw new Error("promotion staging leaf identity changed between mkdir and open");
}
openedIdentityVerified = true;
} catch (error) {
try {
await openedHandle.close();
} catch (closeError) {
throw new AggregateError(
[error, closeError],
error instanceof Error
? `${error.message}; rejected staging descriptor close also failed`
: "rejected staging descriptor and close both failed",
{ cause: closeError },
);
}
throw error;
}
stagingHandle = openedHandle;
await stagingHandle.chmod(0o700);
stagingIdentity = Object.freeze({ dev: openedStaging.dev, ino: openedStaging.ino });
const stagingDescriptorRoot = `/proc/self/fd/${stagingHandle.fd}`;
assertStagingIdentity(await stat(stagingDescriptorRoot), stagingIdentity);
for (const file of files) {
const handle = await open(
path.join(descriptorStaging, file.name),
path.join(stagingDescriptorRoot, file.name),
constants.O_WRONLY |
constants.O_CREAT |
constants.O_EXCL |
@@ -313,15 +513,20 @@ async function publishPrivateStaging(
0o400,
);
try {
await handle.chmod(0o400);
await handle.writeFile(file.bytes);
await handle.sync();
} finally {
await handle.close();
}
await afterFileWrite?.(file.name);
}
await syncDirectory(descriptorStaging);
await syncHandle(stagingHandle);
await syncHandle(parentHandle);
await afterStagingWrite?.();
await beforeSeal?.();
const capturedFiles = await captureStagedFiles(stagingHandle, files);
await sealStagedFiles?.(capturedFiles);
const after = await lstat(parentPath);
if (
after.dev !== before.dev ||
@@ -335,20 +540,102 @@ async function publishPrivateStaging(
if (!visible.isDirectory() || visible.isSymbolicLink()) {
throw new Error("promotion staging visibility identity mismatch");
}
assertStagingIdentity(visible, stagingIdentity);
ownsStaging = false;
return Object.freeze({
finalizedPromotion = Object.freeze({
stagingRoot: visibleStaging,
cleanupToken,
runnerTempIdentity: Object.freeze({ dev: before.dev, ino: before.ino }),
stagingIdentity,
files: Object.freeze(
files.map(({ name, sha256: digest }) => Object.freeze({ name, sha256: digest })),
),
});
} finally {
if (ownsStaging) {
await rm(descriptorStaging, { recursive: true, force: true }).catch(() => undefined);
} catch (error) {
await cleanup(error);
throw error;
}
await cleanup();
return finalizedPromotion;
}
async function captureStagedFiles(
stagingHandle: Awaited<ReturnType<typeof open>>,
declaredFiles: readonly StagedFile[],
): Promise<Readonly<Record<PromotedFileName, Buffer>>> {
const descriptorRoot = `/proc/self/fd/${stagingHandle.fd}`;
const names = (await readdir(descriptorRoot)).sort(asciiCompare);
if (
JSON.stringify(names) !==
JSON.stringify([...PROMOTED_FILE_NAMES].sort(asciiCompare))
) {
throw new Error("staged promotion seal requires exactly the canonical five files");
}
const declared = new Map(declaredFiles.map((file) => [file.name, file] as const));
const captured = {} as Record<PromotedFileName, Buffer>;
for (const name of PROMOTED_FILE_NAMES) {
const expected = declared.get(name)!;
const handle = await open(
path.join(descriptorRoot, name),
constants.O_RDONLY | constants.O_NOFOLLOW,
);
try {
const before = await handle.stat();
const maxBytes = name === "release-candidate.tar.gz" ? 268_435_456 : 16_777_216;
if (
!before.isFile() ||
before.nlink !== 1 ||
(before.mode & 0o777) !== 0o400 ||
before.size <= 0 ||
before.size > maxBytes
) {
throw new Error(
`staged promotion file must be regular, single-link, bounded, and mode 0400: ${name}`,
);
}
const bytes = await handle.readFile();
const after = await handle.stat();
if (
after.dev !== before.dev ||
after.ino !== before.ino ||
after.size !== before.size ||
after.nlink !== 1 ||
(after.mode & 0o777) !== 0o400 ||
bytes.byteLength !== before.size
) {
throw new Error(`staged promotion file inode or size changed during seal: ${name}`);
}
if (sha256(bytes) !== expected.sha256) {
throw new Error(`staged promotion file digest mismatch during seal: ${name}`);
}
captured[name] = bytes;
} finally {
await handle.close();
}
await parentHandle.close();
}
return Object.freeze(captured);
}
function asciiCompare(left: string, right: string): number {
return left < right ? -1 : left > right ? 1 : 0;
}
function assertStagingIdentity(
metadata: Readonly<{
dev: number;
ino: number;
isDirectory: () => boolean;
isSymbolicLink?: () => boolean;
}>,
expected: Readonly<{ dev: number; ino: number }>,
): void {
if (
metadata.dev !== expected.dev ||
metadata.ino !== expected.ino ||
!metadata.isDirectory() ||
metadata.isSymbolicLink?.()
) {
throw new Error("promotion staging leaf identity changed");
}
}
@@ -409,18 +696,6 @@ function sha256(bytes: Buffer): string {
return createHash("sha256").update(bytes).digest("hex");
}
async function syncDirectory(directory: string): Promise<void> {
const handle = await open(
directory,
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
);
try {
await syncHandle(handle);
} finally {
await handle.close();
}
}
async function syncHandle(handle: Awaited<ReturnType<typeof open>>): Promise<void> {
try {
await handle.sync();
-333
View File
@@ -1,333 +0,0 @@
import { createHash, createPublicKey } from "node:crypto";
import path from "node:path";
import {
PROMOTION_VERIFIER_ID,
PROMOTION_VERIFIER_VERSION,
evaluatePromotionEvidence,
providerPublicKeyFingerprint,
trustPolicySha256,
type ProviderVerificationArtifactType,
type ProviderTrust,
} from "./provider-evidence.ts";
import {
RELEASE_CANDIDATE_MANIFEST_PATH,
releaseCandidateManifestSchema,
verifyReleaseCandidate,
} from "./release-candidate.ts";
import { verifyArchivedLocalEvidence } from "./local-release-evidence.ts";
import { readBoundedRegularFile } from "./ci-artifact-validator.ts";
import { supplyChainDigest } from "./supply-chain.ts";
type LocalEvidenceVerifier = typeof verifyArchivedLocalEvidence;
export type VerifyPromotionInputsOptions = Readonly<{
artifactType: ProviderVerificationArtifactType;
environment?: NodeJS.ProcessEnv;
repositoryRoot?: string;
providerEvidenceRoot?: string;
trustRoot?: string;
verifyLocalEvidence?: LocalEvidenceVerifier;
nowEpochMs?: () => number;
}>;
export async function verifyPromotionInputs(
options: VerifyPromotionInputsOptions,
) {
const environment = options.environment ?? process.env;
const repositoryRoot = path.resolve(options.repositoryRoot ?? process.cwd());
const trustRoot = path.resolve(options.trustRoot ?? repositoryRoot);
const providerEvidenceRoot = path.resolve(
options.providerEvidenceRoot ?? repositoryRoot,
);
const inputFailures: string[] = [];
const archive = await captureOptionalInput(
providerEvidenceRoot,
environment.CANDIDATE_ARCHIVE_PATH,
268_435_456,
"candidate archive",
inputFailures,
);
if (!environment.CANDIDATE_ARCHIVE_SHA256) {
inputFailures.push("candidate archive expected SHA-256 is missing");
} else if (
archive.sha256 &&
archive.sha256 !== environment.CANDIDATE_ARCHIVE_SHA256
) {
inputFailures.push("candidate archive SHA-256 does not match immutable output");
}
const vulnerabilityCapture = await captureOptionalInput(
providerEvidenceRoot,
environment.VULNERABILITY_REPORT_PATH,
16_777_216,
"vulnerability report",
inputFailures,
);
const provenanceCapture = await captureOptionalInput(
providerEvidenceRoot,
environment.PROVENANCE_ATTESTATION_PATH,
16_777_216,
"provenance attestation",
inputFailures,
);
const manifestDocument = await requiredJson(
repositoryRoot,
RELEASE_CANDIDATE_MANIFEST_PATH,
);
const manifest = releaseCandidateManifestSchema.parse(manifestDocument);
const candidate = await verifyReleaseCandidate(
manifestDocument,
repositoryRoot,
);
const localEvidence = await (
options.verifyLocalEvidence ?? verifyArchivedLocalEvidence
)({ extractionRoot: repositoryRoot, expectedManifest: manifest });
const vulnerabilityReport = parseCapturedJson(vulnerabilityCapture.bytes);
const provenanceAttestation = parseCapturedJson(provenanceCapture.bytes);
const vulnerabilityTrust = await readProviderTrust(
trustRoot,
environment.VULNERABILITY_PUBLIC_KEY_PATH,
environment.VULNERABILITY_KEY_ID,
);
const provenanceTrust = await readProviderTrust(
trustRoot,
environment.PROVENANCE_PUBLIC_KEY_PATH,
environment.PROVENANCE_KEY_ID,
);
const runId = environment.CI_RUN_ID ?? "missing-run";
const runAttempt = Number(environment.CI_RUN_ATTEMPT);
if (!environment.CI_RUN_ID) inputFailures.push("provider expected run ID is missing");
if (!Number.isInteger(runAttempt) || runAttempt < 1 || runAttempt > 1_000) {
inputFailures.push("provider expected run attempt is missing or invalid");
}
if (!localEvidence.identity) {
inputFailures.push("archived local evidence identity is unavailable");
}
if (
environment.EXPECTED_SOURCE_REVISION &&
localEvidence.identity &&
environment.EXPECTED_SOURCE_REVISION !== localEvidence.identity.sourceRevision
) {
inputFailures.push(
`provider expected source revision mismatch: expected ${environment.EXPECTED_SOURCE_REVISION}, archived ${localEvidence.identity.sourceRevision}`,
);
}
const vulnerabilityInvocationNonce = requiredExpectedNonce(
environment.VULNERABILITY_INVOCATION_NONCE,
"vulnerability",
inputFailures,
);
const provenanceInvocationNonce = requiredExpectedNonce(
environment.PROVENANCE_INVOCATION_NONCE,
"provenance",
inputFailures,
);
const expected = {
run: { id: runId, attempt: Number.isInteger(runAttempt) ? runAttempt : 1 },
source: {
revision:
localEvidence.identity?.sourceRevision ??
environment.EXPECTED_SOURCE_REVISION ??
"0".repeat(40),
sourceSetSha256: localEvidence.identity?.sourceSetSha256 ?? "0".repeat(64),
},
candidate: {
archiveSha256: archive.sha256 ?? "0".repeat(64),
bundleSha256: manifest.bundleSha256,
distSha256: manifest.distSha256,
lockfileSha256: manifest.lockfileSha256,
},
vulnerabilityInvocationNonce,
provenanceInvocationNonce,
} as const;
const result = evaluatePromotionEvidence({
expected,
localStatus: localEvidence.status,
vulnerabilityReport,
provenanceAttestation,
vulnerabilityTrust,
provenanceTrust,
nowEpochMs: options.nowEpochMs,
});
const failures = [
...inputFailures,
...candidate.failures,
...localEvidence.failures,
...result.failures,
];
const now = (options.nowEpochMs ?? Date.now)();
const common = {
schemaVersion: 3 as const,
artifactType: options.artifactType,
verifiedAt: new Date(now).toISOString(),
status:
failures.length === 0 && result.status === "PASS"
? ("PASS" as const)
: ("FAIL_UNVERIFIED" as const),
verifier: Object.freeze({
id: PROMOTION_VERIFIER_ID,
version: PROMOTION_VERIFIER_VERSION,
}),
run: expected.run,
source: expected.source,
candidate: expected.candidate,
providerEvidence: Object.freeze({
vulnerabilityReportSha256: vulnerabilityCapture.sha256 ?? "0".repeat(64),
provenanceAttestationSha256: provenanceCapture.sha256 ?? "0".repeat(64),
vulnerabilityInvocationNonce: expected.vulnerabilityInvocationNonce,
provenanceInvocationNonce: expected.provenanceInvocationNonce,
vulnerabilityKeyId:
vulnerabilityTrust?.keyId ?? environment.VULNERABILITY_KEY_ID ?? "missing-key",
vulnerabilityKeyFingerprint:
vulnerabilityTrust?.publicKeyFingerprint ?? `sha256:${"0".repeat(64)}`,
provenanceKeyId:
provenanceTrust?.keyId ?? environment.PROVENANCE_KEY_ID ?? "missing-key",
provenanceKeyFingerprint:
provenanceTrust?.publicKeyFingerprint ?? `sha256:${"0".repeat(64)}`,
}),
trustPolicySha256: verificationTrustPolicySha256(
vulnerabilityTrust,
provenanceTrust,
environment,
),
failures: Object.freeze(failures),
};
return options.artifactType === "provider-verification"
? Object.freeze({
...common,
artifactType: "provider-verification" as const,
vulnerabilityStatus: result.vulnerabilityStatus,
provenanceAttestationStatus: result.provenanceAttestationStatus,
})
: Object.freeze({
...common,
artifactType: "promotion-verification" as const,
localEvidenceStatus: localEvidence.status,
localEvidenceAssessmentSha256:
localEvidence.identity?.assessmentSha256 ?? "0".repeat(64),
providerVerificationSha256:
environment.PROVIDER_VERIFICATION_SHA256 ?? "0".repeat(64),
});
}
function requiredExpectedNonce(
value: string | undefined,
label: "vulnerability" | "provenance",
failures: string[],
): string {
if (value && /^[a-f0-9]{64}$/u.test(value)) return value;
failures.push(`${label} expected invocation nonce is missing or invalid`);
return "0".repeat(64);
}
function verificationTrustPolicySha256(
vulnerabilityTrust: ProviderTrust | null,
provenanceTrust: ProviderTrust | null,
environment: NodeJS.ProcessEnv,
): string {
if (vulnerabilityTrust && provenanceTrust) {
return trustPolicySha256({ vulnerabilityTrust, provenanceTrust });
}
return supplyChainDigest({
algorithm: "Ed25519",
vulnerability: {
keyId: vulnerabilityTrust?.keyId ?? environment.VULNERABILITY_KEY_ID ?? "missing-key",
publicKeyFingerprint:
vulnerabilityTrust?.publicKeyFingerprint ?? `sha256:${"0".repeat(64)}`,
},
provenance: {
keyId: provenanceTrust?.keyId ?? environment.PROVENANCE_KEY_ID ?? "missing-key",
publicKeyFingerprint:
provenanceTrust?.publicKeyFingerprint ?? `sha256:${"0".repeat(64)}`,
},
issuedAtFutureSkewMs: 5 * 60 * 1_000,
maximumLifetimeMs: 2 * 60 * 60 * 1_000,
});
}
export async function readProviderTrust(
repositoryRoot: string,
publicKeyPath: string | undefined,
keyId: string | undefined,
): Promise<ProviderTrust | null> {
if (!publicKeyPath || !keyId?.trim()) return null;
try {
const publicKey = createPublicKey(
new TextDecoder("utf-8", { fatal: true }).decode(
await boundedConfiguredFile(repositoryRoot, publicKeyPath, 1_048_576),
),
);
return Object.freeze({
keyId,
publicKey,
publicKeyFingerprint: providerPublicKeyFingerprint(publicKey),
});
} catch {
return null;
}
}
async function captureOptionalInput(
root: string,
configuredPath: string | undefined,
maxBytes: number,
label: string,
failures: string[],
): Promise<Readonly<{ bytes: Buffer | null; sha256: string | null }>> {
if (!configuredPath) {
failures.push(`${label} path is missing`);
return Object.freeze({ bytes: null, sha256: null });
}
try {
const bytes = await boundedConfiguredFile(root, configuredPath, maxBytes);
return Object.freeze({
bytes,
sha256: createHash("sha256").update(bytes).digest("hex"),
});
} catch (error) {
failures.push(
`${label} capture failed: ${error instanceof Error ? error.message : String(error)}`,
);
return Object.freeze({ bytes: null, sha256: null });
}
}
function parseCapturedJson(bytes: Buffer | null): unknown {
if (!bytes) return null;
try {
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) as unknown;
} catch {
return null;
}
}
async function requiredJson(
repositoryRoot: string,
file: string,
): Promise<Record<string, unknown>> {
const value: unknown = JSON.parse(
new TextDecoder("utf-8", { fatal: true }).decode(
await boundedConfiguredFile(repositoryRoot, file, 8_388_608),
),
);
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new TypeError(`${file} must be a JSON object`);
}
return value as Record<string, unknown>;
}
async function boundedConfiguredFile(
configuredRoot: string,
configuredPath: string,
maxBytes: number,
): Promise<Buffer> {
const root = path.resolve(configuredRoot);
const absolute = path.resolve(root, configuredPath);
const relative = path.relative(root, absolute);
const outside = relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
return readBoundedRegularFile({
root: outside ? path.dirname(absolute) : root,
relativePath: outside ? path.basename(absolute) : relative.replaceAll(path.sep, "/"),
maxBytes,
});
}
+169
View File
@@ -0,0 +1,169 @@
export type ProviderKind = "vulnerability" | "provenance";
const MEMORY_MAX = 1_073_741_824;
const TASKS_MAX = 64;
const STOP_TIMEOUT_MS = 5_000;
const RUNTIME_GRACE_MS = 10_000;
const UNIT_NAME = /^ca-provider-(?:vulnerability|provenance)-[1-9][0-9]*-[0-9a-f]{24}\.scope$/u;
const UNIT_NONCE = /^[0-9a-f]{24}$/u;
const ENVIRONMENT_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/u;
const ENFORCEMENT_GATE = [
'cgroup_path=""',
"while IFS=: read -r hierarchy controllers candidate; do",
' if [ "$hierarchy" = 0 ] && [ -z "$controllers" ]; then cgroup_path=$candidate; fi',
"done < /proc/self/cgroup",
'if [ -z "$cgroup_path" ]; then',
" printf '%s\\n' 'provider cgroup enforcement failed: unified cgroup v2 membership is required' >&2",
" exit 125",
"fi",
'case "$cgroup_path" in',
' */"$0") ;;',
" *)",
" printf '%s\\n' 'provider cgroup enforcement failed: unit membership is invalid' >&2",
" exit 125",
" ;;",
"esac",
"cgroup_root=/sys/fs/cgroup$cgroup_path",
"require_cgroup_value() {",
' actual=$(/bin/cat "$cgroup_root/$1") || {',
" printf 'provider cgroup enforcement failed: cannot read %s\\n' \"$1\" >&2",
" exit 125",
" }",
' if [ "$actual" != "$2" ]; then',
" printf 'provider cgroup enforcement failed: %s is %s, expected %s\\n' \"$1\" \"$actual\" \"$2\" >&2",
" exit 125",
" fi",
"}",
`require_cgroup_value memory.max ${MEMORY_MAX}`,
"require_cgroup_value memory.swap.max 0",
`require_cgroup_value pids.max ${TASKS_MAX}`,
"require_cgroup_value cpu.max '100000 100000'",
'exec "$@"',
].join("\n");
export function formatProviderCgroupUnitName(
kind: ProviderKind,
supervisorPid: number,
nonce: string,
): string {
if (!Number.isSafeInteger(supervisorPid) || supervisorPid <= 0 || !UNIT_NONCE.test(nonce)) {
throw new TypeError("provider cgroup unit identity is invalid");
}
const unit = `ca-provider-${kind}-${supervisorPid}-${nonce}.scope`;
assertUnit(unit);
return unit;
}
export function systemdRunProviderArguments(
unit: string,
timeoutMs: number,
cpuSeconds: number,
nodeExecutable: string,
wrapperScript: string,
reportPath: string,
reportDev: number,
reportIno: number,
): string[] {
assertUnit(unit);
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > Number.MAX_SAFE_INTEGER - RUNTIME_GRACE_MS) {
throw new TypeError("provider cgroup runtime is invalid");
}
if (!Number.isSafeInteger(cpuSeconds) || cpuSeconds <= 0) {
throw new TypeError("provider cgroup CPU limit is invalid");
}
if (
!nodeExecutable.startsWith("/") || !wrapperScript.startsWith("/") ||
!reportPath.startsWith("/") || reportPath.includes("\0") ||
!Number.isSafeInteger(reportDev) || reportDev <= 0 ||
!Number.isSafeInteger(reportIno) || reportIno <= 0
) {
throw new TypeError("provider scope wrapper path is invalid");
}
return [
"--user",
"--scope",
"--collect",
"--quiet",
"--expand-environment=no",
`--unit=${unit}`,
`--property=MemoryMax=${MEMORY_MAX}`,
"--property=MemorySwapMax=0",
`--property=TasksMax=${TASKS_MAX}`,
"--property=CPUQuota=100%",
"--property=CPUQuotaPeriodSec=100ms",
"--property=KillMode=control-group",
"--property=SendSIGKILL=yes",
`--property=TimeoutStopSec=${STOP_TIMEOUT_MS}ms`,
`--property=RuntimeMaxSec=${timeoutMs + RUNTIME_GRACE_MS}ms`,
"--",
"/bin/sh",
"-eu",
"-c",
ENFORCEMENT_GATE,
unit,
nodeExecutable,
wrapperScript,
String(cpuSeconds),
reportPath,
String(reportDev),
String(reportIno),
];
}
export type ProviderScopeFrame = Readonly<{
bwrapInput: Buffer;
reportPath: string;
reportDev: number;
reportIno: number;
}>;
export function encodeProviderScopeFrame(input: ProviderScopeFrame): Buffer {
if (
!Buffer.isBuffer(input.bwrapInput) || input.bwrapInput.byteLength === 0 ||
!input.reportPath.startsWith("/") || input.reportPath.includes("\0") ||
!Number.isSafeInteger(input.reportDev) || input.reportDev <= 0 ||
!Number.isSafeInteger(input.reportIno) || input.reportIno <= 0
) {
throw new TypeError("provider scope frame is invalid");
}
const payload = Buffer.from(JSON.stringify({
bwrapInputBase64: input.bwrapInput.toString("base64"),
reportPath: input.reportPath,
reportDev: input.reportDev,
reportIno: input.reportIno,
}));
const frame = Buffer.allocUnsafe(4 + payload.byteLength);
frame.writeUInt32BE(payload.byteLength, 0);
payload.copy(frame, 4);
return frame;
}
export function encodeProviderBwrapInput(
arguments_: readonly string[],
environment: Readonly<Record<string, string | undefined>>,
): Buffer {
if (arguments_.some((argument) => argument.includes("\0"))) {
throw new TypeError("provider bwrap argument is invalid");
}
const entries = Object.entries(environment).sort(([left], [right]) =>
left < right ? -1 : left > right ? 1 : 0,
);
if (entries.some(([name, value]) =>
!ENVIRONMENT_NAME.test(name) || value === undefined || value.includes("\0")
)) {
throw new TypeError("provider bwrap environment is invalid");
}
const input = ["--clearenv"];
for (const [name, value] of entries) input.push("--setenv", name, value ?? "");
input.push(...arguments_);
return Buffer.from(`${input.join("\0")}\0`);
}
export function systemctlKillProviderArguments(unit: string): string[] {
assertUnit(unit);
return ["--user", "kill", "--kill-whom=all", "--signal=SIGKILL", unit];
}
function assertUnit(unit: string): void {
if (!UNIT_NAME.test(unit)) throw new TypeError("provider cgroup unit name is invalid");
}
+46
View File
@@ -37,6 +37,16 @@ const candidateSchema = z
})
.strict();
const providerRunSchema = runSchema.extend({ invocationNonce: nonce }).strict();
export const secretScanAttestationSchema = z
.object({
status: z.literal("PASS"),
localEvidenceAssessmentSha256: sha256,
sourceSetSha256: sha256,
policySha256: sha256,
sarifSha256: sha256,
scanInputSha256: sha256,
})
.strict();
const signatureSchema = z
.object({
algorithm: z.literal("Ed25519"),
@@ -60,6 +70,7 @@ export const vulnerabilityProviderReportSchema = z
.object({
...providerCommon,
evidenceType: z.literal("vulnerability-report"),
secretScanAttestation: secretScanAttestationSchema,
findings: z.array(z.record(z.string(), z.json())),
})
.strict();
@@ -95,6 +106,7 @@ const verificationCommon = {
vulnerabilityKeyFingerprint: fingerprint,
provenanceKeyId: nonEmptyString,
provenanceKeyFingerprint: fingerprint,
secretScanAttestation: secretScanAttestationSchema,
})
.strict(),
trustPolicySha256: sha256,
@@ -169,6 +181,7 @@ export type ExpectedPromotionContext = Readonly<{
}>;
vulnerabilityInvocationNonce: string;
provenanceInvocationNonce: string;
secretScanAttestation: z.infer<typeof secretScanAttestationSchema>;
}>;
export type PromotionEvidenceResult = Readonly<{
@@ -211,6 +224,12 @@ export function validateProviderEvidence(input: Readonly<{
now,
failures,
);
if (
JSON.stringify(parsed.data.secretScanAttestation) !==
JSON.stringify(input.expected.secretScanAttestation)
) {
failures.push("vulnerability report secret scan attestation mismatch");
}
if (parsed.data.findings.length > 0) {
failures.push("vulnerability report contains findings");
}
@@ -268,6 +287,7 @@ export function createTrustPolicy(input: Readonly<{
vulnerabilityTrust: ProviderTrust;
provenanceTrust: ProviderTrust;
}>) {
assertDistinctProviderTrust(input);
return Object.freeze({
algorithm: "Ed25519" as const,
vulnerability: Object.freeze({
@@ -283,6 +303,26 @@ export function createTrustPolicy(input: Readonly<{
});
}
export function assertDistinctProviderTrust(input: Readonly<{
vulnerabilityTrust: ProviderTrust;
provenanceTrust: ProviderTrust;
}>): void {
const vulnerabilityFingerprint = providerPublicKeyFingerprint(
input.vulnerabilityTrust.publicKey,
);
const provenanceFingerprint = providerPublicKeyFingerprint(
input.provenanceTrust.publicKey,
);
if (
input.vulnerabilityTrust.keyId === input.provenanceTrust.keyId ||
vulnerabilityFingerprint === provenanceFingerprint ||
input.vulnerabilityTrust.publicKeyFingerprint ===
input.provenanceTrust.publicKeyFingerprint
) {
throw new TypeError("provider trust roles require distinct key identities and DER-SPKI fingerprints");
}
}
export function trustPolicySha256(input: Readonly<{
vulnerabilityTrust: ProviderTrust;
provenanceTrust: ProviderTrust;
@@ -323,6 +363,12 @@ export function evaluatePromotionEvidence(input: Readonly<{
now,
failures,
);
if (
JSON.stringify(vulnerability.data.secretScanAttestation) !==
JSON.stringify(input.expected.secretScanAttestation)
) {
failures.push("vulnerability report secret scan attestation mismatch");
}
if (vulnerability.data.findings.length > 0) {
failures.push("vulnerability report contains findings");
}
+763
View File
@@ -0,0 +1,763 @@
import { spawn } from "node:child_process";
import { createHash, randomBytes } from "node:crypto";
import { constants } from "node:fs";
import { lstat, open, type FileHandle } from "node:fs/promises";
import path from "node:path";
import {
decodeProviderGuardianPublished,
decodeProviderGuardianReady,
encodeProviderGuardianCommit,
encodeProviderGuardianGuard,
encodeProviderGuardianPublish,
MAX_PROVIDER_GUARDIAN_FRAME_PAYLOAD_BYTES,
MAX_PROVIDER_GUARDIAN_LEASE_MS,
MAX_PROVIDER_SEALED_BYTES,
providerGuardianRawStagingLeaf,
providerGuardianSealedTempLeaf,
type ProviderGuardianKind,
} from "./provider-guardian-protocol.ts";
import { cleanupOwnedProviderReport } from "./provider-raw-cleanup.ts";
const RESPONSE_TIMEOUT_MS = 5_000;
const CLOSE_TIMEOUT_MS = 5_000;
const MAX_CONTROL_OUTPUT_BYTES = 4_096;
type OwnedIdentity = Readonly<{ dev: number; ino: number }>;
type RecoveryAuthority = Readonly<{
rawDirectoryHandle: FileHandle;
evidenceDirectoryHandle: FileHandle;
rawStagingHandle: FileHandle;
sealedTempHandle: FileHandle;
rawIdentity: OwnedIdentity;
sealedIdentity: OwnedIdentity;
rawStagingPinnedPath: string;
rawPinnedPath: string;
sealedTempPinnedPath: string;
sealedPinnedPath: string;
}>;
export type ProviderGuardianLease = Readonly<{
pid: number;
rawPath: string;
rawIdentity: OwnedIdentity;
sealedPath: string;
sealedTempPath: string;
sealedIdentity: OwnedIdentity;
prematureExit: Promise<Error>;
publish(bytes: Buffer): Promise<void>;
commit(): Promise<void>;
abort(): Promise<void>;
}>;
export type StartProviderGuardianInput = Readonly<{
kind: ProviderGuardianKind;
workspaceRoot: string;
leaseMs: number;
guardianScript: string;
}>;
export type ProviderScopeGuardianLatch = Readonly<{
activeFailure: Promise<Error>;
close(): Promise<void>;
failure(): Error | undefined;
}>;
export function createProviderScopeGuardianLatch(
guardianExit: Promise<Error>,
): ProviderScopeGuardianLatch {
let active = true;
let closing: Promise<void> | undefined;
let observedFailure: Error | undefined;
let signalActiveFailure!: (error: Error) => void;
const activeFailure = new Promise<Error>((resolve) => { signalActiveFailure = resolve; });
void guardianExit.then((error) => {
observedFailure = error;
if (active) signalActiveFailure(error);
});
return Object.freeze({
activeFailure,
close: () => {
closing ??= Promise.resolve().then(() => { active = false; });
return closing;
},
failure: () => observedFailure,
});
}
export function assertProviderGuardianLeasePaths(
lease: Readonly<{ rawPath: string; sealedPath: string }>,
expected: Readonly<{ rawPath: string; sealedPath: string }>,
): void {
if (lease.rawPath !== expected.rawPath) {
throw new Error("provider guardian returned a noncanonical raw path");
}
if (lease.sealedPath !== expected.sealedPath) {
throw new Error("provider guardian returned a noncanonical sealed path");
}
}
type GuardianResult = Readonly<{
code: number | null;
error?: Error;
signal: NodeJS.Signals | null;
}>;
export async function startProviderGuardian(
input: StartProviderGuardianInput,
): Promise<ProviderGuardianLease> {
if (
(input.kind !== "vulnerability" && input.kind !== "provenance") ||
!path.isAbsolute(input.workspaceRoot) || !path.isAbsolute(input.guardianScript) ||
!Number.isSafeInteger(input.leaseMs) || input.leaseMs <= 0 ||
input.leaseMs > MAX_PROVIDER_GUARDIAN_LEASE_MS
) {
throw new TypeError("provider guardian client input is invalid");
}
const rawLeaf = input.kind === "vulnerability"
? "vulnerability-report.json"
: "provenance-attestation.json";
const evidenceRoot = path.resolve(input.workspaceRoot, "provider-evidence");
const rawDirectory = path.join(evidenceRoot, "untrusted");
const rawPath = path.join(evidenceRoot, "untrusted", rawLeaf);
const sealedPath = path.join(evidenceRoot, rawLeaf);
const nonce = randomBytes(32);
const rawStagingLeaf = providerGuardianRawStagingLeaf(input.kind, nonce);
const sealedTempLeaf = providerGuardianSealedTempLeaf(input.kind, nonce);
const sealedTempPath = path.join(evidenceRoot, sealedTempLeaf);
const recovery = await openRecoveryAuthority({
rawDirectory,
evidenceRoot,
rawLeaf,
rawStagingLeaf,
sealedLeaf: rawLeaf,
sealedTempLeaf,
});
let child: ReturnType<typeof spawn>;
try {
await assertRecoveryLeavesMissing(recovery);
child = spawn(process.execPath, [input.guardianScript], {
cwd: input.workspaceRoot,
env: {},
stdio: [
"pipe",
"pipe",
"pipe",
recovery.rawDirectoryHandle.fd,
recovery.evidenceDirectoryHandle.fd,
recovery.rawStagingHandle.fd,
recovery.sealedTempHandle.fd,
],
});
} catch (error) {
return await closeRecoveryAndThrow(recovery, error);
}
if (!child.pid || !child.stdin || !child.stdout || !child.stderr) {
child.kill("SIGKILL");
return await closeRecoveryAndThrow(
recovery,
new Error("provider guardian process pipes are unavailable"),
);
}
let state: "starting" | "guarding" | "publishing" | "published" |
"committing" | "aborting" | "terminated" = "starting";
let stderr = Buffer.alloc(0);
let inputError: Error | undefined;
child.stdin.once("error", (error) => { inputError = error; });
child.stderr.on("data", (chunk: Buffer | string) => {
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
if (stderr.byteLength < MAX_CONTROL_OUTPUT_BYTES) {
stderr = Buffer.concat([stderr, bytes.subarray(0, MAX_CONTROL_OUTPUT_BYTES - stderr.byteLength)]);
}
});
const completion = guardianCompletion(child);
let signalPrematureExit!: (error: Error) => void;
const prematureExit = new Promise<Error>((resolve) => { signalPrematureExit = resolve; });
void completion.then((result) => {
if (state === "guarding" || state === "publishing" || state === "published") {
signalPrematureExit(guardianCloseError(result, stderr));
}
});
let ready: ReturnType<typeof decodeProviderGuardianReady>;
try {
const readyResponse = waitForFrame(child.stdout, completion, "READY");
child.stdin.write(encodeProviderGuardianGuard({
kind: input.kind,
nonce,
deadlineEpochMs: Date.now() + input.leaseMs,
}));
ready = decodeProviderGuardianReady(await readyResponse, nonce);
if (
ready.sealedTempLeaf !== sealedTempLeaf ||
ready.rawDev !== recovery.rawIdentity.dev ||
ready.rawIno !== recovery.rawIdentity.ino ||
ready.sealedDev !== recovery.sealedIdentity.dev ||
ready.sealedIno !== recovery.sealedIdentity.ino
) {
throw new TypeError("provider guardian READY identity is invalid for its allocation");
}
await assertPinnedLeafIdentity(recovery.rawPinnedPath, {
dev: ready.rawDev,
ino: ready.rawIno,
}, 0o600);
await assertPinnedLeafIdentity(recovery.sealedTempPinnedPath, {
dev: ready.sealedDev,
ino: ready.sealedIno,
}, 0o600);
await assertPinnedLeafMissing(recovery.rawStagingPinnedPath);
if (child.exitCode !== null || child.signalCode !== null) {
throw guardianCloseError(await completion, stderr);
}
state = "guarding";
} catch (error) {
state = "aborting";
child.stdin.end();
const failures = [toError(error)];
try {
await waitForClose(completion, child);
} catch (closeError) {
failures.push(toError(closeError));
}
try {
await cleanupStartupRecovery(recovery);
} catch (cleanupError) {
failures.push(toError(cleanupError));
}
try {
await closeRecoveryAuthority(recovery);
} catch (closeError) {
failures.push(toError(closeError));
}
state = "terminated";
if (failures.length > 1) {
throw new AggregateError(failures, "provider guardian startup failed", { cause: error });
}
throw failures[0]!;
}
const rawIdentity = Object.freeze({ dev: ready.rawDev, ino: ready.rawIno });
const sealedIdentity = Object.freeze({ dev: ready.sealedDev, ino: ready.sealedIno });
const fallback = Object.freeze({
rawStagingPath: recovery.rawStagingPinnedPath,
rawPath: recovery.rawPinnedPath,
rawIdentity,
sealedPath: recovery.sealedPinnedPath,
sealedTempPath: recovery.sealedTempPinnedPath,
sealedIdentity,
});
const publish = async (bytes: Buffer): Promise<void> => {
if (state !== "guarding") throw new Error("provider guardian lease is not ready to publish");
if (!Buffer.isBuffer(bytes) || bytes.byteLength <= 0 || bytes.byteLength > MAX_PROVIDER_SEALED_BYTES) {
throw new TypeError("provider guardian sealed bytes are invalid");
}
state = "publishing";
try {
await writePinnedSealedBytes(recovery.sealedTempHandle, sealedIdentity, bytes);
const publishedResponse = waitForFrame(child.stdout!, completion, "PUBLISHED");
child.stdin!.write(encodeProviderGuardianPublish({
nonce,
sealedDev: sealedIdentity.dev,
sealedIno: sealedIdentity.ino,
size: bytes.byteLength,
sha256: createHash("sha256").update(bytes).digest("hex"),
}));
decodeProviderGuardianPublished(
await publishedResponse,
nonce,
sealedIdentity,
);
if (inputError) throw inputError;
state = "published";
} catch (error) {
state = "guarding";
throw error;
}
};
const commit = async (): Promise<void> => {
if (state !== "published") throw new Error("provider guardian lease is not ready to commit");
state = "committing";
child.stdin!.write(encodeProviderGuardianCommit(nonce));
child.stdin!.end();
let result: GuardianResult;
try {
result = await waitForClose(completion, child);
} catch (error) {
state = "terminated";
return await cleanupFallbackCloseAndThrow(fallback, recovery, error);
}
state = "terminated";
if (inputError) return await cleanupFallbackCloseAndThrow(fallback, recovery, inputError);
if (result.error || result.code !== 0 || result.signal !== null) {
return await cleanupFallbackCloseAndThrow(
fallback,
recovery,
guardianCloseError(result, stderr),
);
}
await closeRecoveryAuthority(recovery);
};
const abort = async (): Promise<void> => {
if (state !== "guarding" && state !== "published") {
throw new Error("provider guardian lease already terminated");
}
state = "aborting";
child.stdin!.end();
let closeError: unknown;
try {
await waitForClose(completion, child);
} catch (error) {
closeError = error;
}
state = "terminated";
if (closeError) return await cleanupFallbackCloseAndThrow(fallback, recovery, closeError);
await cleanupFallbackAndClose(fallback, recovery);
};
return Object.freeze({
pid: child.pid,
rawPath,
rawIdentity,
sealedPath,
sealedTempPath,
sealedIdentity,
prematureExit,
publish,
commit,
abort,
});
}
async function writePinnedSealedBytes(
handle: FileHandle,
identity: OwnedIdentity,
bytes: Buffer,
): Promise<void> {
assertPinnedMetadata(await handle.stat(), identity, 0o600, 0);
await handle.truncate(0);
await handle.writeFile(bytes);
await handle.chmod(0o400);
await handle.sync();
assertPinnedMetadata(await handle.stat(), identity, 0o400, bytes.byteLength);
}
function assertPinnedMetadata(
metadata: Awaited<ReturnType<Awaited<ReturnType<typeof open>>["stat"]>>,
identity: OwnedIdentity,
mode: number,
size: number,
): void {
if (
!metadata.isFile() || Number(metadata.dev) !== identity.dev ||
Number(metadata.ino) !== identity.ino || Number(metadata.nlink) !== 1 ||
(Number(metadata.mode) & 0o777) !== mode || Number(metadata.size) !== size
) {
throw new TypeError("provider guardian sealed temp identity changed");
}
}
function guardianCompletion(child: ReturnType<typeof spawn>): Promise<GuardianResult> {
return new Promise((resolve) => {
child.once("error", (error) => resolve({ code: null, error, signal: null }));
child.once("close", (code, signal) => resolve({ code, signal }));
});
}
async function waitForFrame(
stdout: NodeJS.ReadableStream,
completion: Promise<GuardianResult>,
label: string,
): Promise<Buffer> {
let timer: NodeJS.Timeout | undefined;
let pending = Buffer.alloc(0);
const response = new Promise<Buffer>((resolve, reject) => {
const onData = (chunk: Buffer | string): void => {
pending = Buffer.concat([pending, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
if (pending.byteLength > MAX_CONTROL_OUTPUT_BYTES) {
reject(new Error(`provider guardian ${label} output exceeded its bound`));
return;
}
if (pending.byteLength < 4) return;
const payloadBytes = pending.readUInt32BE(0);
if (payloadBytes <= 0 || payloadBytes > MAX_PROVIDER_GUARDIAN_FRAME_PAYLOAD_BYTES) {
reject(new Error(`provider guardian ${label} frame length is invalid`));
return;
}
if (pending.byteLength < payloadBytes + 4) return;
if (pending.byteLength !== payloadBytes + 4) {
reject(new Error(`provider guardian ${label} output has trailing bytes`));
return;
}
resolve(pending.subarray(4));
};
stdout.on("data", onData);
});
try {
return await Promise.race([
response,
completion.then((result) => { throw guardianCloseError(result, Buffer.alloc(0)); }),
new Promise<never>((_resolve, reject) => {
timer = setTimeout(
() => reject(new Error(`provider guardian ${label} timed out`)),
RESPONSE_TIMEOUT_MS,
);
}),
]);
} finally {
if (timer) clearTimeout(timer);
stdout.removeAllListeners("data");
}
}
async function waitForClose(
completion: Promise<GuardianResult>,
child: ReturnType<typeof spawn>,
): Promise<GuardianResult> {
let timer: NodeJS.Timeout | undefined;
try {
return await Promise.race([
completion,
new Promise<never>((_resolve, reject) => {
timer = setTimeout(() => {
child.kill("SIGKILL");
reject(new Error("provider guardian did not close within its bound"));
}, CLOSE_TIMEOUT_MS);
}),
]);
} finally {
if (timer) clearTimeout(timer);
}
}
type FallbackIdentity = Readonly<{
rawStagingPath: string;
rawPath: string;
rawIdentity: OwnedIdentity;
sealedPath: string;
sealedTempPath: string;
sealedIdentity: OwnedIdentity;
}>;
async function cleanupFallback(input: FallbackIdentity): Promise<void> {
const failures: Error[] = [];
for (const target of [
{ path: input.rawStagingPath, identity: input.rawIdentity },
{ path: input.rawPath, identity: input.rawIdentity },
{ path: input.sealedTempPath, identity: input.sealedIdentity },
{ path: input.sealedPath, identity: input.sealedIdentity },
]) {
try {
await cleanupOwnedProviderReport({
reportPath: target.path,
reportDev: target.identity.dev,
reportIno: target.identity.ino,
});
} catch (error) {
failures.push(toError(error));
}
}
if (failures.length > 0) {
throw new AggregateError(failures, "provider guardian fallback cleanup failed", {
cause: failures[0],
});
}
}
async function cleanupFallbackCloseAndThrow(
fallback: FallbackIdentity,
recovery: RecoveryAuthority,
primaryError: unknown,
): Promise<never> {
const failures = [toError(primaryError)];
try {
await cleanupFallback(fallback);
} catch (cleanupError) {
failures.push(toError(cleanupError));
}
try {
await closeRecoveryAuthority(recovery);
} catch (closeError) {
failures.push(toError(closeError));
}
if (failures.length > 1) {
throw new AggregateError(failures, "provider guardian failure and recovery failed", {
cause: failures[0],
});
}
throw failures[0]!;
}
async function cleanupFallbackAndClose(
fallback: FallbackIdentity,
recovery: RecoveryAuthority,
): Promise<void> {
const failures: Error[] = [];
try {
await cleanupFallback(fallback);
} catch (cleanupError) {
failures.push(toError(cleanupError));
}
try {
await closeRecoveryAuthority(recovery);
} catch (closeError) {
failures.push(toError(closeError));
}
if (failures.length > 0) {
throw new AggregateError(failures, "provider guardian abort recovery failed", {
cause: failures[0],
});
}
}
async function openRecoveryAuthority(input: Readonly<{
rawDirectory: string;
evidenceRoot: string;
rawLeaf: string;
rawStagingLeaf: string;
sealedLeaf: string;
sealedTempLeaf: string;
}>): Promise<RecoveryAuthority> {
let rawDirectoryHandle: FileHandle | undefined;
let evidenceDirectoryHandle: FileHandle | undefined;
let rawStagingHandle: FileHandle | undefined;
let sealedTempHandle: FileHandle | undefined;
let rawIdentity: OwnedIdentity | undefined;
let sealedIdentity: OwnedIdentity | undefined;
let rawStagingPinnedPath: string | undefined;
let rawPinnedPath: string | undefined;
let sealedTempPinnedPath: string | undefined;
let sealedPinnedPath: string | undefined;
try {
rawDirectoryHandle = await open(
input.rawDirectory,
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
);
await assertPinnedDirectory(rawDirectoryHandle, input.rawDirectory, "raw");
evidenceDirectoryHandle = await open(
input.evidenceRoot,
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
);
await assertPinnedDirectory(evidenceDirectoryHandle, input.evidenceRoot, "evidence");
rawStagingPinnedPath =
`/proc/self/fd/${rawDirectoryHandle.fd}/${input.rawStagingLeaf}`;
rawPinnedPath = `/proc/self/fd/${rawDirectoryHandle.fd}/${input.rawLeaf}`;
sealedTempPinnedPath =
`/proc/self/fd/${evidenceDirectoryHandle.fd}/${input.sealedTempLeaf}`;
sealedPinnedPath = `/proc/self/fd/${evidenceDirectoryHandle.fd}/${input.sealedLeaf}`;
rawStagingHandle = await open(
rawStagingPinnedPath,
constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
0o600,
);
const rawMetadata = await rawStagingHandle.stat();
rawIdentity = Object.freeze({ dev: rawMetadata.dev, ino: rawMetadata.ino });
assertAllocatedPrivateMetadata(rawMetadata, rawIdentity, "raw staging");
await assertPinnedLeafIdentity(rawStagingPinnedPath, rawIdentity, 0o600);
sealedTempHandle = await open(
sealedTempPinnedPath,
constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
0o600,
);
const sealedMetadata = await sealedTempHandle.stat();
sealedIdentity = Object.freeze({ dev: sealedMetadata.dev, ino: sealedMetadata.ino });
assertAllocatedPrivateMetadata(sealedMetadata, sealedIdentity, "sealed temp");
await assertPinnedLeafIdentity(sealedTempPinnedPath, sealedIdentity, 0o600);
return Object.freeze({
rawDirectoryHandle,
evidenceDirectoryHandle,
rawStagingHandle,
sealedTempHandle,
rawIdentity,
sealedIdentity,
rawStagingPinnedPath,
rawPinnedPath,
sealedTempPinnedPath,
sealedPinnedPath,
});
} catch (error) {
const failures = [toError(error)];
for (const target of [
{ path: rawStagingPinnedPath, identity: rawIdentity },
{ path: rawPinnedPath, identity: rawIdentity },
{ path: sealedTempPinnedPath, identity: sealedIdentity },
{ path: sealedPinnedPath, identity: sealedIdentity },
]) {
if (!target.path || !target.identity) continue;
try {
await cleanupOwnedProviderReport({
reportPath: target.path,
reportDev: target.identity.dev,
reportIno: target.identity.ino,
});
} catch (cleanupError) {
failures.push(toError(cleanupError));
}
}
for (const handle of [
sealedTempHandle,
rawStagingHandle,
evidenceDirectoryHandle,
rawDirectoryHandle,
]) {
if (!handle) continue;
try { await handle.close(); } catch (closeError) { failures.push(toError(closeError)); }
}
if (failures.length > 1) {
throw new AggregateError(failures, "provider guardian recovery setup failed", {
cause: error,
});
}
throw failures[0]!;
}
}
function assertAllocatedPrivateMetadata(
metadata: Awaited<ReturnType<FileHandle["stat"]>>,
identity: OwnedIdentity,
label: string,
): void {
if (
!metadata.isFile() || Number(metadata.dev) !== identity.dev ||
Number(metadata.ino) !== identity.ino || Number(metadata.nlink) !== 1 ||
(Number(metadata.mode) & 0o777) !== 0o600 || Number(metadata.size) !== 0
) {
throw new TypeError(`provider guardian ${label} allocation is invalid`);
}
}
async function assertPinnedDirectory(
handle: FileHandle,
canonicalPath: string,
label: string,
): Promise<void> {
const [descriptorMetadata, pathMetadata] = await Promise.all([
handle.stat(),
lstat(canonicalPath),
]);
if (
!descriptorMetadata.isDirectory() || !pathMetadata.isDirectory() ||
pathMetadata.isSymbolicLink() || descriptorMetadata.dev !== pathMetadata.dev ||
descriptorMetadata.ino !== pathMetadata.ino
) {
throw new TypeError(`provider guardian ${label} recovery directory identity changed`);
}
}
async function assertRecoveryLeavesMissing(recovery: RecoveryAuthority): Promise<void> {
await assertPinnedLeafMissing(recovery.rawPinnedPath);
await assertPinnedLeafMissing(recovery.sealedPinnedPath);
assertAllocatedPrivateMetadata(
await recovery.rawStagingHandle.stat(),
recovery.rawIdentity,
"raw staging",
);
assertAllocatedPrivateMetadata(
await recovery.sealedTempHandle.stat(),
recovery.sealedIdentity,
"sealed temp",
);
await assertPinnedLeafIdentity(
recovery.rawStagingPinnedPath,
recovery.rawIdentity,
0o600,
);
await assertPinnedLeafIdentity(
recovery.sealedTempPinnedPath,
recovery.sealedIdentity,
0o600,
);
}
async function assertPinnedLeafMissing(target: string): Promise<void> {
try {
await lstat(target);
throw new Error("provider guardian transaction leaf already exists");
} catch (error) {
if (!hasErrorCode(error, "ENOENT")) throw error;
}
}
async function assertPinnedLeafIdentity(
target: string,
identity: OwnedIdentity,
mode: number,
): Promise<void> {
const metadata = await lstat(target);
if (
!metadata.isFile() || metadata.isSymbolicLink() || metadata.dev !== identity.dev ||
metadata.ino !== identity.ino || metadata.nlink !== 1 ||
(metadata.mode & 0o777) !== mode || metadata.size !== 0
) {
throw new TypeError("provider guardian READY identity changed");
}
}
async function cleanupStartupRecovery(recovery: RecoveryAuthority): Promise<void> {
await cleanupFallback({
rawStagingPath: recovery.rawStagingPinnedPath,
rawPath: recovery.rawPinnedPath,
rawIdentity: recovery.rawIdentity,
sealedTempPath: recovery.sealedTempPinnedPath,
sealedPath: recovery.sealedPinnedPath,
sealedIdentity: recovery.sealedIdentity,
});
}
async function closeRecoveryAuthority(recovery: RecoveryAuthority): Promise<void> {
const failures: Error[] = [];
for (const handle of [
recovery.rawStagingHandle,
recovery.sealedTempHandle,
recovery.rawDirectoryHandle,
recovery.evidenceDirectoryHandle,
]) {
try { await handle.close(); } catch (error) { failures.push(toError(error)); }
}
if (failures.length > 0) {
throw new AggregateError(failures, "provider guardian recovery directory close failed", {
cause: failures[0],
});
}
}
async function closeRecoveryAndThrow(
recovery: RecoveryAuthority,
primaryError: unknown,
): Promise<never> {
const failures = [toError(primaryError)];
try {
await cleanupStartupRecovery(recovery);
} catch (cleanupError) {
failures.push(toError(cleanupError));
}
try {
await closeRecoveryAuthority(recovery);
} catch (closeError) {
failures.push(toError(closeError));
}
if (failures.length > 1) {
throw new AggregateError(failures,
"provider guardian failure and recovery close failed", { cause: failures[0] });
}
throw failures[0]!;
}
function hasErrorCode(error: unknown, code: string): boolean {
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
}
function guardianCloseError(result: GuardianResult, stderr: Buffer): Error {
if (result.error) return result.error;
const detail = stderr.toString("utf8").trim();
return new Error(
`provider guardian failed: exit=${result.code ?? "none"}, signal=${result.signal ?? "none"}${detail ? `, output=${detail}` : ""}`,
);
}
function toError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error));
}
+396
View File
@@ -0,0 +1,396 @@
import { timingSafeEqual } from "node:crypto";
export type ProviderGuardianKind = "vulnerability" | "provenance";
export const MAX_PROVIDER_GUARDIAN_FRAME_PAYLOAD_BYTES = 4_092;
export const MAX_PROVIDER_GUARDIAN_LEASE_MS = 40 * 60 * 1_000;
export const MAX_PROVIDER_SEALED_BYTES = 8_388_608;
const V2_GUARD_KEYS = ["type", "version", "kind", "nonce", "deadlineEpochMs"] as const;
const READY_KEYS = [
"type", "version", "nonce", "rawDev", "rawIno",
"sealedTempLeaf", "sealedDev", "sealedIno",
] as const;
const PUBLISH_KEYS = [
"type", "version", "nonce", "sealedDev", "sealedIno", "size", "sha256",
] as const;
const PUBLISHED_KEYS = ["type", "version", "nonce", "sealedDev", "sealedIno"] as const;
const COMMIT_KEYS = ["type", "version", "nonce"] as const;
export type ProviderGuardianGuard = Readonly<{
kind: ProviderGuardianKind;
nonce: Buffer;
deadlineEpochMs: number;
}>;
export type ProviderGuardianReady = Readonly<{
nonce: Buffer;
rawDev: number;
rawIno: number;
sealedTempLeaf: string;
sealedDev: number;
sealedIno: number;
}>;
export type ProviderGuardianPublish = Readonly<{
nonce: Buffer;
sealedDev: number;
sealedIno: number;
size: number;
sha256: string;
}>;
export type ProviderGuardianPublished = Readonly<{
nonce: Buffer;
sealedDev: number;
sealedIno: number;
}>;
export function providerGuardianSealedTempLeaf(
kind: ProviderGuardianKind,
nonce: Buffer,
): string {
const rawLeaf = baseLeaf(kind);
assertV2Nonce(nonce);
return `.${rawLeaf}.guardian-${nonce.subarray(0, 16).toString("hex")}.tmp`;
}
export function providerGuardianRawStagingLeaf(
kind: ProviderGuardianKind,
nonce: Buffer,
): string {
const rawLeaf = baseLeaf(kind);
assertV2Nonce(nonce);
return `.${rawLeaf}.guardian-${nonce.subarray(0, 16).toString("hex")}.raw.tmp`;
}
export function encodeProviderGuardianGuard(input: ProviderGuardianGuard): Buffer {
assertV2Guard(input);
return prefixFrame(encodeV2GuardPayload(input));
}
export function decodeProviderGuardianGuard(
payload: Buffer,
options: Readonly<{ nowEpochMs: number; maxLeaseMs: number }>,
): ProviderGuardianGuard {
const value = parseRecord(payload, V2_GUARD_KEYS, "guard");
const guard: ProviderGuardianGuard = {
kind: value.kind as ProviderGuardianKind,
nonce: parseV2Nonce(value.nonce),
deadlineEpochMs: value.deadlineEpochMs as number,
};
if (value.type !== "guard" || value.version !== 2) {
throw new TypeError("provider guardian guard version is invalid");
}
assertV2Guard(guard);
if (
!Number.isSafeInteger(options.nowEpochMs) ||
!Number.isSafeInteger(options.maxLeaseMs) || options.maxLeaseMs <= 0 ||
guard.deadlineEpochMs <= options.nowEpochMs ||
guard.deadlineEpochMs > options.nowEpochMs + options.maxLeaseMs
) {
throw new TypeError("provider guardian guard deadline is invalid");
}
assertCanonical(payload, encodeV2GuardPayload(guard), "guard");
return guard;
}
export function encodeProviderGuardianReady(input: ProviderGuardianReady): Buffer {
assertReady(input);
return prefixFrame(encodeReadyPayload(input));
}
export function decodeProviderGuardianReady(
payload: Buffer,
expectedNonce: Buffer,
): ProviderGuardianReady {
assertV2Nonce(expectedNonce);
const value = parseRecord(payload, READY_KEYS, "READY");
const ready: ProviderGuardianReady = {
nonce: parseV2Nonce(value.nonce),
rawDev: value.rawDev as number,
rawIno: value.rawIno as number,
sealedTempLeaf: value.sealedTempLeaf as string,
sealedDev: value.sealedDev as number,
sealedIno: value.sealedIno as number,
};
if (value.type !== "ready" || value.version !== 2) {
throw new TypeError("provider guardian READY version is invalid");
}
assertReady(ready);
assertAuthenticatedNonce(ready.nonce, expectedNonce, "READY");
assertCanonical(payload, encodeReadyPayload(ready), "READY");
return ready;
}
export function encodeProviderGuardianPublish(input: ProviderGuardianPublish): Buffer {
assertPublish(input);
return prefixFrame(encodePublishPayload(input));
}
export function decodeProviderGuardianPublish(
payload: Buffer,
expectedNonce: Buffer,
): ProviderGuardianPublish {
assertV2Nonce(expectedNonce);
const value = parseRecord(payload, PUBLISH_KEYS, "publish");
const publish: ProviderGuardianPublish = {
nonce: parseV2Nonce(value.nonce),
sealedDev: value.sealedDev as number,
sealedIno: value.sealedIno as number,
size: value.size as number,
sha256: value.sha256 as string,
};
if (value.type !== "publish" || value.version !== 2) {
throw new TypeError("provider guardian publish version is invalid");
}
assertPublish(publish);
assertAuthenticatedNonce(publish.nonce, expectedNonce, "publish");
assertCanonical(payload, encodePublishPayload(publish), "publish");
return publish;
}
export function encodeProviderGuardianPublished(input: ProviderGuardianPublished): Buffer {
assertPublished(input);
return prefixFrame(encodePublishedPayload(input));
}
export function decodeProviderGuardianPublished(
payload: Buffer,
expectedNonce: Buffer,
expectedIdentity: Readonly<{ dev: number; ino: number }>,
): void {
assertV2Nonce(expectedNonce);
const value = parseRecord(payload, PUBLISHED_KEYS, "PUBLISHED");
const published: ProviderGuardianPublished = {
nonce: parseV2Nonce(value.nonce),
sealedDev: value.sealedDev as number,
sealedIno: value.sealedIno as number,
};
if (value.type !== "published" || value.version !== 2) {
throw new TypeError("provider guardian PUBLISHED version is invalid");
}
assertPublished(published);
assertAuthenticatedNonce(published.nonce, expectedNonce, "PUBLISHED");
if (published.sealedDev !== expectedIdentity.dev || published.sealedIno !== expectedIdentity.ino) {
throw new TypeError("provider guardian PUBLISHED identity is invalid");
}
assertCanonical(payload, encodePublishedPayload(published), "PUBLISHED");
}
function encodeV2GuardPayload(input: ProviderGuardianGuard): Buffer {
return Buffer.from(JSON.stringify({
type: "guard",
version: 2,
kind: input.kind,
nonce: input.nonce.toString("hex"),
deadlineEpochMs: input.deadlineEpochMs,
}));
}
export function encodeProviderGuardianCommit(nonce: Buffer): Buffer {
assertV2Nonce(nonce);
return prefixFrame(encodeCommitPayload(nonce));
}
export function decodeProviderGuardianCommit(payload: Buffer, expectedNonce: Buffer): void {
assertPayloadSize(payload);
assertV2Nonce(expectedNonce);
const decoded = decodeUtf8(payload);
let value: unknown;
try {
value = JSON.parse(decoded);
} catch {
throw new TypeError("provider guardian commit JSON is invalid");
}
if (!isRecord(value) || !hasExactKeys(value, COMMIT_KEYS)) {
throw new TypeError("provider guardian commit fields are invalid");
}
const nonce = parseV2Nonce(value.nonce);
if (
value.type !== "commit" || value.version !== 2 ||
nonce.byteLength !== expectedNonce.byteLength ||
!timingSafeEqual(nonce, expectedNonce)
) {
throw new TypeError("provider guardian commit authentication failed");
}
if (!payload.equals(encodeCommitPayload(nonce))) {
throw new TypeError("provider guardian commit is not canonical");
}
}
function encodeCommitPayload(nonce: Buffer): Buffer {
return Buffer.from(JSON.stringify({
type: "commit",
version: 2,
nonce: nonce.toString("hex"),
}));
}
function encodeReadyPayload(input: ProviderGuardianReady): Buffer {
return Buffer.from(JSON.stringify({
type: "ready",
version: 2,
nonce: input.nonce.toString("hex"),
rawDev: input.rawDev,
rawIno: input.rawIno,
sealedTempLeaf: input.sealedTempLeaf,
sealedDev: input.sealedDev,
sealedIno: input.sealedIno,
}));
}
function encodePublishPayload(input: ProviderGuardianPublish): Buffer {
return Buffer.from(JSON.stringify({
type: "publish",
version: 2,
nonce: input.nonce.toString("hex"),
sealedDev: input.sealedDev,
sealedIno: input.sealedIno,
size: input.size,
sha256: input.sha256,
}));
}
function encodePublishedPayload(input: ProviderGuardianPublished): Buffer {
return Buffer.from(JSON.stringify({
type: "published",
version: 2,
nonce: input.nonce.toString("hex"),
sealedDev: input.sealedDev,
sealedIno: input.sealedIno,
}));
}
function prefixFrame(payload: Buffer): Buffer {
if (payload.byteLength <= 0 || payload.byteLength > MAX_PROVIDER_GUARDIAN_FRAME_PAYLOAD_BYTES) {
throw new TypeError("provider guardian frame size is invalid");
}
const frame = Buffer.allocUnsafe(payload.byteLength + 4);
frame.writeUInt32BE(payload.byteLength, 0);
payload.copy(frame, 4);
return frame;
}
function assertV2Guard(input: ProviderGuardianGuard): void {
if (
(input.kind !== "vulnerability" && input.kind !== "provenance") ||
!isV2Nonce(input.nonce) ||
!Number.isSafeInteger(input.deadlineEpochMs) || input.deadlineEpochMs <= 0
) {
throw new TypeError("provider guardian guard fields are invalid");
}
}
function baseLeaf(kind: ProviderGuardianKind): string {
if (kind === "vulnerability") return "vulnerability-report.json";
if (kind === "provenance") return "provenance-attestation.json";
throw new TypeError("provider guardian kind is invalid");
}
function assertReady(input: ProviderGuardianReady): void {
if (
!isV2Nonce(input.nonce) ||
!isIdentityPart(input.rawDev) || !isIdentityPart(input.rawIno) ||
typeof input.sealedTempLeaf !== "string" ||
!/^\.(?:vulnerability-report|provenance-attestation)\.json\.guardian-[0-9a-f]{32}\.tmp$/u
.test(input.sealedTempLeaf) ||
!isIdentityPart(input.sealedDev) || !isIdentityPart(input.sealedIno)
) {
throw new TypeError("provider guardian READY fields are invalid");
}
}
function assertPublish(input: ProviderGuardianPublish): void {
if (
!isV2Nonce(input.nonce) ||
!isIdentityPart(input.sealedDev) || !isIdentityPart(input.sealedIno) ||
!Number.isSafeInteger(input.size) || input.size <= 0 || input.size > MAX_PROVIDER_SEALED_BYTES ||
typeof input.sha256 !== "string" || !/^[0-9a-f]{64}$/u.test(input.sha256)
) {
throw new TypeError("provider guardian publish fields are invalid");
}
}
function assertPublished(input: ProviderGuardianPublished): void {
if (
!isV2Nonce(input.nonce) ||
!isIdentityPart(input.sealedDev) || !isIdentityPart(input.sealedIno)
) {
throw new TypeError("provider guardian PUBLISHED fields are invalid");
}
}
function assertV2Nonce(nonce: Buffer): void {
if (!isV2Nonce(nonce)) throw new TypeError("provider guardian nonce is invalid");
}
function isV2Nonce(nonce: Buffer): boolean {
return Buffer.isBuffer(nonce) && nonce.byteLength === 32;
}
function parseV2Nonce(value: unknown): Buffer {
return typeof value === "string" && /^[0-9a-f]{64}$/u.test(value)
? Buffer.from(value, "hex")
: Buffer.alloc(0);
}
function isIdentityPart(value: unknown): value is number {
return Number.isSafeInteger(value) && (value as number) > 0;
}
function assertAuthenticatedNonce(received: Buffer, expected: Buffer, label: string): void {
if (received.byteLength !== expected.byteLength || !timingSafeEqual(received, expected)) {
throw new TypeError(`provider guardian ${label} authentication failed`);
}
}
function parseRecord(
payload: Buffer,
expectedKeys: readonly string[],
label: string,
): Record<string, unknown> {
assertPayloadSize(payload);
let value: unknown;
try {
value = JSON.parse(decodeUtf8(payload));
} catch (error) {
if (error instanceof TypeError && /provider guardian/u.test(error.message)) throw error;
throw new TypeError(`provider guardian ${label} JSON is invalid`, { cause: error });
}
if (!isRecord(value) || !hasExactKeys(value, expectedKeys)) {
throw new TypeError(`provider guardian ${label} fields are invalid`);
}
return value;
}
function assertCanonical(payload: Buffer, canonical: Buffer, label: string): void {
if (!payload.equals(canonical)) {
throw new TypeError(`provider guardian ${label} frame is not canonical`);
}
}
function assertPayloadSize(payload: Buffer): void {
if (!Buffer.isBuffer(payload) || payload.byteLength <= 0 || payload.byteLength > MAX_PROVIDER_GUARDIAN_FRAME_PAYLOAD_BYTES) {
throw new TypeError("provider guardian frame size is invalid");
}
}
function decodeUtf8(payload: Buffer): string {
let decoded: string;
try {
decoded = new TextDecoder("utf-8", { fatal: true }).decode(payload);
} catch {
throw new TypeError("provider guardian frame UTF-8 is invalid");
}
if (decoded.includes("\0")) throw new TypeError("provider guardian frame contains NUL");
return decoded;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function hasExactKeys(value: Record<string, unknown>, expected: readonly string[]): boolean {
const keys = Object.keys(value);
return keys.length === expected.length && keys.every((key, index) => key === expected[index]);
}
+27
View File
@@ -0,0 +1,27 @@
export type ProviderOutputLimiter = Readonly<{
consume(chunk: Buffer | string): void;
bytes(): number;
}>;
export function createProviderOutputLimiter(
maxBytes: number,
onExceeded: () => void,
): ProviderOutputLimiter {
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0 || typeof onExceeded !== "function") {
throw new TypeError("provider output limiter input is invalid");
}
let observedBytes = 0;
let exceeded = false;
return Object.freeze({
consume: (chunk: Buffer | string) => {
if (exceeded) return;
const bytes = Buffer.isBuffer(chunk) ? chunk.byteLength : Buffer.byteLength(chunk);
observedBytes += bytes;
if (observedBytes > maxBytes) {
exceeded = true;
onExceeded();
}
},
bytes: () => observedBytes,
});
}
+103
View File
@@ -0,0 +1,103 @@
import { spawn, type ChildProcess } from "node:child_process";
export type ProviderProcessInput = Readonly<{
executable: string;
arguments: readonly string[];
environment: NodeJS.ProcessEnv;
timeoutMs: number;
}>;
type ProviderChild = Pick<ChildProcess, "kill" | "once" | "pid">;
export async function runProviderProcess(
input: ProviderProcessInput,
dependencies: Readonly<{
spawnChild?: (input: ProviderProcessInput) => ProviderChild;
setTimer?: (callback: () => void, milliseconds: number) => ReturnType<typeof setTimeout>;
clearTimer?: (timer: ReturnType<typeof setTimeout>) => void;
killProcessGroup?: (child: ProviderChild) => void;
}> = {},
): Promise<void> {
if (!Number.isSafeInteger(input.timeoutMs) || input.timeoutMs <= 0) {
throw new TypeError("provider process timeout must be a positive integer");
}
const child = (dependencies.spawnChild ?? defaultSpawn)(input);
const setTimer = dependencies.setTimer ?? setTimeout;
const clearTimer = dependencies.clearTimer ?? clearTimeout;
const killProcessGroup = dependencies.killProcessGroup ?? defaultKillProcessGroup;
await new Promise<void>((resolve, reject) => {
let settled = false;
let timedOut = false;
const killFailures: unknown[] = [];
const settle = (error?: Error): void => {
if (settled) return;
settled = true;
clearTimer(timer);
error ? reject(error) : resolve();
};
const timer = setTimer(() => {
timedOut = true;
try {
killProcessGroup(child);
} catch (groupError) {
killFailures.push(groupError);
try {
child.kill("SIGKILL");
} catch (fallbackError) {
killFailures.push(fallbackError);
}
}
}, input.timeoutMs);
child.once("error", (error: Error) => {
if (!timedOut) settle(error);
});
child.once("close", (code: number | null, signal: NodeJS.Signals | null) => {
if (timedOut) {
const timeoutError = new Error(
"sandboxed external provider command timed out after process close",
);
settle(
killFailures.length === 0
? timeoutError
: new AggregateError(
[timeoutError, ...killFailures],
"sandboxed external provider timed out and process-group kill failed before close",
{ cause: killFailures.at(-1) },
),
);
} else if (code === 0 && signal === null) {
settle();
} else {
settle(
new Error(
`sandboxed external provider failed: exit=${code ?? "none"}, signal=${signal ?? "none"}`,
),
);
}
});
});
}
function defaultSpawn(input: ProviderProcessInput): ProviderChild {
return spawn(input.executable, [...input.arguments], {
env: input.environment,
stdio: "inherit",
detached: true,
});
}
function defaultKillProcessGroup(child: ProviderChild): void {
if (child.pid && child.pid > 0) {
try {
process.kill(-child.pid, "SIGKILL");
return;
} catch (error) {
if (!hasErrorCode(error, "ESRCH")) throw error;
}
}
child.kill("SIGKILL");
}
function hasErrorCode(error: unknown, code: string): boolean {
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
}
+40
View File
@@ -0,0 +1,40 @@
import { randomBytes } from "node:crypto";
import { lstat, rename, unlink } from "node:fs/promises";
export type ProviderRawIdentity = Readonly<{
reportPath: string;
reportDev: number;
reportIno: number;
}>;
export async function cleanupOwnedProviderReport(
identity: ProviderRawIdentity,
): Promise<boolean> {
try {
const metadata = await lstat(identity.reportPath);
if (!matchesReportIdentity(metadata, identity)) return false;
const quarantine = `${identity.reportPath}.parent-loss-${process.pid}-${randomBytes(16).toString("hex")}`;
await rename(identity.reportPath, quarantine);
const quarantinedMetadata = await lstat(quarantine);
if (!matchesReportIdentity(quarantinedMetadata, identity)) {
throw new Error("provider raw output identity changed during parent-loss cleanup");
}
await unlink(quarantine);
return true;
} catch (error) {
if (hasErrorCode(error, "ENOENT")) return false;
throw error;
}
}
function matchesReportIdentity(
metadata: Awaited<ReturnType<typeof lstat>>,
identity: ProviderRawIdentity,
): boolean {
return metadata.isFile() && !metadata.isSymbolicLink() &&
metadata.dev === identity.reportDev && metadata.ino === identity.reportIno;
}
function hasErrorCode(error: unknown, code: string): boolean {
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
}
+641
View File
@@ -0,0 +1,641 @@
import { createHash } from "node:crypto";
import {
closeSync,
fstatSync,
fsyncSync,
lstatSync,
readlinkSync,
readSync,
type Stats,
writeSync,
createReadStream,
} from "node:fs";
import { link, lstat, unlink } from "node:fs/promises";
import path from "node:path";
import {
decodeProviderGuardianCommit,
decodeProviderGuardianGuard,
decodeProviderGuardianPublish,
encodeProviderGuardianPublished,
encodeProviderGuardianReady,
MAX_PROVIDER_GUARDIAN_FRAME_PAYLOAD_BYTES,
MAX_PROVIDER_GUARDIAN_LEASE_MS,
providerGuardianRawStagingLeaf,
providerGuardianSealedTempLeaf,
type ProviderGuardianGuard,
type ProviderGuardianKind,
} from "./provider-guardian-protocol.ts";
import { cleanupOwnedProviderReport } from "./provider-raw-cleanup.ts";
type OwnedIdentity = Readonly<{ dev: number; ino: number }>;
type BootstrapAuthority = Readonly<{
kind: ProviderGuardianKind;
noncePrefix: string;
rawStagingLeaf: string;
rawStagingPath: string;
rawPath: string;
rawIdentity: OwnedIdentity;
sealedTempLeaf: string;
sealedTempPath: string;
sealedPath: string;
sealedIdentity: OwnedIdentity;
}>;
type BoundPrivateAuthority = Readonly<{
identity: OwnedIdentity;
leaf: string;
noncePrefix: string;
path: string;
stem: string;
}>;
type GuardianTransaction = Readonly<{ guard: ProviderGuardianGuard }>;
const RAW_DIRECTORY_FD = 3;
const EVIDENCE_DIRECTORY_FD = 4;
const RAW_STAGING_FD = 5;
const SEALED_TEMP_FD = 6;
const RAW_DIRECTORY_PATH = `/proc/self/fd/${RAW_DIRECTORY_FD}`;
const EVIDENCE_DIRECTORY_PATH = `/proc/self/fd/${EVIDENCE_DIRECTORY_FD}`;
const RAW_STAGING_FD_PATH = `/proc/self/fd/${RAW_STAGING_FD}`;
const SEALED_TEMP_FD_PATH = `/proc/self/fd/${SEALED_TEMP_FD}`;
const RAW_STAGING_PATTERN =
/^\.(vulnerability-report|provenance-attestation)\.json\.guardian-([0-9a-f]{32})\.raw\.tmp$/u;
const SEALED_TEMP_PATTERN =
/^\.(vulnerability-report|provenance-attestation)\.json\.guardian-([0-9a-f]{32})\.tmp$/u;
let privateFdsClosed = false;
const bootstrap = await initializeBootstrap();
let pending = Buffer.alloc(0);
let expectedBytes: number | undefined;
let state: "starting" | "guarding" | "published" | "commitPending" = "starting";
let transaction: GuardianTransaction | undefined;
let terminal = false;
let deadline: NodeJS.Timeout | undefined;
let operations = Promise.resolve();
const liveness = createReadStream("", { fd: 0, autoClose: false });
liveness.on("data", consumeChunk);
liveness.once("end", () => {
enqueue(async () => {
if (state === "commitPending" && pending.byteLength === 0 && expectedBytes === undefined) {
await succeedOnCommittedEof();
return;
}
if (state === "starting" && pending.byteLength > 0) {
await failClosed(126, "provider guardian frame is truncated");
return;
}
await failClosed(125, "provider guardian liveness EOF");
});
});
liveness.once("error", (error) => {
enqueue(async () => failClosed(125, `provider guardian liveness error: ${error.message}`));
});
async function initializeBootstrap(): Promise<BootstrapAuthority> {
const rawDirectory = path.resolve(process.cwd(), "provider-evidence/untrusted");
const evidenceDirectory = path.resolve(process.cwd(), "provider-evidence");
const failures: Error[] = [];
const rawDirectoryValid = captureInheritedDirectory(
RAW_DIRECTORY_FD,
rawDirectory,
"raw",
failures,
);
const evidenceDirectoryValid = captureInheritedDirectory(
EVIDENCE_DIRECTORY_FD,
evidenceDirectory,
"evidence",
failures,
);
const rawDescriptor = capturePrivateDescriptor(RAW_STAGING_FD, "raw staging", failures);
const sealedDescriptor = capturePrivateDescriptor(SEALED_TEMP_FD, "sealed temp", failures);
const rawAuthority = rawDescriptor && rawDirectoryValid
? capturePrivateAlias({
descriptorMetadata: rawDescriptor,
descriptorTarget: RAW_STAGING_FD_PATH,
expectedDirectory: rawDirectory,
descriptorDirectory: RAW_DIRECTORY_PATH,
grammar: RAW_STAGING_PATTERN,
label: "raw staging",
}, failures)
: undefined;
const sealedAuthority = sealedDescriptor && evidenceDirectoryValid
? capturePrivateAlias({
descriptorMetadata: sealedDescriptor,
descriptorTarget: SEALED_TEMP_FD_PATH,
expectedDirectory: evidenceDirectory,
descriptorDirectory: EVIDENCE_DIRECTORY_PATH,
grammar: SEALED_TEMP_PATTERN,
label: "sealed temp",
}, failures)
: undefined;
if (!rawAuthority || !sealedAuthority) {
return await failBootstrap(rawAuthority, sealedAuthority, failures);
}
try {
if (
rawAuthority.stem !== sealedAuthority.stem ||
rawAuthority.noncePrefix !== sealedAuthority.noncePrefix
) {
throw new TypeError("provider guardian inherited private aliases disagree");
}
const kind = providerKindFromStem(rawAuthority.stem);
const rawLeaf = kind === "vulnerability"
? "vulnerability-report.json"
: "provenance-attestation.json";
return Object.freeze({
kind,
noncePrefix: rawAuthority.noncePrefix,
rawStagingLeaf: rawAuthority.leaf,
rawStagingPath: rawAuthority.path,
rawPath: `${RAW_DIRECTORY_PATH}/${rawLeaf}`,
rawIdentity: rawAuthority.identity,
sealedTempLeaf: sealedAuthority.leaf,
sealedTempPath: sealedAuthority.path,
sealedPath: `${EVIDENCE_DIRECTORY_PATH}/${rawLeaf}`,
sealedIdentity: sealedAuthority.identity,
});
} catch (error) {
failures.push(toError(error));
return await failBootstrap(rawAuthority, sealedAuthority, failures);
}
}
function captureInheritedDirectory(
fd: number,
canonicalPath: string,
label: string,
failures: Error[],
): boolean {
try {
assertInheritedDirectory(fd, canonicalPath, label);
return true;
} catch (error) {
failures.push(toError(error));
return false;
}
}
function capturePrivateDescriptor(
fd: number,
label: string,
failures: Error[],
): Stats | undefined {
try {
return fstatSync(fd);
} catch (error) {
failures.push(new Error(`provider guardian inherited ${label} fd is invalid`, {
cause: error,
}));
return undefined;
}
}
function capturePrivateAlias(
input: Readonly<{
descriptorMetadata: Stats;
descriptorTarget: string;
expectedDirectory: string;
descriptorDirectory: string;
grammar: RegExp;
label: string;
}>,
failures: Error[],
): BoundPrivateAuthority | undefined {
try {
return bindPrivateAlias(input);
} catch (error) {
failures.push(toError(error));
return undefined;
}
}
function bindPrivateAlias(input: Readonly<{
descriptorMetadata: Stats;
descriptorTarget: string;
expectedDirectory: string;
descriptorDirectory: string;
grammar: RegExp;
label: string;
}>): BoundPrivateAuthority {
const descriptorTarget = readlinkSync(input.descriptorTarget);
if (path.dirname(descriptorTarget) !== input.expectedDirectory) {
throw new TypeError(
`provider guardian inherited ${input.label} alias is outside its directory`,
);
}
const leaf = path.basename(descriptorTarget);
const match = input.grammar.exec(leaf);
if (!match) {
throw new TypeError(`provider guardian inherited ${input.label} alias is invalid`);
}
const boundPath = `${input.descriptorDirectory}/${leaf}`;
const pathnameMetadata = lstatSync(boundPath);
assertPrivateMetadata(input.descriptorMetadata, pathnameMetadata, input.label);
return Object.freeze({
identity: Object.freeze({
dev: input.descriptorMetadata.dev,
ino: input.descriptorMetadata.ino,
}),
leaf,
noncePrefix: match[2]!,
path: boundPath,
stem: match[1]!,
});
}
async function failBootstrap(
rawAuthority: BoundPrivateAuthority | undefined,
sealedAuthority: BoundPrivateAuthority | undefined,
failures: Error[],
): Promise<never> {
for (const authority of [rawAuthority, sealedAuthority]) {
if (!authority) continue;
await cleanupOwnedPath(authority.path, authority.identity, failures);
}
closePrivateFds(failures);
writeAggregateDiagnostic("provider guardian bootstrap failed", failures);
process.exit(126);
}
function assertPrivateMetadata(
descriptorMetadata: Stats,
pathnameMetadata: Stats,
label: string,
): void {
if (
!descriptorMetadata.isFile() || !pathnameMetadata.isFile() ||
pathnameMetadata.isSymbolicLink() || descriptorMetadata.dev !== pathnameMetadata.dev ||
descriptorMetadata.ino !== pathnameMetadata.ino || descriptorMetadata.nlink !== 1 ||
pathnameMetadata.nlink !== 1 || (descriptorMetadata.mode & 0o777) !== 0o600 ||
(pathnameMetadata.mode & 0o777) !== 0o600 || descriptorMetadata.size !== 0 ||
pathnameMetadata.size !== 0
) {
throw new TypeError(`provider guardian inherited ${label} identity is invalid`);
}
}
function providerKindFromStem(stem: string): ProviderGuardianKind {
if (stem === "vulnerability-report") return "vulnerability";
if (stem === "provenance-attestation") return "provenance";
throw new TypeError("provider guardian inherited private kind is invalid");
}
function consumeChunk(chunk: Buffer | string): void {
if (terminal) return;
pending = Buffer.concat([pending, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
if (expectedBytes === undefined && pending.byteLength >= 4) {
expectedBytes = pending.readUInt32BE(0);
if (expectedBytes <= 0 || expectedBytes > MAX_PROVIDER_GUARDIAN_FRAME_PAYLOAD_BYTES) {
enqueue(async () => failClosed(126, "provider guardian frame length is invalid"));
return;
}
}
if (expectedBytes !== undefined && pending.byteLength === expectedBytes + 4) {
const payload = pending.subarray(4);
pending = Buffer.alloc(0);
expectedBytes = undefined;
enqueue(async () => handleFrame(payload));
} else if (expectedBytes !== undefined && pending.byteLength > expectedBytes + 4) {
enqueue(async () => failClosed(126, "provider guardian frame has trailing bytes"));
}
}
function enqueue(operation: () => Promise<void>): void {
operations = operations.then(operation).catch(async (error) => {
await failClosed(126, error instanceof Error ? error.message : String(error));
});
}
async function handleFrame(payload: Buffer): Promise<void> {
if (state === "starting") {
await establishTransaction(payload);
} else if (state === "guarding") {
await publishSealedArtifact(payload);
} else if (state === "published") {
await prepareCommit(payload);
} else {
await failClosed(126, "provider guardian received data after commit");
}
}
async function establishTransaction(payload: Buffer): Promise<void> {
const nowEpochMs = Date.now();
const guard = decodeProviderGuardianGuard(payload, {
nowEpochMs,
maxLeaseMs: MAX_PROVIDER_GUARDIAN_LEASE_MS,
});
if (
guard.kind !== bootstrap.kind ||
guard.nonce.subarray(0, 16).toString("hex") !== bootstrap.noncePrefix ||
providerGuardianRawStagingLeaf(guard.kind, guard.nonce) !== bootstrap.rawStagingLeaf ||
providerGuardianSealedTempLeaf(guard.kind, guard.nonce) !== bootstrap.sealedTempLeaf
) {
throw new TypeError("provider guardian guard does not match inherited private aliases");
}
assertBoundPrivateLeaf(
RAW_STAGING_FD,
bootstrap.rawStagingPath,
bootstrap.rawIdentity,
"raw staging",
);
assertBoundPrivateLeaf(
SEALED_TEMP_FD,
bootstrap.sealedTempPath,
bootstrap.sealedIdentity,
"sealed temp",
);
transaction = Object.freeze({ guard });
await link(bootstrap.rawStagingPath, bootstrap.rawPath);
assertOwnedPathMetadata(bootstrap.rawStagingPath, bootstrap.rawIdentity, 2, 0o600, 0,
"raw staging link");
assertOwnedPathMetadata(bootstrap.rawPath, bootstrap.rawIdentity, 2, 0o600, 0,
"canonical raw link");
await unlink(bootstrap.rawStagingPath);
fsyncSync(RAW_DIRECTORY_FD);
assertOwnedPathMetadata(bootstrap.rawPath, bootstrap.rawIdentity, 1, 0o600, 0,
"canonical raw");
assertBoundPrivateLeaf(
SEALED_TEMP_FD,
bootstrap.sealedTempPath,
bootstrap.sealedIdentity,
"sealed temp",
);
const remainingLeaseMs = guard.deadlineEpochMs - Date.now();
if (remainingLeaseMs <= 0) {
throw new TypeError("provider guardian guard deadline expired during startup");
}
deadline = setTimeout(() => {
enqueue(async () => failClosed(124, "provider guardian lease deadline expired", true));
}, remainingLeaseMs);
writeSync(1, encodeProviderGuardianReady({
nonce: guard.nonce,
rawDev: bootstrap.rawIdentity.dev,
rawIno: bootstrap.rawIdentity.ino,
sealedTempLeaf: bootstrap.sealedTempLeaf,
sealedDev: bootstrap.sealedIdentity.dev,
sealedIno: bootstrap.sealedIdentity.ino,
}));
state = "guarding";
}
function assertBoundPrivateLeaf(
fd: number,
target: string,
identity: OwnedIdentity,
label: string,
): void {
const descriptorMetadata = fstatSync(fd);
const pathnameMetadata = lstatSync(target);
assertOwnedMetadata(descriptorMetadata, identity, 1, 0o600, 0, label);
assertOwnedMetadata(pathnameMetadata, identity, 1, 0o600, 0, label);
if (pathnameMetadata.isSymbolicLink()) {
throw new TypeError(`provider guardian ${label} alias became symbolic`);
}
}
async function publishSealedArtifact(payload: Buffer): Promise<void> {
if (!transaction) {
throw new Error("provider guardian transaction identity is unavailable");
}
const publication = decodeProviderGuardianPublish(payload, transaction.guard.nonce);
if (
publication.sealedDev !== bootstrap.sealedIdentity.dev ||
publication.sealedIno !== bootstrap.sealedIdentity.ino
) {
throw new TypeError("provider guardian publish identity is invalid");
}
assertOwnedMetadata(
fstatSync(SEALED_TEMP_FD),
bootstrap.sealedIdentity,
1,
0o400,
publication.size,
"sealed publish descriptor",
);
const pathnameMetadata = await lstat(bootstrap.sealedTempPath);
assertOwnedMetadata(
pathnameMetadata,
bootstrap.sealedIdentity,
1,
0o400,
publication.size,
"sealed publish pathname",
);
if (pathnameMetadata.isSymbolicLink()) {
throw new TypeError("provider guardian sealed publish pathname became symbolic");
}
const actualSha256 = hashInheritedFile(SEALED_TEMP_FD, publication.size);
if (actualSha256 !== publication.sha256) {
throw new TypeError("provider guardian publish hash is invalid");
}
try {
await lstat(bootstrap.sealedPath);
throw new Error("provider guardian sealed output already exists");
} catch (error) {
if (!hasErrorCode(error, "ENOENT")) throw error;
}
await link(bootstrap.sealedTempPath, bootstrap.sealedPath);
await unlink(bootstrap.sealedTempPath);
fsyncSync(EVIDENCE_DIRECTORY_FD);
const finalMetadata = await lstat(bootstrap.sealedPath);
assertOwnedMetadata(
finalMetadata,
bootstrap.sealedIdentity,
1,
0o400,
publication.size,
"sealed final",
);
if (finalMetadata.isSymbolicLink()) {
throw new TypeError("provider guardian sealed final became symbolic");
}
state = "published";
writeSync(1, encodeProviderGuardianPublished({
nonce: transaction.guard.nonce,
sealedDev: bootstrap.sealedIdentity.dev,
sealedIno: bootstrap.sealedIdentity.ino,
}));
}
function assertOwnedPathMetadata(
target: string,
identity: OwnedIdentity,
expectedLinks: number,
expectedMode: number,
expectedSize: number,
label: string,
): void {
const metadata = lstatSync(target);
assertOwnedMetadata(metadata, identity, expectedLinks, expectedMode, expectedSize, label);
if (metadata.isSymbolicLink()) {
throw new TypeError(`provider guardian ${label} became symbolic`);
}
}
function assertOwnedMetadata(
metadata: Stats,
identity: OwnedIdentity,
expectedLinks: number,
expectedMode: number,
expectedSize: number,
label: string,
): void {
if (
!metadata.isFile() || metadata.dev !== identity.dev || metadata.ino !== identity.ino ||
metadata.nlink !== expectedLinks || (metadata.mode & 0o777) !== expectedMode ||
metadata.size !== expectedSize
) {
throw new TypeError(`provider guardian ${label} metadata is invalid`);
}
}
function hashInheritedFile(fd: number, size: number): string {
const digest = createHash("sha256");
const buffer = Buffer.allocUnsafe(Math.min(65_536, size));
let position = 0;
while (position < size) {
const requested = Math.min(buffer.byteLength, size - position);
const bytesRead = readSync(fd, buffer, 0, requested, position);
if (bytesRead <= 0) throw new Error("provider guardian sealed publish read was truncated");
digest.update(buffer.subarray(0, bytesRead));
position += bytesRead;
}
return digest.digest("hex");
}
async function prepareCommit(payload: Buffer): Promise<void> {
if (!transaction) throw new Error("provider guardian transaction identity is unavailable");
decodeProviderGuardianCommit(payload, transaction.guard.nonce);
const removedRaw = await cleanupOwnedProviderReport({
reportPath: bootstrap.rawPath,
reportDev: bootstrap.rawIdentity.dev,
reportIno: bootstrap.rawIdentity.ino,
});
if (!removedRaw) throw new Error("provider guardian raw output disappeared before commit");
state = "commitPending";
}
async function succeedOnCommittedEof(): Promise<void> {
const closeErrors: Error[] = [];
closePrivateFds(closeErrors);
if (closeErrors.length > 0) {
await failClosed(
126,
"provider guardian private descriptor close failed",
false,
closeErrors,
);
return;
}
terminal = true;
if (deadline) clearTimeout(deadline);
liveness.removeAllListeners();
liveness.destroy();
closeControlInputBestEffort();
process.exit(0);
}
async function failClosed(
exitCode: number,
message: string,
forceSignal = false,
priorErrors: readonly Error[] = [],
): Promise<void> {
if (terminal) return;
terminal = true;
if (deadline) clearTimeout(deadline);
liveness.removeAllListeners();
liveness.destroy();
const failures = [new Error(message), ...priorErrors];
await cleanupOwnedPath(bootstrap.rawStagingPath, bootstrap.rawIdentity, failures);
await cleanupOwnedPath(bootstrap.rawPath, bootstrap.rawIdentity, failures);
await cleanupOwnedPath(bootstrap.sealedTempPath, bootstrap.sealedIdentity, failures);
await cleanupOwnedPath(bootstrap.sealedPath, bootstrap.sealedIdentity, failures);
closePrivateFds(failures);
closeControlInputBestEffort();
writeAggregateDiagnostic("provider guardian failed", failures);
if (forceSignal) {
try {
process.kill(process.pid, "SIGKILL");
} finally {
process.exit(exitCode);
}
}
process.exit(exitCode);
}
async function cleanupOwnedPath(
target: string,
identity: OwnedIdentity,
errors: Error[],
): Promise<void> {
try {
await cleanupOwnedProviderReport({
reportPath: target,
reportDev: identity.dev,
reportIno: identity.ino,
});
} catch (error) {
errors.push(toError(error));
}
}
function closePrivateFds(errors: Error[]): void {
if (privateFdsClosed) return;
privateFdsClosed = true;
for (const fd of [RAW_STAGING_FD, SEALED_TEMP_FD]) {
try {
closeSync(fd);
} catch (error) {
errors.push(toError(error));
}
}
}
function closeControlInputBestEffort(): void {
try {
closeSync(0);
} catch {
// Terminal cleanup and the exit status must not depend on a diagnostic fd.
}
}
function writeAggregateDiagnostic(label: string, failures: readonly Error[]): void {
const aggregate = failures.length > 1
? new AggregateError(failures, label, { cause: failures[0] })
: failures[0];
const detail = aggregate instanceof AggregateError
? aggregate.errors.map((error) => toError(error).message).join("; ")
: aggregate?.message ?? label;
try {
writeSync(2, `${label}: ${detail}\n`);
} catch {
// A closed parent-side pipe must not convert fail-closed termination to exit 0.
}
}
function assertInheritedDirectory(fd: number, canonicalPath: string, label: string): void {
const descriptorMetadata = fstatSync(fd);
const pathMetadata = lstatSync(canonicalPath);
if (
!descriptorMetadata.isDirectory() || !pathMetadata.isDirectory() ||
pathMetadata.isSymbolicLink() || descriptorMetadata.dev !== pathMetadata.dev ||
descriptorMetadata.ino !== pathMetadata.ino
) {
throw new TypeError(`provider guardian inherited ${label} fd is not a directory`);
}
}
function toError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error));
}
function hasErrorCode(error: unknown, code: string): boolean {
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
}
+166
View File
@@ -0,0 +1,166 @@
import { spawn } from "node:child_process";
import { closeSync, createReadStream, writeSync } from "node:fs";
import { cleanupOwnedProviderReport } from "./provider-raw-cleanup.ts";
const MAX_FRAME_BYTES = 16_777_216;
const reportIdentity = parseReportIdentity(process.argv.slice(2));
let pending = Buffer.alloc(0);
let expectedBytes: number | undefined;
let provider: ReturnType<typeof spawn> | undefined;
let providerClosed = false;
let livenessLost = false;
const liveness = createReadStream("", { fd: 0, autoClose: false });
liveness.on("data", (chunk: Buffer | string) => {
if (provider) {
terminateForProtocolFailure("provider scope received trailing protocol bytes");
return;
}
pending = Buffer.concat([pending, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
if (expectedBytes === undefined && pending.byteLength >= 4) {
expectedBytes = pending.readUInt32BE(0);
if (expectedBytes <= 0 || expectedBytes > MAX_FRAME_BYTES) {
terminateForProtocolFailure("provider scope frame length is invalid");
return;
}
}
if (expectedBytes !== undefined && pending.byteLength === expectedBytes + 4) {
launchProvider(pending.subarray(4));
pending = Buffer.alloc(0);
} else if (expectedBytes !== undefined && pending.byteLength > expectedBytes + 4) {
terminateForProtocolFailure("provider scope frame has trailing bytes");
}
});
liveness.once("end", () => terminateForParentLoss());
liveness.once("error", () => terminateForParentLoss());
function launchProvider(payload: Buffer): void {
const frame = parseFrame(payload);
if (
frame.reportPath !== reportIdentity.reportPath ||
frame.reportDev !== reportIdentity.reportDev ||
frame.reportIno !== reportIdentity.reportIno
) {
throw new TypeError("provider scope frame identity does not match its launch identity");
}
const bwrapInput = Buffer.from(frame.bwrapInputBase64, "base64");
provider = spawn("/usr/bin/bwrap", ["--args", "0"], {
detached: true,
stdio: ["pipe", "inherit", "inherit"],
});
provider.stdin?.end(bwrapInput);
provider.once("error", (error) => finishProvider(frame, null, null, error));
provider.once("close", (code, signal) => finishProvider(frame, code, signal));
}
async function finishProvider(
frame: ReturnType<typeof parseFrame>,
code: number | null,
signal: NodeJS.Signals | null,
error?: Error,
): Promise<void> {
if (providerClosed) return;
providerClosed = true;
if (livenessLost) await cleanupOwnedProviderReport(frame);
closeLivenessInput();
if (error) {
writeSync(2, `${error.message}\n`);
process.exit(1);
}
if (signal) process.exit(128 + signalNumber(signal));
process.exit(code ?? 1);
}
function terminateForParentLoss(): void {
if (livenessLost) return;
livenessLost = true;
if (!provider || providerClosed) {
void cleanupAfterParentLossAndExit();
return;
}
try {
process.kill(-provider.pid!, "SIGKILL");
} catch (error) {
if (!hasErrorCode(error, "ESRCH")) throw error;
}
}
async function cleanupAfterParentLossAndExit(): Promise<void> {
try {
await cleanupOwnedProviderReport(reportIdentity);
} catch (error) {
writeSync(2, `${error instanceof Error ? error.message : String(error)}\n`);
}
closeLivenessInput();
process.exit(125);
}
function terminateForProtocolFailure(message: string): void {
writeSync(2, `${message}\n`);
terminateForParentLoss();
}
function closeLivenessInput(): void {
liveness.removeAllListeners();
liveness.destroy();
try {
closeSync(0);
} catch (error) {
if (!hasErrorCode(error, "EBADF")) throw error;
}
}
function parseFrame(payload: Buffer): Readonly<{
bwrapInputBase64: string;
reportPath: string;
reportDev: number;
reportIno: number;
}> {
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(payload)) as Record<string, unknown>;
if (
typeof value.bwrapInputBase64 !== "string" ||
typeof value.reportPath !== "string" || !value.reportPath.startsWith("/") ||
!Number.isSafeInteger(value.reportDev) || Number(value.reportDev) <= 0 ||
!Number.isSafeInteger(value.reportIno) || Number(value.reportIno) <= 0
) {
throw new TypeError("provider scope frame payload is invalid");
}
return {
bwrapInputBase64: value.bwrapInputBase64,
reportPath: value.reportPath,
reportDev: Number(value.reportDev),
reportIno: Number(value.reportIno),
};
}
function parseReportIdentity(arguments_: readonly string[]): Readonly<{
cpuSeconds: number;
reportPath: string;
reportDev: number;
reportIno: number;
}> {
const [cpuValue, reportPath, devValue, inoValue, ...trailing] = arguments_;
const cpuSeconds = Number(cpuValue);
const reportDev = Number(devValue);
const reportIno = Number(inoValue);
if (
trailing.length > 0 ||
!Number.isSafeInteger(cpuSeconds) || cpuSeconds <= 0 ||
typeof reportPath !== "string" || !reportPath.startsWith("/") || reportPath.includes("\0") ||
!Number.isSafeInteger(reportDev) || reportDev <= 0 ||
!Number.isSafeInteger(reportIno) || reportIno <= 0
) {
throw new TypeError("provider scope launch identity is invalid");
}
return { cpuSeconds, reportPath, reportDev, reportIno };
}
function signalNumber(signal: NodeJS.Signals): number {
return signal === "SIGKILL" ? 9 : signal === "SIGXCPU" ? 24 : 1;
}
function hasErrorCode(error: unknown, code: string): boolean {
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
}
+24 -4
View File
@@ -43,7 +43,7 @@ export async function superviseProviderEvidence(input: Readonly<{
throw new TypeError("provider invocation nonce must contain exactly 32 bytes");
}
const invocationNonce = nonceBytes.toString("hex");
const now = (dependencies.nowEpochMs ?? Date.now)();
const nowEpochMs = dependencies.nowEpochMs ?? Date.now;
const result = await (dependencies.withVerifiedCandidate ?? withVerifiedCapturedCandidate)({
captured,
verify: async ({ extractionRoot, manifest }) => {
@@ -71,13 +71,22 @@ export async function superviseProviderEvidence(input: Readonly<{
distSha256: manifest.distSha256,
lockfileSha256: manifest.lockfileSha256,
}),
secretScanAttestation: Object.freeze({
status: "PASS" as const,
localEvidenceAssessmentSha256: local.identity.assessmentSha256,
sourceSetSha256: local.identity.sourceSetSha256,
policySha256: local.identity.secretScan.policySha256,
sarifSha256: local.identity.secretScan.sarifSha256,
scanInputSha256: local.identity.secretScan.scanInputSha256,
}),
vulnerabilityInvocationNonce:
input.kind === "vulnerability" ? invocationNonce : "0".repeat(64),
provenanceInvocationNonce:
input.kind === "provenance" ? invocationNonce : "0".repeat(64),
});
const issuedAt = new Date(now).toISOString();
const expiresAt = new Date(now + 60 * 60 * 1_000).toISOString();
const issuedNow = nowEpochMs();
const issuedAt = new Date(issuedNow).toISOString();
const expiresAt = new Date(issuedNow + 60 * 60 * 1_000).toISOString();
await input.executeProvider({
candidateRoot: extractionRoot,
environment: providerInvocationEnvironment({
@@ -98,7 +107,7 @@ export async function superviseProviderEvidence(input: Readonly<{
capturedReport,
expectedContext,
trust: input.trust,
nowEpochMs: () => now,
nowEpochMs,
});
return Object.freeze({ evidence, invocationNonce, expectedContext });
},
@@ -135,6 +144,17 @@ export function providerInvocationEnvironment(input: Readonly<{
CANDIDATE_BUNDLE_SHA256: input.expectedContext.candidate.bundleSha256,
CANDIDATE_DIST_SHA256: input.expectedContext.candidate.distSha256,
CANDIDATE_LOCKFILE_SHA256: input.expectedContext.candidate.lockfileSha256,
SECRET_SCAN_STATUS: input.expectedContext.secretScanAttestation.status,
SECRET_SCAN_LOCAL_EVIDENCE_ASSESSMENT_SHA256:
input.expectedContext.secretScanAttestation.localEvidenceAssessmentSha256,
SECRET_SCAN_SOURCE_SET_SHA256:
input.expectedContext.secretScanAttestation.sourceSetSha256,
SECRET_SCAN_POLICY_SHA256:
input.expectedContext.secretScanAttestation.policySha256,
SECRET_SCAN_SARIF_SHA256:
input.expectedContext.secretScanAttestation.sarifSha256,
SECRET_SCAN_INPUT_SHA256:
input.expectedContext.secretScanAttestation.scanInputSha256,
});
}
+42
View File
@@ -0,0 +1,42 @@
import { createPublicKey } from "node:crypto";
import path from "node:path";
import { readBoundedRegularFile } from "./ci-artifact-validator.ts";
import {
providerPublicKeyFingerprint,
type ProviderTrust,
} from "./provider-evidence.ts";
export async function readProviderTrust(
configuredRoot: string,
publicKeyPath: string | undefined,
keyId: string | undefined,
): Promise<ProviderTrust | null> {
if (!publicKeyPath || !keyId?.trim()) return null;
try {
const root = path.resolve(configuredRoot);
const absolute = path.resolve(root, publicKeyPath);
const relative = path.relative(root, absolute);
const outside =
relative === ".." ||
relative.startsWith(`..${path.sep}`) ||
path.isAbsolute(relative);
const bytes = await readBoundedRegularFile({
root: outside ? path.dirname(absolute) : root,
relativePath: outside
? path.basename(absolute)
: relative.replaceAll(path.sep, "/"),
maxBytes: 1_048_576,
});
const publicKey = createPublicKey(
new TextDecoder("utf-8", { fatal: true }).decode(bytes),
);
return Object.freeze({
keyId,
publicKey,
publicKeyFingerprint: providerPublicKeyFingerprint(publicKey),
});
} catch {
return null;
}
}
+37
View File
@@ -35,6 +35,42 @@ export const RELEASE_CANDIDATE_MANIFEST_PATH =
export const LOCAL_EVIDENCE_ASSESSMENT_PATH =
"artifacts/security/local-evidence-assessment.json";
export const LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS = Object.freeze([
"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/secret-scan.ts",
"scripts/lib/supply-chain.ts",
"scripts/lib/validated-json-artifact.ts",
"src/contracts/release-artifacts.ts",
"src/features/installed-contract-contributions.ts",
"src/features/installed-feature-contracts.ts",
] as const);
export const LOCAL_EVIDENCE_POLICY_INPUT_PATHS = Object.freeze([
"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",
...LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS,
] as const);
export const RELEASE_CANDIDATE_EVIDENCE_PATHS = Object.freeze([
"pnpm-lock.yaml",
"artifacts/performance/bundle.json",
@@ -52,6 +88,7 @@ export const RELEASE_CANDIDATE_EVIDENCE_PATHS = Object.freeze([
"artifacts/security/supply-chain-coherence.json",
"artifacts/security/supply-chain-verification.json",
"artifacts/security/vulnerability-report.json",
...LOCAL_EVIDENCE_POLICY_INPUT_PATHS,
]);
export type DistOutput = Readonly<{
+1 -1
View File
@@ -231,7 +231,7 @@ export async function pruneRemovalFixtureCiContract(options: Readonly<{
contract.artifactSchemas = contract.artifactSchemas.filter(({ id }) =>
referencedSchemaIds.has(id)
);
const validated = parseCiGateContract(contract);
const validated = parseCiGateContract(contract, { mode: "removal-fixture" });
await Promise.all([
writeFile(packagePath, `${JSON.stringify(packageDocument, null, 2)}\n`),
writeFile(gatesPath, `${JSON.stringify(validated, null, 2)}\n`),
+12 -1
View File
@@ -1,3 +1,4 @@
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import path from "node:path";
@@ -175,6 +176,7 @@ export async function evaluateSecretScan(input: Readonly<{
const excluded = new Set(
input.policy.excludedPaths.map((entry) => entry.replaceAll("\\", "/")),
);
const scanInputs: Readonly<{ path: string; bytes: number; sha256: string }>[] = [];
for (const scanFile of [...new Set(scanFiles)].sort()) {
const normalized = scanFile.replaceAll("\\", "/");
if (
@@ -186,8 +188,15 @@ export async function evaluateSecretScan(input: Readonly<{
) {
continue;
}
const content = await input.readText(scanFile);
const bytes = Buffer.from(content, "utf8");
scanInputs.push(Object.freeze({
path: normalized,
bytes: bytes.byteLength,
sha256: createHash("sha256").update(bytes).digest("hex"),
}));
findings.push(
...findSecretMatches(normalized, await input.readText(scanFile), {
...findSecretMatches(normalized, content, {
allowlist: input.policy.allowlist,
now,
}),
@@ -235,6 +244,8 @@ export async function evaluateSecretScan(input: Readonly<{
findings: Object.freeze(findings),
policyFailures: Object.freeze(policyFailures),
scanFiles: Object.freeze([...scanFiles]),
scanInputs: Object.freeze(scanInputs),
scanInputSha256: supplyChainDigest(scanInputs),
sarif,
});
}
@@ -0,0 +1,94 @@
import { appendFile } from "node:fs/promises";
import {
cleanupFinalizedPromotion,
finalizeVerifiedPromotion,
} from "./promotion-stager.ts";
export async function runStageVerifiedPromotionCli(
environment: NodeJS.ProcessEnv,
dependencies: Readonly<{
cwd?: () => string;
finalize?: typeof finalizeVerifiedPromotion;
cleanup?: typeof cleanupFinalizedPromotion;
appendOutput?: (path: string, content: string) => Promise<void>;
writeStdout?: (content: string) => void;
}> = {},
): Promise<void> {
const required = (name: string): string => {
const value = environment[name];
if (!value) throw new TypeError(`promotion staging environment is missing ${name}`);
return value;
};
const attempt = Number(
environment.GITEA_RUN_ATTEMPT ??
environment.GITHUB_RUN_ATTEMPT ??
required("CI_RUN_ATTEMPT"),
);
if (!Number.isInteger(attempt) || attempt < 1 || attempt > 1_000) {
throw new TypeError("promotion staging run attempt is invalid");
}
const runnerTempRoot = required("RUNNER_TEMP");
const staged = await (dependencies.finalize ?? finalizeVerifiedPromotion)({
repositoryRoot: (dependencies.cwd ?? process.cwd)(),
archivePath: required("CANDIDATE_ARCHIVE_PATH"),
expectedArchiveSha256: required("CANDIDATE_ARCHIVE_SHA256"),
vulnerabilityReportPath: required("VULNERABILITY_REPORT_PATH"),
provenanceAttestationPath: required("PROVENANCE_ATTESTATION_PATH"),
vulnerabilityPublicKeyPath: required("VULNERABILITY_PUBLIC_KEY_PATH"),
vulnerabilityKeyId: required("VULNERABILITY_KEY_ID"),
provenancePublicKeyPath: required("PROVENANCE_PUBLIC_KEY_PATH"),
provenanceKeyId: required("PROVENANCE_KEY_ID"),
expectedRun: {
id:
environment.GITEA_RUN_ID ??
environment.GITHUB_RUN_ID ??
required("CI_RUN_ID"),
attempt,
sourceRevision:
environment.EXPECTED_SOURCE_REVISION ?? required("VITE_COMMIT_SHA"),
},
vulnerabilityInvocationNonce: required("VULNERABILITY_INVOCATION_NONCE"),
provenanceInvocationNonce: required("PROVENANCE_INVOCATION_NONCE"),
runnerTempRoot,
});
try {
const output = required("GITHUB_OUTPUT");
const content = [
`staging_root=${staged.stagingRoot}`,
`cleanup_token=${staged.cleanupToken}`,
`runner_temp_dev=${staged.runnerTempIdentity.dev}`,
`runner_temp_ino=${staged.runnerTempIdentity.ino}`,
`staging_dev=${staged.stagingIdentity.dev}`,
`staging_ino=${staged.stagingIdentity.ino}`,
"",
].join("\n");
await (dependencies.appendOutput ?? defaultAppendOutput)(output, content);
} catch (error) {
try {
await (dependencies.cleanup ?? cleanupFinalizedPromotion)({
runnerTempRoot,
stagingRoot: staged.stagingRoot,
cleanupToken: staged.cleanupToken,
runnerTempIdentity: staged.runnerTempIdentity,
stagingIdentity: staged.stagingIdentity,
});
} catch (cleanupError) {
throw new AggregateError(
[error, cleanupError],
"promotion output publication and direct staging cleanup both failed",
{ cause: cleanupError },
);
}
throw error;
}
(dependencies.writeStdout ?? process.stdout.write.bind(process.stdout))(
`Promotion staging: ${staged.files
.map(({ name, sha256 }) => `${name}=${sha256}`)
.join(", ")} PASS\n`,
);
}
async function defaultAppendOutput(path: string, content: string): Promise<void> {
await appendFile(path, content, { encoding: "utf8" });
}
+13 -6
View File
@@ -15,6 +15,17 @@ export type ValidatedJsonArtifactInput = Readonly<{
value: unknown;
}>;
export function serializeValidatedJsonArtifact(
input: ValidatedJsonArtifactInput,
): Buffer {
const parsed = input.schema.parse(input.value);
const serialized = JSON.stringify(parsed, null, 2);
if (serialized === undefined) {
throw new TypeError("Validated JSON artifact is not serializable");
}
return Buffer.from(`${serialized}\n`, "utf8");
}
export type ValidatedJsonArtifactFileSystem = Readonly<{
open: (path: string, flags: number, mode: number) => Promise<{
writeFile(data: string, encoding: "utf8"): Promise<unknown>;
@@ -64,11 +75,7 @@ export function createValidatedJsonArtifactWriter(
return async function writeArtifact(
input: ValidatedJsonArtifactInput,
): Promise<void> {
const parsed = input.schema.parse(input.value);
const serialized = JSON.stringify(parsed, null, 2);
if (serialized === undefined) {
throw new TypeError("Validated JSON artifact is not serializable");
}
const serialized = serializeValidatedJsonArtifact(input);
const temporaryPath = path.join(
path.dirname(input.path),
@@ -88,7 +95,7 @@ export function createValidatedJsonArtifactWriter(
let writeFailed = false;
let writeFailure: unknown;
try {
await handle.writeFile(`${serialized}\n`, "utf8");
await handle.writeFile(serialized.toString("utf8"), "utf8");
await handle.sync();
} catch (error) {
writeFailed = true;
+439 -115
View File
@@ -1,7 +1,7 @@
import { spawn } from "node:child_process";
import { randomBytes } from "node:crypto";
import { constants } from "node:fs";
import { access, appendFile, lstat, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { access, appendFile, lstat } from "node:fs/promises";
import path from "node:path";
import {
@@ -9,15 +9,40 @@ import {
vulnerabilityProviderReportSchema,
} from "./lib/provider-evidence.ts";
import { readBoundedRegularFile } from "./lib/ci-artifact-validator.ts";
import { readProviderTrust } from "./lib/promotion-verifier.ts";
import { readProviderTrust } from "./lib/provider-trust.ts";
import { superviseProviderEvidence } from "./lib/provider-supervisor.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
import { serializeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
import {
assertSafePublishLeaf,
ensureSafePublishDirectory,
} from "./lib/ci-gate-log.ts";
import {
encodeProviderBwrapInput,
encodeProviderScopeFrame,
formatProviderCgroupUnitName,
systemctlKillProviderArguments,
systemdRunProviderArguments,
} from "./lib/provider-cgroup.ts";
import {
assertProviderGuardianLeasePaths,
createProviderScopeGuardianLatch,
startProviderGuardian,
type ProviderGuardianLease,
} from "./lib/provider-guardian-client.ts";
import { createProviderOutputLimiter } from "./lib/provider-output-limiter.ts";
const kind = process.argv[process.argv.indexOf("--kind") + 1];
const PROVIDER_TMP_BYTES = 16_777_216;
const PROVIDER_MASK_BYTES = 1_048_576;
const PROVIDER_MAX_OUTPUT_BYTES = 1_048_576;
const DEFAULT_PROVIDER_CPU_SECONDS = 1_200;
const DEFAULT_PROVIDER_TIMEOUT_MS = 30 * 60 * 1_000;
const PROVIDER_POSTPROCESS_TIMEOUT_MS = 10 * 60 * 1_000;
const PROVIDER_REAP_TIMEOUT_MS = 5_000;
const PROVIDER_CONTROL_TIMEOUT_MS = 5_000;
const PROVIDER_CONTROL_MAX_OUTPUT_BYTES = 65_536;
const PROVIDER_CONTROL_POLL_MS = 25;
const MINIMUM_PROVIDER_SYSTEMD_VERSION = 254;
if (kind !== "vulnerability" && kind !== "provenance") {
process.stderr.write("Usage: run-and-validate-provider --kind vulnerability|provenance\n");
process.exit(2);
@@ -67,7 +92,12 @@ const workspaceRoot = process.cwd();
const reportAbsolute = path.resolve(reportPath);
const rawDirectory = path.dirname(reportAbsolute);
const sealedAbsolute = path.resolve(sealedPath);
const expectedRawLeaf = kind === "vulnerability"
? "vulnerability-report.json"
: "provenance-attestation.json";
const expectedRawDirectory = path.resolve(workspaceRoot, "provider-evidence/untrusted");
if (
reportAbsolute !== path.join(expectedRawDirectory, expectedRawLeaf) ||
path.basename(rawDirectory) !== "untrusted" ||
path.dirname(rawDirectory) !== path.dirname(sealedAbsolute) ||
reportAbsolute === sealedAbsolute
@@ -79,46 +109,98 @@ await prepareMissingProviderOutput(workspaceRoot, sealedAbsolute, sealedPath, "s
await access("/usr/bin/bwrap", constants.X_OK).catch(() => {
throw new Error("provider sandbox unavailable: /usr/bin/bwrap is required");
});
await access("/usr/bin/prlimit", constants.X_OK).catch(() => {
throw new Error("provider sandbox unavailable: /usr/bin/prlimit is required");
});
await access("/usr/bin/systemd-run", constants.X_OK).catch(() => {
throw new Error("provider cgroup unavailable: /usr/bin/systemd-run is required");
});
await access("/usr/bin/systemctl", constants.X_OK).catch(() => {
throw new Error("provider cgroup unavailable: /usr/bin/systemctl is required");
});
await assertProviderCgroupManagerAvailable();
const trust = await readProviderTrust(workspaceRoot, publicKeyPath, keyId);
if (!trust) throw new TypeError("provider supervisor trust key is invalid");
const supervised = await superviseProviderEvidence({
kind,
archivePath,
expectedArchiveSha256: archiveSha256,
expectedRun: { id: runId, attempt: runAttempt, sourceRevision },
trust,
executeProvider: async ({ candidateRoot, environment }) => {
const childEnvironment = createProviderEnvironment(kind, reportPath, environment);
await runProviderInSandbox(
command,
childEnvironment,
rawDirectory,
workspaceRoot,
candidateRoot,
let guardianLease: ProviderGuardianLease | undefined;
let guardianTerminalStarted = false;
try {
const providerWallTimeoutMs = providerTimeoutMs();
const supervised = await superviseProviderEvidence({
kind,
archivePath,
expectedArchiveSha256: archiveSha256,
expectedRun: { id: runId, attempt: runAttempt, sourceRevision },
trust,
executeProvider: async ({ candidateRoot, environment }) => {
guardianLease = await startProviderGuardian({
kind,
workspaceRoot,
leaseMs: providerWallTimeoutMs + PROVIDER_POSTPROCESS_TIMEOUT_MS,
guardianScript: path.join(workspaceRoot, "scripts/lib/provider-raw-guardian.ts"),
});
const activeGuardian = guardianLease;
assertProviderGuardianLeasePaths(activeGuardian, {
rawPath: reportAbsolute,
sealedPath: sealedAbsolute,
});
const childEnvironment = createProviderEnvironment(kind, reportPath, environment);
await runProviderInSandbox(
kind,
command,
childEnvironment,
activeGuardian.rawPath,
activeGuardian.rawIdentity,
workspaceRoot,
candidateRoot,
providerWallTimeoutMs,
activeGuardian.prematureExit,
);
await assertOwnedProviderOutput(activeGuardian.rawPath, activeGuardian.rawIdentity);
},
captureReport: async () => {
if (!guardianLease) throw new Error("provider raw guardian lease was not established");
await assertOwnedProviderOutput(guardianLease.rawPath, guardianLease.rawIdentity);
return readBoundedRegularFile({
root: workspaceRoot,
relativePath: path.relative(workspaceRoot, guardianLease.rawPath).replaceAll(path.sep, "/"),
maxBytes: 8_388_608,
});
},
});
if (!guardianLease) throw new Error("provider raw guardian lease was not established");
await assertSafePublishLeaf(sealedAbsolute, sealedPath);
await guardianLease.publish(serializeValidatedJsonArtifact({
path: sealedPath,
schema:
kind === "vulnerability"
? vulnerabilityProviderReportSchema
: provenanceProviderAttestationSchema,
value: supervised.evidence,
}));
if (process.env.GITHUB_OUTPUT) {
await appendFile(
process.env.GITHUB_OUTPUT,
`invocation_nonce=${supervised.invocationNonce}\n`,
"utf8",
);
},
captureReport: () =>
readBoundedRegularFile({
root: workspaceRoot,
relativePath: path.relative(workspaceRoot, reportAbsolute).replaceAll(path.sep, "/"),
maxBytes: 8_388_608,
}),
});
await assertSafePublishLeaf(sealedAbsolute, sealedPath);
await writeValidatedJsonArtifact({
path: sealedPath,
schema:
kind === "vulnerability"
? vulnerabilityProviderReportSchema
: provenanceProviderAttestationSchema,
value: supervised.evidence,
});
if (process.env.GITHUB_OUTPUT) {
await appendFile(
process.env.GITHUB_OUTPUT,
`invocation_nonce=${supervised.invocationNonce}\n`,
"utf8",
);
}
guardianTerminalStarted = true;
await guardianLease.commit();
} catch (providerError) {
const failures = [toError(providerError)];
if (guardianLease && !guardianTerminalStarted) {
try {
await guardianLease.abort();
} catch (guardianError) {
failures.push(toError(guardianError));
}
}
if (failures.length > 1) {
throw new AggregateError(failures, "provider lifecycle and owned output cleanup failed", {
cause: providerError,
});
}
throw providerError;
}
process.stdout.write(`${kind} provider supervised validation: PASS\n`);
@@ -139,7 +221,7 @@ function createProviderEnvironment(
? { VULNERABILITY_REPORT_PATH: rawReportPath }
: { PROVENANCE_ATTESTATION_PATH: rawReportPath }),
};
for (const name of ["LANG", "LC_ALL", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY"] as const) {
for (const name of ["LANG", "LC_ALL"] as const) {
if (process.env[name]) environment[name] = process.env[name];
}
const credentialPrefix = `${providerKind.toUpperCase()}_PROVIDER_`;
@@ -152,85 +234,312 @@ function createProviderEnvironment(
}
async function runProviderInSandbox(
providerKind: "vulnerability" | "provenance",
command: string,
environment: NodeJS.ProcessEnv,
rawDirectory: string,
reportAbsolute: string,
reportIdentity: Readonly<{ dev: number; ino: number }>,
workspaceRoot: string,
candidateRoot: string,
timeoutMs: number,
guardianExit: Promise<Error>,
): Promise<void> {
const scratch = await mkdtemp(path.join(tmpdir(), "ci-provider-sandbox-"));
try {
await mkdir(path.join(scratch, "provider-home"));
await writeFile(path.join(scratch, "node"), "", { mode: 0o500 });
const arguments_ = [
"--die-with-parent",
"--new-session",
"--as-pid-1",
"--unshare-pid",
"--unshare-ipc",
"--unshare-uts",
"--dev", "/dev",
"--proc", "/proc",
"--bind", scratch, "/tmp",
"--dir", "/etc",
];
for (const source of ["/usr", "/bin", "/lib", "/lib64"]) {
if (await exists(source)) arguments_.push("--ro-bind", source, source);
}
// setup-node commonly installs outside /usr. Expose only the exact trusted
// runtime binary, never its credential-bearing user/toolcache directory.
arguments_.push("--ro-bind", process.execPath, "/tmp/node");
for (const source of [
"/etc/ca-certificates",
"/etc/ssl",
"/etc/resolv.conf",
"/etc/hosts",
"/etc/nsswitch.conf",
"/etc/passwd",
"/etc/group",
]) {
if (await exists(source)) arguments_.push("--ro-bind", source, source);
}
for (const directory of missingDestinationAncestors(workspaceRoot)) {
arguments_.push("--dir", directory);
}
arguments_.push(
"--ro-bind", workspaceRoot, workspaceRoot,
);
if (await exists(path.join(workspaceRoot, ".git"))) {
arguments_.push("--tmpfs", path.join(workspaceRoot, ".git"));
}
arguments_.push(
"--bind", rawDirectory, rawDirectory,
"--ro-bind", candidateRoot, "/candidate",
"--chdir", workspaceRoot,
"/bin/sh", "-eu", "-c", command,
);
await new Promise<void>((resolve, reject) => {
const child = spawn("/usr/bin/bwrap", arguments_, {
env: { ...environment, PATH: `/tmp:${environment.PATH ?? ""}` },
stdio: "inherit",
});
let settled = false;
const finish = (error?: Error): void => {
if (settled) return;
settled = true;
clearTimeout(timeout);
error ? reject(error) : resolve();
};
const timeout = setTimeout(() => {
child.kill("SIGKILL");
finish(new Error("sandboxed external provider command timed out"));
}, 30 * 60 * 1_000);
child.once("error", (error) => finish(error));
child.once("close", (code, signal) => {
if (code === 0 && signal === null) finish();
else finish(new Error(`sandboxed external provider failed: exit=${code ?? "none"}, signal=${signal ?? "none"}`));
});
});
} finally {
await rm(scratch, { recursive: true, force: true });
const cpuSeconds = providerCpuSeconds();
const bwrapArguments = [
"--die-with-parent", "--new-session", "--as-pid-1",
"--unshare-pid", "--unshare-ipc", "--unshare-uts", "--unshare-net",
"--dev", "/dev", "--remount-ro", "/dev",
"--proc", "/proc", "--remount-ro", "/proc",
"--size", String(PROVIDER_TMP_BYTES), "--tmpfs", "/tmp",
"--dir", "/tmp/provider-home",
"--size", String(PROVIDER_MASK_BYTES), "--tmpfs", "/etc",
];
for (const source of ["/usr", "/bin", "/lib", "/lib64"]) {
if (await exists(source)) bwrapArguments.push("--ro-bind", source, source);
}
bwrapArguments.push("--ro-bind", process.execPath, "/tmp/node");
for (const source of [
"/etc/ca-certificates", "/etc/ssl", "/etc/nsswitch.conf", "/etc/passwd", "/etc/group",
]) {
if (await exists(source)) bwrapArguments.push("--ro-bind", source, source);
}
bwrapArguments.push("--remount-ro", "/etc");
for (const directory of missingDestinationAncestors(workspaceRoot)) {
bwrapArguments.push("--dir", directory);
}
bwrapArguments.push("--ro-bind", workspaceRoot, workspaceRoot);
if (await exists(path.join(workspaceRoot, ".git"))) {
bwrapArguments.push(
"--size", String(PROVIDER_MASK_BYTES),
"--tmpfs", path.join(workspaceRoot, ".git"),
"--remount-ro", path.join(workspaceRoot, ".git"),
);
}
bwrapArguments.push(
"--ro-bind", candidateRoot, "/candidate",
"--remount-ro", "/tmp",
"--remount-ro", "/",
"--bind", reportAbsolute, reportAbsolute,
"--chdir", workspaceRoot,
"--", "/usr/bin/prlimit",
"--core=0:0",
"--fsize=8388607:8388607",
"--nofile=64:64",
`--cpu=${cpuSeconds}:${cpuSeconds}`,
"--", "/bin/sh", "-eu", "-c",
'exec /bin/sh -eu -c "$PROVIDER_COMMAND"',
);
const unitName = formatProviderCgroupUnitName(
providerKind,
process.pid,
randomBytes(12).toString("hex"),
);
const bwrapInput = encodeProviderBwrapInput(bwrapArguments, {
...environment,
PATH: `/tmp:${environment.PATH ?? ""}`,
PROVIDER_COMMAND: command,
});
const scopeFrame = encodeProviderScopeFrame({
bwrapInput,
reportPath: reportAbsolute,
reportDev: reportIdentity.dev,
reportIno: reportIdentity.ino,
});
await waitForProvider(
spawn("/usr/bin/systemd-run", systemdRunProviderArguments(
unitName,
timeoutMs,
cpuSeconds,
process.execPath,
path.join(workspaceRoot, "scripts/lib/provider-scope-wrapper.ts"),
reportAbsolute,
reportIdentity.dev,
reportIdentity.ino,
), {
env: providerCgroupClientEnvironment(),
stdio: ["pipe", "pipe", "pipe"],
}),
timeoutMs,
unitName,
scopeFrame,
guardianExit,
);
}
async function waitForProvider(
child: ReturnType<typeof spawn>,
timeoutMs: number,
unitName: string,
scopeFrame: Buffer,
guardianExit: Promise<Error>,
): Promise<void> {
let termination: "guardian" | "timeout" | "output" | undefined;
let guardianError: Error | undefined;
let signalTermination!: () => void;
let kill: Promise<void> | undefined;
const terminationStarted = new Promise<void>((resolve) => { signalTermination = resolve; });
const close = new Promise<Readonly<{ code: number | null; error?: Error; signal: NodeJS.Signals | null }>>((resolve) => {
child.once("error", (error) => resolve({ code: null, error, signal: null }));
child.once("close", (code, signal) => resolve({ code, signal }));
});
let inputError: Error | undefined;
child.stdin?.once("error", (error) => { inputError = error; });
if (child.stdin) child.stdin.write(scopeFrame);
else inputError = new Error("systemd-run provider argument pipe is unavailable");
const terminate = (reason: "guardian" | "timeout" | "output"): void => {
if (termination) return;
termination = reason;
kill = killProviderUnit(unitName);
signalTermination();
};
const guardianLatch = createProviderScopeGuardianLatch(guardianExit);
void guardianLatch.activeFailure.then((error) => {
guardianError = error;
terminate("guardian");
});
const outputLimiter = createProviderOutputLimiter(
PROVIDER_MAX_OUTPUT_BYTES,
() => terminate("output"),
);
const capture = (chunk: Buffer | string): void => {
if (termination) return;
outputLimiter.consume(chunk);
};
child.stdout?.on("data", capture);
child.stderr?.on("data", capture);
const timeout = setTimeout(() => terminate("timeout"), timeoutMs);
try {
const first = await Promise.race([
close.then((result) => ({ type: "close" as const, result })),
terminationStarted.then(() => ({ type: "termination" as const })),
]);
let collection: Promise<void> | undefined;
if (first.type === "close" && !termination) {
collection = waitForProviderUnitCollected(unitName);
const scopeOutcome = await Promise.race([
collection.then(() => "collected" as const),
terminationStarted.then(() => "termination" as const),
]);
if (scopeOutcome === "collected") {
await guardianLatch.close();
const boundaryFailure = guardianLatch.failure();
if (boundaryFailure && !termination) {
guardianError = boundaryFailure;
terminate("guardian");
}
}
}
if (termination) {
let killError: Error | undefined;
try { await kill; } catch (error) { killError = toError(error); child.kill("SIGKILL"); }
const closed = await waitForProviderClose(close);
if (!closed) child.kill("SIGKILL");
let collectionError: Error | undefined;
try {
await (collection ?? waitForProviderUnitCollected(unitName));
} catch (error) {
collectionError = toError(error);
}
await guardianLatch.close();
const reason = termination === "timeout"
? "sandboxed external provider command timed out"
: termination === "output"
? `sandboxed external provider output exceeded the ${PROVIDER_MAX_OUTPUT_BYTES}-byte aggregate limit`
: `provider raw guardian failed${guardianError ? `: ${guardianError.message}` : ""}`;
if (!closed) throw new Error(`${reason} and systemd-run did not close within the reap bound`);
if (collectionError) throw new Error(`${reason}; provider cgroup collection failed: ${collectionError.message}`);
if (killError) throw new Error(`${reason}; provider cgroup kill failed: ${killError.message}`);
throw new Error(reason);
}
const result = first.type === "close" ? first.result : await close;
await collection;
if (result.error) throw result.error;
if (result.code !== 0 || result.signal !== null) {
throw new Error(`sandboxed external provider failed: exit=${result.code ?? "none"}, signal=${result.signal ?? "none"}`);
}
if (inputError) throw inputError;
} finally {
clearTimeout(timeout);
await guardianLatch.close();
}
}
async function waitForProviderClose(
close: Promise<Readonly<{ code: number | null; error?: Error; signal: NodeJS.Signals | null }>>,
): Promise<boolean> {
let timer: NodeJS.Timeout | undefined;
const closed = await Promise.race([
close.then(() => true),
new Promise<false>((resolve) => { timer = setTimeout(() => resolve(false), PROVIDER_REAP_TIMEOUT_MS); }),
]);
if (timer) clearTimeout(timer);
return closed;
}
async function assertProviderCgroupManagerAvailable(): Promise<void> {
const output = await runBoundedSystemctl(
["--user", "show", "--property=Version", "--value"],
"provider cgroup user manager probe",
);
const match = /^([0-9]+)/u.exec(output.trim());
const version = match ? Number(match[1]) : Number.NaN;
if (!Number.isSafeInteger(version) || version < MINIMUM_PROVIDER_SYSTEMD_VERSION) {
throw new Error(`provider cgroup unavailable: systemd ${MINIMUM_PROVIDER_SYSTEMD_VERSION} or newer is required`);
}
}
async function killProviderUnit(unitName: string): Promise<void> {
await runBoundedSystemctl(systemctlKillProviderArguments(unitName), "provider cgroup unit kill");
}
async function waitForProviderUnitCollected(unitName: string): Promise<void> {
const deadline = Date.now() + PROVIDER_CONTROL_TIMEOUT_MS;
let loadState = "unknown";
while (Date.now() <= deadline) {
loadState = (await runBoundedSystemctl(
["--user", "show", unitName, "--property=LoadState", "--value"],
"provider cgroup collection probe",
)).trim();
if (loadState === "not-found") return;
await new Promise<void>((resolve) => setTimeout(resolve, PROVIDER_CONTROL_POLL_MS));
}
throw new Error(`provider unit remained loaded with state ${loadState || "unknown"}`);
}
async function runBoundedSystemctl(arguments_: readonly string[], label: string): Promise<string> {
return await new Promise<string>((resolve, reject) => {
const child = spawn("/usr/bin/systemctl", arguments_, {
env: providerCgroupClientEnvironment(),
stdio: ["ignore", "pipe", "pipe"],
});
let bytes = 0;
const chunks: Buffer[] = [];
let settled = false;
let termination: "output" | "timeout" | undefined;
let reap: NodeJS.Timeout | undefined;
const finish = (error?: Error): void => {
if (settled) return;
settled = true;
clearTimeout(timeout);
if (reap) clearTimeout(reap);
error ? reject(error) : resolve(Buffer.concat(chunks).toString("utf8"));
};
const terminate = (reason: "output" | "timeout"): void => {
if (termination) return;
termination = reason;
child.kill("SIGKILL");
reap = setTimeout(() => finish(new Error(`${label} ${reason} bound was exceeded and systemctl did not close`)), PROVIDER_REAP_TIMEOUT_MS);
};
const capture = (chunk: Buffer | string): void => {
if (termination) return;
const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
const remaining = PROVIDER_CONTROL_MAX_OUTPUT_BYTES - bytes;
if (remaining > 0) { chunks.push(value.subarray(0, remaining)); bytes += Math.min(value.length, remaining); }
if (value.length > remaining) terminate("output");
};
child.stdout?.on("data", capture);
child.stderr?.on("data", capture);
const timeout = setTimeout(() => terminate("timeout"), PROVIDER_CONTROL_TIMEOUT_MS);
child.once("error", (error) => finish(error));
child.once("close", (code, signal) => {
if (termination) return finish(new Error(`${label} ${termination} bound was exceeded`));
if (code === 0 && signal === null) return finish();
const detail = Buffer.concat(chunks).toString("utf8").trim();
finish(new Error(`${label} failed: exit=${code ?? "none"}, signal=${signal ?? "none"}${detail ? `, output=${detail}` : ""}`));
});
});
}
function providerCgroupClientEnvironment(): NodeJS.ProcessEnv {
const environment: NodeJS.ProcessEnv = { PATH: "/usr/bin:/bin" };
for (const name of ["DBUS_SESSION_BUS_ADDRESS", "HOME", "LANG", "LC_ALL", "LOGNAME", "USER", "XDG_RUNTIME_DIR"] as const) {
if (process.env[name]) environment[name] = process.env[name];
}
return environment;
}
function providerCpuSeconds(): number {
const value = process.env.PROVIDER_SUPERVISOR_CPU_SECONDS;
if (!value) return DEFAULT_PROVIDER_CPU_SECONDS;
const parsed = Number(value);
if (!Number.isSafeInteger(parsed) || parsed <= 0 || parsed > DEFAULT_PROVIDER_CPU_SECONDS) {
throw new TypeError("PROVIDER_SUPERVISOR_CPU_SECONDS must be a positive integer no greater than 1200");
}
return parsed;
}
function providerTimeoutMs(): number {
const value = process.env.PROVIDER_SUPERVISOR_TIMEOUT_MS;
if (!value) return DEFAULT_PROVIDER_TIMEOUT_MS;
const parsed = Number(value);
if (!Number.isSafeInteger(parsed) || parsed <= 0 || parsed > DEFAULT_PROVIDER_TIMEOUT_MS) {
throw new TypeError("PROVIDER_SUPERVISOR_TIMEOUT_MS must be a positive integer no greater than 1800000");
}
return parsed;
}
function toError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error));
}
function missingDestinationAncestors(target: string): string[] {
@@ -269,6 +578,21 @@ async function prepareMissingProviderOutput(
}
}
async function assertOwnedProviderOutput(
absolutePath: string,
identity: Readonly<{ dev: number; ino: number }>,
): Promise<void> {
const metadata = await lstat(absolutePath);
if (
metadata.isSymbolicLink() ||
!metadata.isFile() ||
metadata.dev !== identity.dev ||
metadata.ino !== identity.ino
) {
throw new Error("provider raw output identity changed");
}
}
function hasErrorCode(error: unknown, code: string): boolean {
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
}
+8 -12
View File
@@ -9,23 +9,17 @@ import {
isValidSourceDateEpoch,
} from "./lib/build-environment.ts";
import { classifyGateStepResult } from "./lib/ci-step-result.ts";
import {
indexCiGateContract,
loadCiGateContract,
} from "./contracts/ci-gates.ts";
import { withCiGatePreflight } from "./contracts/ci-gates.ts";
import { validateCiArtifact } from "./lib/ci-artifact-validator.ts";
import { writeCiGateLogAtomic } from "./lib/ci-gate-log.ts";
const gateId = process.argv
const requestedGateId = process.argv
.slice(2)
.find((argument) => /^FE-GATE-\d{3}$/.test(argument));
const contract = await loadCiGateContract(process.cwd());
const contractIndex = indexCiGateContract(contract);
const gate = gateId ? contractIndex.gates.get(gateId) : undefined;
if (!gateId || !gate) {
process.stderr.write("Usage: ci:gate -- FE-GATE-001..FE-GATE-026\n");
process.exit(2);
}
await withCiGatePreflight(
process.cwd(),
requestedGateId,
async ({ contractIndex, gateId, gate }) => {
const logArtifact = contractIndex.artifacts.get(gate.logArtifactId);
if (!logArtifact) throw new TypeError(`CI gate log artifact disappeared: ${gate.logArtifactId}`);
@@ -252,4 +246,6 @@ if (!passed) {
}
process.stdout.write(
`${gateId} ${gate.name}: PASS (${gate.retentionClassId})\n`,
);
},
);
+2 -43
View File
@@ -1,44 +1,3 @@
import { appendFile } from "node:fs/promises";
import { runStageVerifiedPromotionCli } from "./lib/stage-verified-promotion-cli.ts";
import { finalizeVerifiedPromotion } from "./lib/promotion-stager.ts";
const required = (name: string): string => {
const value = process.env[name];
if (!value) throw new TypeError(`promotion staging environment is missing ${name}`);
return value;
};
const staged = await finalizeVerifiedPromotion({
repositoryRoot: process.cwd(),
archivePath: required("CANDIDATE_ARCHIVE_PATH"),
expectedArchiveSha256: required("CANDIDATE_ARCHIVE_SHA256"),
vulnerabilityReportPath: required("VULNERABILITY_REPORT_PATH"),
provenanceAttestationPath: required("PROVENANCE_ATTESTATION_PATH"),
vulnerabilityPublicKeyPath: required("VULNERABILITY_PUBLIC_KEY_PATH"),
vulnerabilityKeyId: required("VULNERABILITY_KEY_ID"),
provenancePublicKeyPath: required("PROVENANCE_PUBLIC_KEY_PATH"),
provenanceKeyId: required("PROVENANCE_KEY_ID"),
expectedRun: {
id: process.env.GITEA_RUN_ID ?? process.env.GITHUB_RUN_ID ?? required("CI_RUN_ID"),
attempt: Number(process.env.GITEA_RUN_ATTEMPT ?? process.env.GITHUB_RUN_ATTEMPT ?? required("CI_RUN_ATTEMPT")),
sourceRevision: process.env.EXPECTED_SOURCE_REVISION ?? required("VITE_COMMIT_SHA"),
},
vulnerabilityInvocationNonce: required("VULNERABILITY_INVOCATION_NONCE"),
provenanceInvocationNonce: required("PROVENANCE_INVOCATION_NONCE"),
runnerTempRoot: required("RUNNER_TEMP"),
});
const output = required("GITHUB_OUTPUT");
await appendFile(
output,
[
`staging_root=${staged.stagingRoot}`,
`cleanup_token=${staged.cleanupToken}`,
`runner_temp_dev=${staged.runnerTempIdentity.dev}`,
`runner_temp_ino=${staged.runnerTempIdentity.ino}`,
"",
].join("\n"),
{ encoding: "utf8" },
);
process.stdout.write(
`Promotion staging: ${staged.files.map(({ name, sha256 }) => `${name}=${sha256}`).join(", ")} PASS\n`,
);
await runStageVerifiedPromotionCli(process.env);
+22 -20
View File
@@ -1,9 +1,11 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { mkdir, readFile } from "node:fs/promises";
import { manualA11yReportArtifactSchema } from "./contracts/release-artifacts.ts";
import {
MANUAL_A11Y_ROUTE_IDS,
validateManualA11yEvidence,
} from "./lib/manual-a11y-evidence.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
type ManualA11yResult = Readonly<{
routeId: string;
@@ -34,34 +36,34 @@ for (const routeId of MANUAL_A11Y_ROUTE_IDS) {
passed: validation.passed && failures.length === 0,
});
}
const releaseIds = new Set(results.map((result) => result.releaseId));
const releaseIds = new Set(
results.map((result) => result.releaseId).filter((releaseId): releaseId is string => Boolean(releaseId)),
);
const coherentRelease =
releaseIds.size === 1 && results.every((result) => Boolean(result.releaseId));
const passed =
results.every((result) => result.passed) &&
releaseIds.size === 1 &&
results.every((result) => Boolean(result.releaseId));
coherentRelease;
await mkdir("artifacts/tests/a11y-manual", { recursive: true });
await writeFile(
"artifacts/tests/a11y-manual/report.json",
`${JSON.stringify(
{
schemaVersion: 1,
generatedAt: new Date().toISOString(),
scope: MANUAL_A11Y_ROUTE_IDS,
results,
coherentRelease: releaseIds.size === 1,
passed,
},
null,
2,
)}\n`,
);
await writeValidatedJsonArtifact({
path: "artifacts/tests/a11y-manual/report.json",
schema: manualA11yReportArtifactSchema,
value: {
schemaVersion: 1,
generatedAt: new Date().toISOString(),
scope: MANUAL_A11Y_ROUTE_IDS,
results,
coherentRelease,
passed,
},
});
if (!passed) {
const failures = results
.filter((result) => !result.passed)
.map((result) => `${result.routeId}: ${result.failures.join(", ")}`);
if (releaseIds.size !== 1) failures.push("release IDs do not match");
if (!coherentRelease) failures.push("release IDs do not match");
process.stderr.write(
`Manual accessibility evidence is incomplete:\n${failures.join("\n")}\n`,
);
+35 -7
View File
@@ -1,18 +1,36 @@
import { readFile } from "node:fs/promises";
import {
captureCiCandidateArchive,
withVerifiedCapturedCandidate,
} from "./lib/ci-candidate-archive.ts";
import { verifyArchivedLocalEvidence } from "./lib/local-release-evidence.ts";
import {
RELEASE_CANDIDATE_MANIFEST_PATH,
releaseCandidateManifestSchema,
} from "./lib/release-candidate.ts";
const candidate = releaseCandidateManifestSchema.parse(
JSON.parse(await readFile(RELEASE_CANDIDATE_MANIFEST_PATH, "utf8")),
);
const result = await verifyArchivedLocalEvidence({
extractionRoot: process.cwd(),
expectedManifest: candidate,
});
const argument = (name: string): string | undefined => {
const index = process.argv.indexOf(name);
if (index < 0) return undefined;
const value = process.argv[index + 1];
if (!value || value.startsWith("--")) {
throw new TypeError(`${name} requires a value`);
}
return value;
};
const archivePath = argument("--archive");
const expectedSha256 = argument("--sha256");
if (Boolean(archivePath) !== Boolean(expectedSha256)) {
throw new TypeError("--archive and --sha256 must be supplied together");
}
const result = archivePath && expectedSha256
? await withVerifiedCapturedCandidate({
captured: await captureCiCandidateArchive({ archivePath, expectedSha256 }),
verify: ({ extractionRoot, manifest }) =>
verifyArchivedLocalEvidence({ extractionRoot, expectedManifest: manifest }),
})
: await verifyCheckoutEvidence();
if (result.status !== "PASS") {
process.stderr.write(
`Archived local evidence verification failed:\n- ${result.failures.join("\n- ")}\n`,
@@ -20,3 +38,13 @@ if (result.status !== "PASS") {
process.exit(1);
}
process.stdout.write("Archived local evidence verification: PASS\n");
async function verifyCheckoutEvidence() {
const candidate = releaseCandidateManifestSchema.parse(
JSON.parse(await readFile(RELEASE_CANDIDATE_MANIFEST_PATH, "utf8")),
);
return verifyArchivedLocalEvidence({
extractionRoot: process.cwd(),
expectedManifest: candidate,
});
}
+19 -19
View File
@@ -1,4 +1,7 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { mkdir, readFile } from "node:fs/promises";
import { documentationReviewArtifactSchema } from "./contracts/release-artifacts.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
type DocumentationReview = Readonly<{
sourcePath: string;
@@ -60,24 +63,21 @@ const passed =
results.length === 2 &&
results.every((result) => result.passed);
await mkdir("artifacts/quality", { recursive: true });
await writeFile(
"artifacts/quality/documentation-review.json",
`${JSON.stringify(
{
schemaVersion: 1,
generatedAt: new Date().toISOString(),
status: ledger.status,
reviewer: ledger.reviewer,
standard: ledger.standard,
evidenceReport: ledger.evidenceReport,
reportDigestValid,
results,
passed,
},
null,
2,
)}\n`,
);
await writeValidatedJsonArtifact({
path: "artifacts/quality/documentation-review.json",
schema: documentationReviewArtifactSchema,
value: {
schemaVersion: 1,
generatedAt: new Date().toISOString(),
status: ledger.status,
reviewer: ledger.reviewer,
standard: ledger.standard,
evidenceReport: ledger.evidenceReport,
reportDigestValid,
results,
passed,
},
});
if (!passed) {
process.stderr.write(
"Documentation readiness: FAIL_UNVERIFIED (canonical scoped-review evidence is incomplete)\n",
+94
View File
@@ -0,0 +1,94 @@
import path from "node:path";
import { readdir } from "node:fs/promises";
import { PROMOTED_FILE_NAMES } from "./contracts/promotion-artifacts.ts";
import { readBoundedRegularFile } from "./lib/ci-artifact-validator.ts";
import { verifyExactPromotionBundle } from "./lib/exact-promotion-bundle.ts";
import { readProviderTrust } from "./lib/provider-trust.ts";
const required = (name: string): string => {
const value = process.env[name];
if (!value) throw new TypeError(`exact promotion verification environment is missing ${name}`);
return value;
};
const rootArgument = process.argv.indexOf("--root");
const bundleRoot = path.resolve(
rootArgument >= 0
? process.argv[rootArgument + 1] ?? ""
: required("PROMOTION_BUNDLE_ROOT"),
);
if (rootArgument >= 0 && !process.argv[rootArgument + 1]) {
throw new TypeError("--root requires a promotion bundle directory");
}
const trustRoot = process.cwd();
const [vulnerabilityTrust, provenanceTrust] = await Promise.all([
readProviderTrust(
trustRoot,
required("VULNERABILITY_PUBLIC_KEY_PATH"),
required("VULNERABILITY_KEY_ID"),
),
readProviderTrust(
trustRoot,
required("PROVENANCE_PUBLIC_KEY_PATH"),
required("PROVENANCE_KEY_ID"),
),
]);
if (!vulnerabilityTrust || !provenanceTrust) {
throw new TypeError("exact promotion verification trust keys are invalid");
}
const names = (await readdir(bundleRoot)).sort(asciiCompare);
if (
JSON.stringify(names) !==
JSON.stringify([...PROMOTED_FILE_NAMES].sort(asciiCompare))
) {
throw new TypeError("promotion bundle directory must contain exactly the canonical five files");
}
const files = Object.fromEntries(
await Promise.all(
PROMOTED_FILE_NAMES.map(async (name) => [
name,
await readBoundedRegularFile({
root: bundleRoot,
relativePath: name,
maxBytes: name === "release-candidate.tar.gz" ? 268_435_456 : 16_777_216,
}),
] as const),
),
);
await verifyExactPromotionBundle(files, {
vulnerabilityTrust,
provenanceTrust,
expected: {
run: {
id: required("EXPECTED_PROMOTION_RUN_ID"),
attempt: requiredPositiveInteger("EXPECTED_PROMOTION_RUN_ATTEMPT"),
},
sourceRevision: required("EXPECTED_PROMOTION_SOURCE_REVISION"),
archiveSha256: required("EXPECTED_PROMOTION_ARCHIVE_SHA256"),
...optionalDigest("sourceSetSha256", "EXPECTED_PROMOTION_SOURCE_SET_SHA256"),
...optionalDigest("bundleSha256", "EXPECTED_PROMOTION_BUNDLE_SHA256"),
...optionalDigest("distSha256", "EXPECTED_PROMOTION_DIST_SHA256"),
...optionalDigest("lockfileSha256", "EXPECTED_PROMOTION_LOCKFILE_SHA256"),
},
});
process.stdout.write("Exact promotion bundle verification: PASS\n");
function asciiCompare(left: string, right: string): number {
return left < right ? -1 : left > right ? 1 : 0;
}
function requiredPositiveInteger(name: string): number {
const value = required(name);
if (!/^[1-9][0-9]*$/u.test(value) || !Number.isSafeInteger(Number(value))) {
throw new TypeError(`${name} must be a positive safe integer`);
}
return Number(value);
}
function optionalDigest(
property: "sourceSetSha256" | "bundleSha256" | "distSha256" | "lockfileSha256",
environmentName: string,
): Readonly<Record<string, string>> {
const value = process.env[environmentName];
return value ? { [property]: value } : {};
}
+16 -17
View File
@@ -1,6 +1,8 @@
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import { mkdir, readFile, readdir } from "node:fs/promises";
import { hostingHeadersArtifactSchema } from "./contracts/release-artifacts.ts";
import { classifyLiveHostingBaseUrl } from "./lib/hosting-probe.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
type Document = Record<string, unknown>;
type ResponseHeaders = Record<string, Record<string, string>>;
@@ -194,22 +196,19 @@ results.push({
const passed = results.every((result) => result.passed);
await mkdir("artifacts/release", { recursive: true });
await writeFile(
"artifacts/release/hosting-headers.json",
`${JSON.stringify(
{
schemaVersion: 1,
generatedAt: new Date().toISOString(),
mode,
baseUrl: liveTarget?.observedOrigin ?? null,
providerVerificationRequired: mode !== "live",
results,
passed,
},
null,
2,
)}\n`,
);
await writeValidatedJsonArtifact({
path: "artifacts/release/hosting-headers.json",
schema: hostingHeadersArtifactSchema,
value: {
schemaVersion: 1,
generatedAt: new Date().toISOString(),
mode,
baseUrl: liveTarget?.observedOrigin ?? null,
providerVerificationRequired: mode !== "live",
results,
passed,
},
});
if (!passed) {
process.stderr.write(
-26
View File
@@ -1,26 +0,0 @@
import { mkdir } from "node:fs/promises";
import path from "node:path";
import { providerVerificationArtifactSchema } from "./lib/provider-evidence.ts";
import { verifyPromotionInputs } from "./lib/promotion-verifier.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
const report = await verifyPromotionInputs({
artifactType: "provider-verification",
repositoryRoot: path.resolve(process.env.CANDIDATE_ROOT ?? process.cwd()),
providerEvidenceRoot: process.cwd(),
trustRoot: process.cwd(),
});
await mkdir("artifacts/security", { recursive: true });
await writeValidatedJsonArtifact({
path: "artifacts/security/provider-verification.json",
schema: providerVerificationArtifactSchema,
value: report,
});
if (report.status !== "PASS") {
process.stderr.write(
`Provider evidence is FAIL_UNVERIFIED:\n- ${report.failures.join("\n- ")}\n`,
);
process.exit(1);
}
process.stdout.write("Provider evidence: PASS\n");
+20 -21
View File
@@ -1,9 +1,11 @@
import { spawnSync } from "node:child_process";
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import { mkdir, readFile, readdir } from "node:fs/promises";
import path from "node:path";
import { reproducibleBuildArtifactSchema } from "./contracts/release-artifacts.ts";
import { assertCiBuildEnvironment } from "./lib/build-environment.ts";
import { supplyChainDigest } from "./lib/supply-chain.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
assertCiBuildEnvironment(process.env);
@@ -54,26 +56,23 @@ const passed =
firstDigest === secondDigest;
await mkdir("artifacts/release", { recursive: true });
await writeFile(
"artifacts/release/reproducible-build.json",
`${JSON.stringify(
{
schemaVersion: 1,
sourceDateEpoch: deterministicEnvironment.SOURCE_DATE_EPOCH,
buildId: process.env.VITE_BUILD_ID ?? "local-build",
commitSha: process.env.VITE_COMMIT_SHA ?? "local",
releaseId: process.env.RELEASE_ID ?? "local-release",
runnerImage:
process.env.CI_RUNNER_IMAGE ?? `${process.platform}-${process.arch}`,
firstDigest,
secondDigest,
restored: restoreBuild.status === 0,
status: passed ? "PASS" : "FAIL",
},
null,
2,
)}\n`,
);
await writeValidatedJsonArtifact({
path: "artifacts/release/reproducible-build.json",
schema: reproducibleBuildArtifactSchema,
value: {
schemaVersion: 1,
sourceDateEpoch: deterministicEnvironment.SOURCE_DATE_EPOCH,
buildId: process.env.VITE_BUILD_ID ?? "local-build",
commitSha: process.env.VITE_COMMIT_SHA ?? "local",
releaseId: process.env.RELEASE_ID ?? "local-release",
runnerImage:
process.env.CI_RUNNER_IMAGE ?? `${process.platform}-${process.arch}`,
firstDigest,
secondDigest,
restored: restoreBuild.status === 0,
status: passed ? "PASS" : "FAIL",
},
});
if (!passed) {
process.stderr.write(
`Reproducible build failed: first=${firstDigest} second=${secondDigest}\n`,
-26
View File
@@ -1,26 +0,0 @@
import { mkdir } from "node:fs/promises";
import path from "node:path";
import { providerVerificationArtifactSchema } from "./lib/provider-evidence.ts";
import { verifyPromotionInputs } from "./lib/promotion-verifier.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
const report = await verifyPromotionInputs({
artifactType: "promotion-verification",
repositoryRoot: path.resolve(process.env.CANDIDATE_ROOT ?? process.cwd()),
providerEvidenceRoot: process.cwd(),
trustRoot: process.cwd(),
});
await mkdir("artifacts/security", { recursive: true });
await writeValidatedJsonArtifact({
path: "artifacts/security/promotion-verification.json",
schema: providerVerificationArtifactSchema,
value: report,
});
if (report.status !== "PASS") {
process.stderr.write(
`Supply-chain promotion is FAIL_UNVERIFIED:\n- ${report.failures.join("\n- ")}\n`,
);
process.exit(1);
}
process.stdout.write("Supply-chain promotion evidence: PASS\n");
+15 -16
View File
@@ -1,20 +1,19 @@
import { mkdir, writeFile } from "node:fs/promises";
import { mkdir } from "node:fs/promises";
import { automatedA11yArtifactSchema } from "./contracts/release-artifacts.ts";
import { MANUAL_A11Y_ROUTE_IDS } from "./lib/manual-a11y-evidence.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
await mkdir("artifacts/tests", { recursive: true });
await writeFile(
"artifacts/tests/a11y.json",
`${JSON.stringify(
{
schemaVersion: 1,
generatedAt: new Date().toISOString(),
scope: MANUAL_A11Y_ROUTE_IDS,
threshold: { critical: 0, serious: 0 },
automatedStatus: "passed",
manualReview: "see artifacts/tests/a11y-manual/report.json",
},
null,
2,
)}\n`,
);
await writeValidatedJsonArtifact({
path: "artifacts/tests/a11y.json",
schema: automatedA11yArtifactSchema,
value: {
schemaVersion: 1,
generatedAt: new Date().toISOString(),
scope: MANUAL_A11Y_ROUTE_IDS,
threshold: { critical: 0, serious: 0 },
automatedStatus: "passed",
manualReview: "see artifacts/tests/a11y-manual/report.json",
},
});