Files
clean-architecture-frontend…/scripts/lib/local-release-evidence.ts
T

662 lines
20 KiB
TypeScript

import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { z, type ZodType } from "zod";
import { EXPECTED_CONTRACT_SET_PACKAGES } from "../../src/features/installed-contract-contributions.ts";
import {
ROUTE_REGISTRY,
ROUTE_RUNTIME_CONTRACT,
} from "../../src/features/installed-feature-contracts.ts";
import {
buildManifestArtifactSchema,
bundlePerformanceArtifactSchema,
dependencyDiffArtifactSchema,
dependencyInventoryArtifactSchema,
licenseReportArtifactSchema,
provenanceArtifactSchema,
releaseManifestArtifactSchema,
releaseVerificationArtifactSchema,
runtimeConfigArtifactSchema,
sbomArtifactSchema,
supplyChainVerificationArtifactSchema,
vulnerabilityReportArtifactSchema,
} from "../contracts/release-artifacts.ts";
import {
CANONICAL_VITE_MANIFEST_PATH,
verifyBuildManifestOutputs,
} from "./build-manifest-outputs.ts";
import { assertMatchesJsonSchema } from "./json-schema.ts";
import type { ReleaseCandidateManifest } from "./release-candidate.ts";
import { collectDistOutputs, distSha256 } from "./release-candidate.ts";
import { verifyReleaseRuntimeCoherence } from "./release-runtime-coherence.ts";
import { digestReleaseInputFiles } from "./release-input-evidence.ts";
import {
compareStoredDependencyEvidence,
compareStoredLicenseEvidence,
compareStoredLocalVulnerabilityReport,
recomputeDependencyEvidence,
recomputeLicenseEvidence,
verifyLocalSupplyChainDefaults,
verifyStoredDistChecksums,
} from "./local-policy-evidence.ts";
import {
buildRepositoryFileInventory,
parseRepositoryFileInventoryPolicy,
} from "./repository-file-inventory.ts";
import {
evaluateRepositorySecretScan,
verifyStoredSecretScan,
} from "./secret-scan-evaluator.ts";
import {
isValidSha512Integrity,
parsePnpmLockfilePackages,
supplyChainDigest,
verifySupplyChainCoherence,
} from "./supply-chain.ts";
const sha256 = z.string().regex(/^[a-f0-9]{64}$/u);
export const supplyChainCoherenceReportSchema = z
.object({
schemaVersion: z.literal(1),
status: z.enum(["PASS", "FAIL"]),
dependencyCount: z.int().nonnegative(),
lockfileSha256: sha256,
distSha256: sha256,
sbomSha256: sha256,
failures: z.array(z.string()),
})
.strict();
export type SupplyChainCoherenceReport = z.infer<
typeof supplyChainCoherenceReportSchema
>;
export async function verifyLocalSupplyChainEvidence(
repositoryRoot = process.cwd(),
): Promise<SupplyChainCoherenceReport> {
const failures: string[] = [];
const inventory = await parseArtifact(
repositoryRoot,
"artifacts/release/dependency-inventory.json",
dependencyInventoryArtifactSchema,
"dependency inventory",
failures,
);
const sbom = await parseArtifact(
repositoryRoot,
"artifacts/release/sbom.cdx.json",
sbomArtifactSchema,
"SBOM",
failures,
);
const provenance = await parseArtifact(
repositoryRoot,
"artifacts/release/provenance.json",
provenanceArtifactSchema,
"local provenance",
failures,
);
const verification = await parseArtifact(
repositoryRoot,
"artifacts/security/supply-chain-verification.json",
supplyChainVerificationArtifactSchema,
"supply-chain verification",
failures,
);
for (const [schemaPath, artifact, label] of [
[
"schemas/artifacts/dependency-inventory.schema.json",
inventory,
"dependency inventory",
],
[
"schemas/artifacts/supply-chain-verification.schema.json",
verification,
"supply-chain verification",
],
] as const) {
if (!artifact) continue;
try {
assertMatchesJsonSchema(
await readJson(repositoryRoot, schemaPath),
artifact,
label,
);
} catch {
failures.push(`${label} JSON Schema mismatch`);
}
}
let lockfileText = "";
let lockfileSha256 = "0".repeat(64);
try {
const rawLockfile = await readFile(
path.join(repositoryRoot, "pnpm-lock.yaml"),
);
lockfileText = rawLockfile.toString("utf8");
lockfileSha256 = createHash("sha256").update(rawLockfile).digest("hex");
} catch {
failures.push("raw pnpm-lock.yaml is missing or unreadable");
}
let distDigest = "0".repeat(64);
try {
distDigest = distSha256(await collectDistOutputs(repositoryRoot));
} catch {
failures.push("candidate dist is missing or unreadable");
}
const sbomSha256 = sbom ? supplyChainDigest(sbom) : "0".repeat(64);
let coherenceFailures: readonly string[] = Object.freeze([]);
if (inventory && sbom && provenance) {
coherenceFailures = verifySupplyChainCoherence(
sbom,
inventory,
provenance,
distDigest,
).failures;
failures.push(...coherenceFailures);
}
if (
!inventory ||
!verification ||
inventory.lockfileSha256 !== lockfileSha256 ||
verification.lockfileSha256 !== lockfileSha256
) {
failures.push("inventory/verification lockfile digest mismatch");
}
if (
!verification ||
verification.localStatus !== "PASS" ||
verification.failures.length > 0 ||
verification.distSha256 !== distDigest ||
verification.sbomSha256 !== sbomSha256
) {
failures.push("verification digest/status set is incoherent");
}
if (verification) {
failures.push(...verifyLocalSupplyChainDefaults(verification));
}
if (inventory && sbom && provenance && verification) {
try {
const policy = parseRepositoryFileInventoryPolicy(
await readJson(
repositoryRoot,
"config/security/secret-scan-policy.json",
),
);
const repositoryInventory = await buildRepositoryFileInventory({
repositoryRoot,
trackedRoots: policy.trackedRoots,
generatedRoots: policy.generatedRoots,
optionalRoots: policy.optionalRoots,
});
const sourceSetSha256 = await digestReleaseInputFiles(
repositoryInventory.trackedFiles,
(file) => readFile(path.join(repositoryRoot, file)),
);
if (
verification.sourceSetSha256 !== sourceSetSha256 ||
provenance.predicate.materials.sourceSetSha256 !== sourceSetSha256 ||
provenance.predicate.materials.sbomSha256 !== sbomSha256
) {
failures.push("source/SBOM provenance materials are incoherent");
}
} catch {
failures.push("release source inventory is unavailable or unreadable");
}
}
if (inventory && verification) {
try {
const dependencyPolicy = recomputeDependencyEvidence({
inventory,
baseline: await optionalReadJson(
repositoryRoot,
"config/security/dependency-baseline.json",
),
baselineApproval: await optionalReadJson(
repositoryRoot,
"config/security/dependency-baseline.approval.json",
),
dependencyChangeEvidence: await readJson(
repositoryRoot,
"config/security/dependency-change-evidence.json",
),
});
const licensePolicy = recomputeLicenseEvidence({
inventory,
policy: await readJson(
repositoryRoot,
"config/security/dependency-policy.json",
),
});
const expectedFailures = [
...licensePolicy.failures,
...dependencyPolicy.failures,
...coherenceFailures,
];
if (
supplyChainDigest(verification.dependencyDiff) !==
supplyChainDigest(dependencyPolicy.dependencyDiff) ||
supplyChainDigest(verification.highRiskReview) !==
supplyChainDigest(dependencyPolicy.highRisk) ||
supplyChainDigest(verification.failures) !==
supplyChainDigest(expectedFailures) ||
verification.localStatus !==
(expectedFailures.length === 0 ? "PASS" : "FAIL")
) {
failures.push(
"supply-chain verification policy fields do not match recomputed evidence",
);
}
} catch {
failures.push("supply-chain verification policy inputs are invalid");
}
}
const lockRows = parsePnpmLockfilePackages(lockfileText);
const inventoryRows = inventory?.dependencies ?? [];
const inventoryByIdentity = new Map<string, (typeof inventoryRows)[number]>(
inventoryRows.map(
(entry) => [`${entry.name}@${entry.version}`, entry] as const,
),
);
if (lockRows.length !== inventoryRows.length) {
failures.push("transitive dependency count differs from lockfile");
}
for (const lockRow of lockRows) {
const identity = `${lockRow.name}@${lockRow.version}`;
const dependency = inventoryByIdentity.get(identity);
if (
!dependency ||
dependency.integrity !== lockRow.integrity ||
!isValidSha512Integrity(lockRow.integrity)
) {
failures.push(`lockfile inventory integrity mismatch: ${identity}`);
}
}
return supplyChainCoherenceReportSchema.parse({
schemaVersion: 1,
status: failures.length === 0 ? "PASS" : "FAIL",
dependencyCount: inventoryRows.length,
lockfileSha256,
distSha256: distDigest,
sbomSha256,
failures,
});
}
export async function verifyArchivedLocalEvidence(input: Readonly<{
repositoryRoot?: string;
candidate: ReleaseCandidateManifest;
}>): Promise<Readonly<{
status: "PASS" | "FAIL";
failures: readonly string[];
}>> {
const repositoryRoot = path.resolve(input.repositoryRoot ?? process.cwd());
const failures: string[] = [];
const supplyReport = await verifyLocalSupplyChainEvidence(repositoryRoot);
failures.push(...supplyReport.failures);
if (supplyReport.lockfileSha256 !== input.candidate.lockfileSha256) {
failures.push("candidate/raw lockfile digest mismatch");
}
const buildManifest = await parseArtifact(
repositoryRoot,
"artifacts/release/build-manifest.json",
buildManifestArtifactSchema,
"build manifest",
failures,
);
const release = await parseArtifact(
repositoryRoot,
"dist/release-manifest.json",
releaseManifestArtifactSchema,
"release manifest",
failures,
);
const runtime = await parseArtifact(
repositoryRoot,
"dist/config.json",
runtimeConfigArtifactSchema,
"runtime config",
failures,
);
const storedRelease = await parseArtifact(
repositoryRoot,
"artifacts/release/verification.json",
releaseVerificationArtifactSchema,
"release verification",
failures,
);
const storedSupply = await parseArtifact(
repositoryRoot,
"artifacts/security/supply-chain-coherence.json",
supplyChainCoherenceReportSchema,
"supply-chain coherence",
failures,
);
await validateSupportingArtifacts(
repositoryRoot,
failures,
supplyReport.lockfileSha256,
);
if (buildManifest) {
try {
assertMatchesJsonSchema(
await readJson(
repositoryRoot,
"schemas/artifacts/build-manifest.schema.json",
),
buildManifest,
"build manifest",
);
} catch {
failures.push("build manifest JSON Schema mismatch");
}
failures.push(
...(await verifyBuildManifestOutputs(buildManifest, { repositoryRoot })),
);
}
if (release && runtime) {
if (!runtime.BUILD_ID || !runtime.RELEASE_ID) {
failures.push("runtime release identity is missing");
} else {
const apiContractVersion =
release.schemaVersion === 1 && "API_CONTRACT_VERSION" in runtime
? runtime.API_CONTRACT_VERSION
: undefined;
if (release.schemaVersion === 1 && apiContractVersion === undefined) {
failures.push("runtime API contract identity is missing");
}
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,
});
failures.push(...coherence.mismatches.map((item) => `release:${item}`));
}
await verifyReleaseOutputs(
repositoryRoot,
release,
buildManifest,
failures,
);
}
if (
!storedRelease ||
!storedRelease.passed ||
!storedRelease.artifact.checked ||
!storedRelease.artifact.compatible ||
storedRelease.artifact.mismatches.length > 0 ||
storedRelease.fixtures.length === 0 ||
storedRelease.fixtures.some((fixture) => !fixture.passed) ||
storedRelease.artifact.releaseId !== release?.releaseId ||
storedRelease.generatedAt !== release?.builtAt
) {
failures.push("stored release verification is not a coherent PASS");
}
if (
!storedSupply ||
storedSupply.status !== "PASS" ||
storedSupply.failures.length > 0 ||
storedSupply.dependencyCount !== supplyReport.dependencyCount ||
storedSupply.lockfileSha256 !== supplyReport.lockfileSha256 ||
storedSupply.distSha256 !== supplyReport.distSha256 ||
storedSupply.sbomSha256 !== supplyReport.sbomSha256
) {
failures.push("stored supply-chain coherence is not a recomputed PASS");
}
await verifySecretScan(repositoryRoot, failures);
return Object.freeze({
status: failures.length === 0 ? "PASS" : "FAIL",
failures: Object.freeze([...new Set(failures)]),
});
}
async function validateSupportingArtifacts(
repositoryRoot: string,
failures: string[],
lockfileSha256: string,
): Promise<void> {
let actualOutputs: Awaited<ReturnType<typeof collectDistOutputs>> | null =
null;
try {
actualOutputs = await collectDistOutputs(repositoryRoot);
} catch {
failures.push("supporting evidence dist inputs are unreadable");
}
const bundle = await parseArtifact(
repositoryRoot,
"artifacts/performance/bundle.json",
bundlePerformanceArtifactSchema,
"bundle report",
failures,
);
if (
bundle &&
actualOutputs &&
JSON.stringify(bundle.outputs) !== JSON.stringify(actualOutputs)
) {
failures.push("bundle report does not describe current dist bytes");
}
if (actualOutputs) {
try {
failures.push(
...verifyStoredDistChecksums(
actualOutputs,
await readFile(
path.join(repositoryRoot, "artifacts/release/checksums.txt"),
"utf8",
),
),
);
} catch {
failures.push("stored dist checksums are missing or unreadable");
}
}
const inventory = await parseArtifact(
repositoryRoot,
"artifacts/release/dependency-inventory.json",
dependencyInventoryArtifactSchema,
"dependency inventory",
failures,
);
const dependencyDiff = await parseArtifact(
repositoryRoot,
"artifacts/security/dependency-diff.json",
dependencyDiffArtifactSchema,
"dependency diff",
failures,
);
if (inventory && dependencyDiff) {
try {
const recomputed = recomputeDependencyEvidence({
inventory,
baseline: await optionalReadJson(
repositoryRoot,
"config/security/dependency-baseline.json",
),
baselineApproval: await optionalReadJson(
repositoryRoot,
"config/security/dependency-baseline.approval.json",
),
dependencyChangeEvidence: await readJson(
repositoryRoot,
"config/security/dependency-change-evidence.json",
),
});
failures.push(
...compareStoredDependencyEvidence(recomputed, dependencyDiff),
);
} catch {
failures.push("dependency policy evidence is missing or invalid");
}
}
const license = await parseArtifact(
repositoryRoot,
"artifacts/security/license-report.json",
licenseReportArtifactSchema,
"license report",
failures,
);
if (inventory && license) {
try {
const recomputed = recomputeLicenseEvidence({
inventory,
policy: await readJson(
repositoryRoot,
"config/security/dependency-policy.json",
),
});
failures.push(...compareStoredLicenseEvidence(recomputed, license));
} catch {
failures.push("license policy evidence is missing or invalid");
}
}
const vulnerability = await parseArtifact(
repositoryRoot,
"artifacts/security/vulnerability-report.json",
vulnerabilityReportArtifactSchema,
"local vulnerability report",
failures,
);
if (vulnerability) {
failures.push(
...compareStoredLocalVulnerabilityReport(lockfileSha256, vulnerability),
);
}
const provenance = await parseArtifact(
repositoryRoot,
"artifacts/release/provenance.json",
provenanceArtifactSchema,
"local provenance",
failures,
);
if (
provenance?.predicate.runDetails.metadata.invocationId !== "LOCAL_UNSIGNED"
) {
failures.push("local provenance must remain LOCAL_UNSIGNED");
}
}
async function verifyReleaseOutputs(
repositoryRoot: string,
release: z.infer<typeof releaseManifestArtifactSchema>,
buildManifest: z.infer<typeof buildManifestArtifactSchema> | null,
failures: string[],
): Promise<void> {
let viteManifest: Record<string, unknown> = {};
try {
const raw = await readFile(
path.join(repositoryRoot, CANONICAL_VITE_MANIFEST_PATH),
"utf8",
);
viteManifest = asRecord(JSON.parse(raw), "Vite manifest");
if (createHash("sha256").update(raw).digest("hex") !== release.assetManifestHash) {
failures.push("release asset manifest hash mismatch");
}
} catch {
failures.push("Vite manifest is missing or invalid");
}
if (
buildManifest &&
(buildManifest.buildId !== release.buildId ||
buildManifest.commitSha !== release.commitSha ||
buildManifest.releaseId !== release.releaseId ||
buildManifest.generatedAt !== release.builtAt)
) {
failures.push("build/release identity mismatch");
}
const runtimeContracts: Readonly<Record<string, { moduleId: string }>> =
ROUTE_RUNTIME_CONTRACT;
for (const definition of Object.values(ROUTE_REGISTRY)) {
const runtime = runtimeContracts[definition.routeId];
const viteEntry = Object.values(viteManifest).find(
(entry) =>
isRecord(entry) &&
entry.name === runtime?.moduleId &&
entry.isDynamicEntry === true,
);
const file = isRecord(viteEntry) ? viteEntry.file : null;
if (
typeof file !== "string" ||
release.routeChunks[definition.chunkId] !== file ||
buildManifest?.outputs.routeChunks[definition.chunkId] !== file
) {
failures.push(`release route chunk mismatch: ${definition.chunkId}`);
}
}
}
async function verifySecretScan(
repositoryRoot: string,
failures: string[],
): Promise<void> {
try {
const evaluation = await evaluateRepositorySecretScan({ repositoryRoot });
failures.push(
...verifyStoredSecretScan(
evaluation,
await readJson(repositoryRoot, "artifacts/security/scan.sarif"),
),
);
} catch {
failures.push("secret scan SARIF is missing or invalid");
}
}
async function parseArtifact<T>(
repositoryRoot: string,
file: string,
schema: ZodType<T>,
label: string,
failures: string[],
): Promise<T | null> {
try {
return schema.parse(await readJson(repositoryRoot, file));
} catch {
failures.push(`${label} executable schema mismatch`);
return null;
}
}
async function readJson(repositoryRoot: string, file: string): Promise<unknown> {
return JSON.parse(await readFile(path.join(repositoryRoot, file), "utf8"));
}
async function optionalReadJson(
repositoryRoot: string,
file: string,
): Promise<unknown | null> {
try {
return await readJson(repositoryRoot, file);
} catch {
return null;
}
}
function asRecord(value: unknown, label: string): Record<string, unknown> {
if (!isRecord(value)) throw new TypeError(`${label} must be a JSON object`);
return value;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}