fix: recompute local promotion evidence

This commit is contained in:
DongHyeonka
2026-08-02 07:02:39 +09:00
parent 92e5cace5c
commit 1b4b0c2821
8 changed files with 837 additions and 253 deletions
+151 -31
View File
@@ -32,10 +32,21 @@ 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,
recomputeDependencyEvidence,
recomputeLicenseEvidence,
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,
@@ -137,15 +148,15 @@ export async function verifyLocalSupplyChainEvidence(
}
const sbomSha256 = sbom ? supplyChainDigest(sbom) : "0".repeat(64);
let coherenceFailures: readonly string[] = Object.freeze([]);
if (inventory && sbom && provenance) {
failures.push(
...verifySupplyChainCoherence(
sbom,
inventory,
provenance,
distDigest,
).failures,
);
coherenceFailures = verifySupplyChainCoherence(
sbom,
inventory,
provenance,
distDigest,
).failures;
failures.push(...coherenceFailures);
}
if (
!inventory ||
@@ -193,6 +204,53 @@ export async function verifyLocalSupplyChainEvidence(
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 ?? [];
@@ -366,6 +424,13 @@ async function validateSupportingArtifacts(
repositoryRoot: string,
failures: 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",
@@ -373,16 +438,35 @@ async function validateSupportingArtifacts(
"bundle report",
failures,
);
if (bundle) {
if (
bundle &&
actualOutputs &&
JSON.stringify(bundle.outputs) !== JSON.stringify(actualOutputs)
) {
failures.push("bundle report does not describe current dist bytes");
}
if (actualOutputs) {
try {
const actual = await collectDistOutputs(repositoryRoot);
if (JSON.stringify(bundle.outputs) !== JSON.stringify(actual)) {
failures.push("bundle report does not describe current dist bytes");
}
failures.push(
...verifyStoredDistChecksums(
actualOutputs,
await readFile(
path.join(repositoryRoot, "artifacts/release/checksums.txt"),
"utf8",
),
),
);
} catch {
failures.push("bundle report dist inputs are unreadable");
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",
@@ -390,8 +474,29 @@ async function validateSupportingArtifacts(
"dependency diff",
failures,
);
if (dependencyDiff && dependencyDiff.reviewFailures.length > 0) {
failures.push("dependency review evidence is not PASS");
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,
@@ -400,8 +505,19 @@ async function validateSupportingArtifacts(
"license report",
failures,
);
if (license && (license.status !== "PASS" || license.failures.length > 0)) {
failures.push("license report is not PASS");
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,
@@ -485,20 +601,13 @@ async function verifySecretScan(
failures: string[],
): Promise<void> {
try {
const sarif = asRecord(
await readJson(repositoryRoot, "artifacts/security/scan.sarif"),
"secret scan SARIF",
const evaluation = await evaluateRepositorySecretScan({ repositoryRoot });
failures.push(
...verifyStoredSecretScan(
evaluation,
await readJson(repositoryRoot, "artifacts/security/scan.sarif"),
),
);
const runs = Array.isArray(sarif.runs) ? sarif.runs : [];
if (
sarif.version !== "2.1.0" ||
runs.length !== 1 ||
!isRecord(runs[0]) ||
!Array.isArray(runs[0].results) ||
runs[0].results.length !== 0
) {
failures.push("secret scan SARIF is not an empty PASS");
}
} catch {
failures.push("secret scan SARIF is missing or invalid");
}
@@ -523,6 +632,17 @@ 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;