1331 lines
44 KiB
TypeScript
1331 lines
44 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,
|
|
localEvidenceAssessmentArtifactSchema,
|
|
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 {
|
|
LOCAL_EVIDENCE_ASSESSMENT_PATH,
|
|
LOCAL_EVIDENCE_POLICY_INPUT_PATHS,
|
|
LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS,
|
|
RELEASE_CANDIDATE_EVIDENCE_PATHS,
|
|
RELEASE_CANDIDATE_MANIFEST_PATH,
|
|
collectDistOutputs,
|
|
distSha256,
|
|
releaseCandidateManifestSchema,
|
|
type ReleaseCandidateManifest,
|
|
} 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 {
|
|
parseSecretScanPolicy,
|
|
secretScanSarifSchema,
|
|
evaluateRepositorySecretScan,
|
|
verifyStoredSecretScan,
|
|
} from "./secret-scan-evaluator.ts";
|
|
import { secretScanRules } from "./secret-scan.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 const LOCAL_EVIDENCE_VERIFIER_ID =
|
|
"tech-log-frontend/local-evidence-verifier";
|
|
export const LOCAL_EVIDENCE_VERIFIER_VERSION = "1";
|
|
export {
|
|
LOCAL_EVIDENCE_POLICY_INPUT_PATHS,
|
|
LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS,
|
|
} from "./release-candidate.ts";
|
|
|
|
export async function createLocalEvidenceAssessment(
|
|
repositoryRoot = process.cwd(),
|
|
): Promise<z.infer<typeof localEvidenceAssessmentArtifactSchema>> {
|
|
const root = path.resolve(repositoryRoot);
|
|
const outputs = await collectDistOutputs(root);
|
|
const evidencePaths = RELEASE_CANDIDATE_EVIDENCE_PATHS.filter(
|
|
(memberPath) => memberPath !== LOCAL_EVIDENCE_ASSESSMENT_PATH,
|
|
);
|
|
const evidenceInputs = (
|
|
await Promise.all([
|
|
...outputs.map(async ({ path: memberPath }) => digestInput(root, memberPath)),
|
|
...evidencePaths.map((memberPath) => digestInput(root, memberPath)),
|
|
])
|
|
).sort((left, right) => asciiCompare(left.path, right.path));
|
|
const lockfile = evidenceInputs.find(({ path: memberPath }) => memberPath === "pnpm-lock.yaml");
|
|
const sbom = evidenceInputs.find(
|
|
({ path: memberPath }) => memberPath === "artifacts/release/sbom.cdx.json",
|
|
);
|
|
if (!lockfile || !sbom) throw new Error("local assessment candidate inputs are incomplete");
|
|
const candidate: ReleaseCandidateManifest = {
|
|
schemaVersion: 1,
|
|
distSha256: distSha256(outputs),
|
|
lockfileSha256: lockfile.sha256,
|
|
bundleSha256: supplyChainDigest(evidenceInputs),
|
|
files: evidenceInputs,
|
|
};
|
|
const evaluated = await evaluateProducerLocalChecks(root, candidate);
|
|
const [build, release, provenance, supply, sbomDocument, policyInputs, secretScan] = 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, "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/release/sbom.cdx.json").then((value) =>
|
|
sbomArtifactSchema.parse(value),
|
|
),
|
|
Promise.all(
|
|
LOCAL_EVIDENCE_POLICY_INPUT_PATHS.map((policyPath) =>
|
|
digestInput(root, policyPath),
|
|
),
|
|
),
|
|
evaluateRepositorySecretScan({ repositoryRoot: root }),
|
|
]);
|
|
const identityFailures: string[] = [];
|
|
if (build.commitSha !== release.commitSha) {
|
|
identityFailures.push("producer build/release source revision mismatch");
|
|
}
|
|
if (
|
|
provenance.predicate.materials.sourceSetSha256 !== supply.sourceSetSha256
|
|
) {
|
|
identityFailures.push("producer provenance/supply source-set mismatch");
|
|
}
|
|
if (supply.distSha256 !== candidate.distSha256) {
|
|
identityFailures.push("producer supply/candidate dist digest mismatch");
|
|
}
|
|
if (supply.lockfileSha256 !== candidate.lockfileSha256) {
|
|
identityFailures.push("producer supply/candidate lockfile digest mismatch");
|
|
}
|
|
if (supply.sbomSha256 !== supplyChainDigest(sbomDocument)) {
|
|
identityFailures.push("producer supply/candidate SBOM digest mismatch");
|
|
}
|
|
const checks = {
|
|
...evaluated.checks,
|
|
...(identityFailures.some((failure) => failure.includes("build/release"))
|
|
? { release: "FAIL" as const }
|
|
: {}),
|
|
...(identityFailures.some((failure) => !failure.includes("build/release"))
|
|
? { supplyChain: "FAIL" as const }
|
|
: {}),
|
|
};
|
|
const failures = [...evaluated.failures, ...identityFailures];
|
|
const status = failures.length === 0 && Object.values(checks).every(
|
|
(check) => check === "PASS",
|
|
)
|
|
? ("PASS" as const)
|
|
: ("FAIL" as const);
|
|
const verifierSources = policyInputs.filter(({ path: policyPath }) =>
|
|
(LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS as readonly string[]).includes(policyPath),
|
|
);
|
|
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",
|
|
generatedAt: build.generatedAt,
|
|
status,
|
|
verifier: {
|
|
id: LOCAL_EVIDENCE_VERIFIER_ID,
|
|
version: LOCAL_EVIDENCE_VERIFIER_VERSION,
|
|
sourceSha256: supplyChainDigest(verifierSources),
|
|
},
|
|
source: {
|
|
revision: build.commitSha,
|
|
sourceSetSha256: supply.sourceSetSha256,
|
|
},
|
|
candidate: {
|
|
distSha256: candidate.distSha256,
|
|
lockfileSha256: candidate.lockfileSha256,
|
|
sbomSha256: sbom.sha256,
|
|
},
|
|
secretScan: {
|
|
policySha256: secretPolicy.sha256,
|
|
sarifSha256: secretSarif.sha256,
|
|
scanInputSha256: secretScan.scanInputSha256,
|
|
},
|
|
policyInputs,
|
|
evidenceInputs,
|
|
checks,
|
|
failures,
|
|
});
|
|
}
|
|
|
|
type LocalCheckName =
|
|
| "release"
|
|
| "supplyChain"
|
|
| "dependencyPolicy"
|
|
| "licensePolicy"
|
|
| "vulnerabilityPolicy"
|
|
| "secretScan";
|
|
|
|
async function evaluateProducerLocalChecks(
|
|
root: string,
|
|
candidate: ReleaseCandidateManifest,
|
|
options: Readonly<{ archived?: boolean }> = {},
|
|
): Promise<Readonly<{
|
|
checks: Readonly<Record<LocalCheckName, "PASS" | "FAIL">>;
|
|
failures: readonly string[];
|
|
}>> {
|
|
const checks: Record<LocalCheckName, "PASS" | "FAIL"> = {
|
|
release: "PASS",
|
|
supplyChain: "PASS",
|
|
dependencyPolicy: "PASS",
|
|
licensePolicy: "PASS",
|
|
vulnerabilityPolicy: "PASS",
|
|
secretScan: "PASS",
|
|
};
|
|
const failures: string[] = [];
|
|
const evaluate = async (
|
|
check: LocalCheckName,
|
|
operation: () => Promise<readonly string[]>,
|
|
): Promise<void> => {
|
|
try {
|
|
const diagnostics = await operation();
|
|
if (diagnostics.length > 0) {
|
|
checks[check] = "FAIL";
|
|
failures.push(...diagnostics.map((failure) => `${check}:${failure}`));
|
|
}
|
|
} catch (error) {
|
|
checks[check] = "FAIL";
|
|
failures.push(
|
|
`${check}:${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
}
|
|
};
|
|
|
|
await evaluate("release", async () => {
|
|
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),
|
|
),
|
|
]);
|
|
const diagnostics: string[] = [];
|
|
if (
|
|
build.commitSha !== release.commitSha ||
|
|
build.buildId !== release.buildId ||
|
|
build.releaseId !== release.releaseId ||
|
|
build.generatedAt !== release.builtAt
|
|
) {
|
|
diagnostics.push("build/release identity mismatch");
|
|
}
|
|
if (
|
|
!stored.passed ||
|
|
!stored.artifact.checked ||
|
|
!stored.artifact.compatible ||
|
|
stored.artifact.mismatches.length > 0 ||
|
|
stored.artifact.releaseId !== release.releaseId ||
|
|
stored.generatedAt !== release.builtAt ||
|
|
stored.fixtures.length === 0 ||
|
|
stored.fixtures.some((fixture) => !fixture.passed)
|
|
) {
|
|
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 [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.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 () => {
|
|
const [inventory, stored] = await Promise.all([
|
|
readJson(root, "artifacts/release/dependency-inventory.json").then((value) =>
|
|
dependencyInventoryArtifactSchema.parse(value),
|
|
),
|
|
readJson(root, "artifacts/security/dependency-diff.json").then((value) =>
|
|
dependencyDiffArtifactSchema.parse(value),
|
|
),
|
|
]);
|
|
const recomputed = recomputeDependencyEvidence({
|
|
inventory,
|
|
baseline: await optionalReadJson(root, "config/security/dependency-baseline.json"),
|
|
baselineApproval: await optionalReadJson(
|
|
root,
|
|
"config/security/dependency-baseline.approval.json",
|
|
),
|
|
dependencyChangeEvidence: await readJson(
|
|
root,
|
|
"config/security/dependency-change-evidence.json",
|
|
),
|
|
});
|
|
return compareStoredDependencyEvidence(recomputed, stored);
|
|
});
|
|
await evaluate("licensePolicy", async () => {
|
|
const [inventory, stored, policy] = await Promise.all([
|
|
readJson(root, "artifacts/release/dependency-inventory.json").then((value) =>
|
|
dependencyInventoryArtifactSchema.parse(value),
|
|
),
|
|
readJson(root, "artifacts/security/license-report.json").then((value) =>
|
|
licenseReportArtifactSchema.parse(value),
|
|
),
|
|
readJson(root, "config/security/dependency-policy.json"),
|
|
]);
|
|
return compareStoredLicenseEvidence(
|
|
recomputeLicenseEvidence({ inventory, policy }),
|
|
stored,
|
|
);
|
|
});
|
|
await evaluate("vulnerabilityPolicy", async () => {
|
|
const vulnerability = vulnerabilityReportArtifactSchema.parse(
|
|
await readJson(root, "artifacts/security/vulnerability-report.json"),
|
|
);
|
|
return compareStoredLocalVulnerabilityReport(
|
|
candidate.lockfileSha256,
|
|
vulnerability,
|
|
);
|
|
});
|
|
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,
|
|
await readJson(root, "artifacts/security/scan.sarif"),
|
|
);
|
|
});
|
|
return Object.freeze({ checks: Object.freeze(checks), failures: Object.freeze(failures) });
|
|
}
|
|
|
|
async function digestInput(
|
|
repositoryRoot: string,
|
|
memberPath: string,
|
|
): Promise<Readonly<{ path: string; bytes: number; sha256: string }>> {
|
|
const absolute = path.resolve(repositoryRoot, memberPath);
|
|
const relative = path.relative(repositoryRoot, absolute);
|
|
if (
|
|
relative === "" ||
|
|
relative === ".." ||
|
|
relative.startsWith(`..${path.sep}`) ||
|
|
path.isAbsolute(relative)
|
|
) {
|
|
throw new TypeError(`local assessment input escapes repository: ${memberPath}`);
|
|
}
|
|
const bytes = await readFile(absolute);
|
|
return Object.freeze({
|
|
path: memberPath,
|
|
bytes: bytes.byteLength,
|
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
});
|
|
}
|
|
|
|
function asciiCompare(left: string, right: string): number {
|
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
}
|
|
|
|
export async function verifyArchivedLocalEvidence(input: Readonly<{
|
|
extractionRoot: string;
|
|
expectedManifest: ReleaseCandidateManifest;
|
|
}>): Promise<Readonly<{
|
|
status: "PASS" | "FAIL";
|
|
identity: null | Readonly<{
|
|
sourceRevision: string;
|
|
sourceSetSha256: string;
|
|
assessmentSha256: string;
|
|
secretScan: Readonly<{
|
|
policySha256: string;
|
|
sarifSha256: string;
|
|
scanInputSha256: string;
|
|
}>;
|
|
}>;
|
|
failures: readonly string[];
|
|
}>> {
|
|
const extractionRoot = path.resolve(input.extractionRoot);
|
|
const failures: string[] = [];
|
|
let extractedManifest: ReleaseCandidateManifest | null = null;
|
|
try {
|
|
extractedManifest = releaseCandidateManifestSchema.parse(
|
|
await readJson(extractionRoot, RELEASE_CANDIDATE_MANIFEST_PATH),
|
|
);
|
|
} catch {
|
|
failures.push("extracted release candidate manifest is missing or invalid");
|
|
}
|
|
if (
|
|
extractedManifest &&
|
|
JSON.stringify(extractedManifest) !== JSON.stringify(input.expectedManifest)
|
|
) {
|
|
failures.push("caller expectedManifest differs from extracted manifest");
|
|
}
|
|
|
|
let assessment: z.infer<typeof localEvidenceAssessmentArtifactSchema> | null = null;
|
|
let assessmentSha256 = "";
|
|
let assessmentBytes: Buffer | null = null;
|
|
try {
|
|
assessmentBytes = await readFile(
|
|
path.join(extractionRoot, LOCAL_EVIDENCE_ASSESSMENT_PATH),
|
|
);
|
|
assessmentSha256 = createHash("sha256").update(assessmentBytes).digest("hex");
|
|
assessment = localEvidenceAssessmentArtifactSchema.parse(
|
|
JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(assessmentBytes)) as unknown,
|
|
);
|
|
} catch {
|
|
failures.push("local evidence assessment is missing or invalid");
|
|
}
|
|
|
|
if (assessment && extractedManifest) {
|
|
const assessmentMember = extractedManifest.files.find(
|
|
({ path: memberPath }) => memberPath === LOCAL_EVIDENCE_ASSESSMENT_PATH,
|
|
);
|
|
if (
|
|
!assessmentMember ||
|
|
assessmentMember.bytes !== assessmentBytes?.byteLength ||
|
|
assessmentMember.sha256 !== assessmentSha256
|
|
) {
|
|
failures.push("local assessment manifest binding mismatch");
|
|
}
|
|
if (
|
|
assessment.verifier.id !== LOCAL_EVIDENCE_VERIFIER_ID ||
|
|
assessment.verifier.version !== LOCAL_EVIDENCE_VERIFIER_VERSION
|
|
) {
|
|
failures.push("local assessment verifier identity mismatch");
|
|
}
|
|
const policyPaths = assessment.policyInputs.map(({ path: policyPath }) => policyPath);
|
|
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),
|
|
);
|
|
if (
|
|
verifierSources.length !== LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS.length ||
|
|
supplyChainDigest(verifierSources) !== assessment.verifier.sourceSha256
|
|
) {
|
|
failures.push("local assessment verifier-source digest mismatch");
|
|
}
|
|
|
|
const expectedEvidenceInputs = extractedManifest.files.filter(
|
|
({ path: memberPath }) => memberPath !== LOCAL_EVIDENCE_ASSESSMENT_PATH,
|
|
);
|
|
if (JSON.stringify(assessment.evidenceInputs) !== JSON.stringify(expectedEvidenceInputs)) {
|
|
failures.push("local assessment evidenceInputs exact member binding mismatch");
|
|
}
|
|
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 ||
|
|
!sbom ||
|
|
assessment.candidate.sbomSha256 !== sbom.sha256
|
|
) {
|
|
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 ||
|
|
identities.releaseRevision !== assessment.source.revision
|
|
) {
|
|
failures.push("local assessment source revision identity mismatch");
|
|
}
|
|
if (
|
|
identities.provenanceSourceSetSha256 !== assessment.source.sourceSetSha256 ||
|
|
identities.supplySourceSetSha256 !== assessment.source.sourceSetSha256
|
|
) {
|
|
failures.push("local assessment source-set identity mismatch");
|
|
}
|
|
}
|
|
|
|
const uniqueFailures = Object.freeze([...new Set(failures)]);
|
|
const passingAssessment = uniqueFailures.length === 0 ? assessment : null;
|
|
return Object.freeze({
|
|
status: passingAssessment ? "PASS" : "FAIL",
|
|
identity: passingAssessment
|
|
? Object.freeze({
|
|
sourceRevision: passingAssessment.source.revision,
|
|
sourceSetSha256: passingAssessment.source.sourceSetSha256,
|
|
assessmentSha256,
|
|
secretScan: passingAssessment.secretScan,
|
|
})
|
|
: null,
|
|
failures: uniqueFailures,
|
|
});
|
|
}
|
|
|
|
async function readArchivedIdentities(
|
|
extractionRoot: string,
|
|
failures: string[],
|
|
): Promise<Readonly<{
|
|
buildRevision: unknown;
|
|
releaseRevision: unknown;
|
|
provenanceSourceSetSha256: unknown;
|
|
supplySourceSetSha256: unknown;
|
|
}>> {
|
|
try {
|
|
const [buildDocument, releaseDocument, provenanceDocument, supplyDocument] = await Promise.all([
|
|
readJson(extractionRoot, "artifacts/release/build-manifest.json"),
|
|
readJson(extractionRoot, "dist/release-manifest.json"),
|
|
readJson(extractionRoot, "artifacts/release/provenance.json"),
|
|
readJson(extractionRoot, "artifacts/security/supply-chain-verification.json"),
|
|
]);
|
|
const build = buildManifestArtifactSchema.parse(buildDocument);
|
|
const release = releaseManifestArtifactSchema.parse(releaseDocument);
|
|
const provenance = provenanceArtifactSchema.parse(provenanceDocument);
|
|
const supply = supplyChainVerificationArtifactSchema.parse(supplyDocument);
|
|
return Object.freeze({
|
|
buildRevision: build.commitSha,
|
|
releaseRevision: release.commitSha,
|
|
provenanceSourceSetSha256: provenance.predicate.materials.sourceSetSha256,
|
|
supplySourceSetSha256: supply.sourceSetSha256,
|
|
});
|
|
} catch {
|
|
failures.push("archived source/build/provenance identities are missing or invalid");
|
|
return Object.freeze({
|
|
buildRevision: null,
|
|
releaseRevision: null,
|
|
provenanceSourceSetSha256: null,
|
|
supplySourceSetSha256: null,
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function assessLocalEvidenceForProducer(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);
|
|
}
|