fix: recompute local promotion evidence
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user