fix: enforce exact local evidence defaults

This commit is contained in:
DongHyeonka
2026-08-02 07:11:33 +09:00
parent 1b4b0c2821
commit f487823442
4 changed files with 153 additions and 20 deletions
+6 -13
View File
@@ -25,7 +25,9 @@ import {
verifySupplyChainCoherence, verifySupplyChainCoherence,
} from "./lib/supply-chain.ts"; } from "./lib/supply-chain.ts";
import { import {
createLocalVulnerabilityReport,
distChecksumsText, distChecksumsText,
LOCAL_SUPPLY_CHAIN_UNVERIFIED_DEFAULTS,
recomputeDependencyEvidence, recomputeDependencyEvidence,
recomputeLicenseEvidence, recomputeLicenseEvidence,
} from "./lib/local-policy-evidence.ts"; } from "./lib/local-policy-evidence.ts";
@@ -186,16 +188,9 @@ const licenseEvidence = recomputeLicenseEvidence({
policy: licensePolicy, policy: licensePolicy,
}); });
const vulnerabilityReport = { const vulnerabilityReport = createLocalVulnerabilityReport(
schemaVersion: 1, inventory.lockfileSha256,
provider: "UNCONFIGURED", );
scannedLockfileSha256: inventory.lockfileSha256,
status: "FAIL_UNVERIFIED",
findings: [],
exceptionsApplied: [],
failures: ["external vulnerability provider report is missing"],
blocking: [],
};
const sourceFiles = [...repositoryInventory.trackedFiles]; const sourceFiles = [...repositoryInventory.trackedFiles];
const sourceSetSha256 = await digestReleaseInputFiles(sourceFiles); const sourceSetSha256 = await digestReleaseInputFiles(sourceFiles);
@@ -301,15 +296,13 @@ const localPassed = localFailures.length === 0;
const verification = { const verification = {
schemaVersion: 1, schemaVersion: 1,
localStatus: localPassed ? "PASS" : "FAIL", localStatus: localPassed ? "PASS" : "FAIL",
promotionStatus: "FAIL_UNVERIFIED", ...LOCAL_SUPPLY_CHAIN_UNVERIFIED_DEFAULTS,
lockfileSha256: inventory.lockfileSha256, lockfileSha256: inventory.lockfileSha256,
sourceSetSha256, sourceSetSha256,
distSha256: distDigest, distSha256: distDigest,
sbomSha256: supplyChainDigest(sbom), sbomSha256: supplyChainDigest(sbom),
dependencyDiff: dependencyPolicy.dependencyDiff, dependencyDiff: dependencyPolicy.dependencyDiff,
highRiskReview: dependencyPolicy.highRisk, highRiskReview: dependencyPolicy.highRisk,
vulnerabilityStatus: vulnerabilityReport.status,
provenanceAttestationStatus: "FAIL_UNVERIFIED",
failures: localFailures, failures: localFailures,
}; };
const bundleReport = { const bundleReport = {
+56
View File
@@ -2,6 +2,8 @@ import {
dependencyDiffArtifactSchema, dependencyDiffArtifactSchema,
dependencyInventoryArtifactSchema, dependencyInventoryArtifactSchema,
licenseReportArtifactSchema, licenseReportArtifactSchema,
supplyChainVerificationArtifactSchema,
vulnerabilityReportArtifactSchema,
} from "../contracts/release-artifacts.ts"; } from "../contracts/release-artifacts.ts";
import type { DistOutput } from "./release-candidate.ts"; import type { DistOutput } from "./release-candidate.ts";
import { import {
@@ -13,6 +15,60 @@ import {
type Document = Record<string, unknown>; type Document = Record<string, unknown>;
export const LOCAL_SUPPLY_CHAIN_UNVERIFIED_DEFAULTS = Object.freeze({
promotionStatus: "FAIL_UNVERIFIED" as const,
vulnerabilityStatus: "FAIL_UNVERIFIED" as const,
provenanceAttestationStatus: "FAIL_UNVERIFIED" as const,
});
export function verifyLocalSupplyChainDefaults(stored: unknown): string[] {
const parsed = supplyChainVerificationArtifactSchema.safeParse(stored);
if (
!parsed.success ||
parsed.data.promotionStatus !==
LOCAL_SUPPLY_CHAIN_UNVERIFIED_DEFAULTS.promotionStatus ||
parsed.data.vulnerabilityStatus !==
LOCAL_SUPPLY_CHAIN_UNVERIFIED_DEFAULTS.vulnerabilityStatus ||
parsed.data.provenanceAttestationStatus !==
LOCAL_SUPPLY_CHAIN_UNVERIFIED_DEFAULTS.provenanceAttestationStatus
) {
return [
"supply-chain verification provider defaults are not local FAIL_UNVERIFIED",
];
}
return [];
}
export function createLocalVulnerabilityReport(lockfileSha256: string) {
return vulnerabilityReportArtifactSchema.parse({
schemaVersion: 1,
provider: "UNCONFIGURED",
scannedLockfileSha256: lockfileSha256,
status: "FAIL_UNVERIFIED",
findings: [],
exceptionsApplied: [],
failures: ["external vulnerability provider report is missing"],
blocking: [],
});
}
export function compareStoredLocalVulnerabilityReport(
lockfileSha256: string,
stored: unknown,
): string[] {
const parsed = vulnerabilityReportArtifactSchema.safeParse(stored);
if (
!parsed.success ||
supplyChainDigest(parsed.data) !==
supplyChainDigest(createLocalVulnerabilityReport(lockfileSha256))
) {
return [
"local vulnerability report does not match exact unconfigured defaults",
];
}
return [];
}
export function recomputeDependencyEvidence(input: Readonly<{ export function recomputeDependencyEvidence(input: Readonly<{
inventory: unknown; inventory: unknown;
baseline: unknown; baseline: unknown;
+15 -7
View File
@@ -35,8 +35,10 @@ import { digestReleaseInputFiles } from "./release-input-evidence.ts";
import { import {
compareStoredDependencyEvidence, compareStoredDependencyEvidence,
compareStoredLicenseEvidence, compareStoredLicenseEvidence,
compareStoredLocalVulnerabilityReport,
recomputeDependencyEvidence, recomputeDependencyEvidence,
recomputeLicenseEvidence, recomputeLicenseEvidence,
verifyLocalSupplyChainDefaults,
verifyStoredDistChecksums, verifyStoredDistChecksums,
} from "./local-policy-evidence.ts"; } from "./local-policy-evidence.ts";
import { import {
@@ -175,6 +177,9 @@ export async function verifyLocalSupplyChainEvidence(
) { ) {
failures.push("verification digest/status set is incoherent"); failures.push("verification digest/status set is incoherent");
} }
if (verification) {
failures.push(...verifyLocalSupplyChainDefaults(verification));
}
if (inventory && sbom && provenance && verification) { if (inventory && sbom && provenance && verification) {
try { try {
const policy = parseRepositoryFileInventoryPolicy( const policy = parseRepositoryFileInventoryPolicy(
@@ -336,7 +341,11 @@ export async function verifyArchivedLocalEvidence(input: Readonly<{
failures, failures,
); );
await validateSupportingArtifacts(repositoryRoot, failures); await validateSupportingArtifacts(
repositoryRoot,
failures,
supplyReport.lockfileSha256,
);
if (buildManifest) { if (buildManifest) {
try { try {
assertMatchesJsonSchema( assertMatchesJsonSchema(
@@ -423,6 +432,7 @@ export async function verifyArchivedLocalEvidence(input: Readonly<{
async function validateSupportingArtifacts( async function validateSupportingArtifacts(
repositoryRoot: string, repositoryRoot: string,
failures: string[], failures: string[],
lockfileSha256: string,
): Promise<void> { ): Promise<void> {
let actualOutputs: Awaited<ReturnType<typeof collectDistOutputs>> | null = let actualOutputs: Awaited<ReturnType<typeof collectDistOutputs>> | null =
null; null;
@@ -526,12 +536,10 @@ async function validateSupportingArtifacts(
"local vulnerability report", "local vulnerability report",
failures, failures,
); );
if ( if (vulnerability) {
vulnerability && failures.push(
(vulnerability.status !== "FAIL_UNVERIFIED" || ...compareStoredLocalVulnerabilityReport(lockfileSha256, vulnerability),
vulnerability.provider !== "UNCONFIGURED") );
) {
failures.push("local vulnerability report may not satisfy promotion");
} }
const provenance = await parseArtifact( const provenance = await parseArtifact(
repositoryRoot, repositoryRoot,
@@ -2,12 +2,18 @@ import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import {
supplyChainVerificationArtifactSchema,
vulnerabilityReportArtifactSchema,
} from "../../scripts/contracts/release-artifacts.ts";
import { import {
compareStoredDependencyEvidence, compareStoredDependencyEvidence,
compareStoredLicenseEvidence, compareStoredLicenseEvidence,
compareStoredLocalVulnerabilityReport,
distChecksumsText, distChecksumsText,
recomputeDependencyEvidence, recomputeDependencyEvidence,
recomputeLicenseEvidence, recomputeLicenseEvidence,
verifyLocalSupplyChainDefaults,
verifyStoredDistChecksums, verifyStoredDistChecksums,
} from "../../scripts/lib/local-policy-evidence.ts"; } from "../../scripts/lib/local-policy-evidence.ts";
import { import {
@@ -173,4 +179,74 @@ describe("recomputed local promotion evidence", () => {
]), ]),
); );
}); });
it("rejects schema-valid verified provider statuses in local supply-chain evidence", () => {
const localVerification = {
schemaVersion: 1 as const,
localStatus: "PASS" as const,
promotionStatus: "FAIL_UNVERIFIED" as const,
lockfileSha256: "1".repeat(64),
sourceSetSha256: "2".repeat(64),
distSha256: "3".repeat(64),
sbomSha256: "4".repeat(64),
dependencyDiff: {
added: [],
removed: [],
changed: [],
upgrades: [],
},
highRiskReview: [],
vulnerabilityStatus: "FAIL_UNVERIFIED" as const,
provenanceAttestationStatus: "FAIL_UNVERIFIED" as const,
failures: [],
};
const tampered = [
{ ...localVerification, promotionStatus: "PASS" as const },
{ ...localVerification, vulnerabilityStatus: "PASS" as const },
{
...localVerification,
provenanceAttestationStatus: "PASS" as const,
},
].map((value) => supplyChainVerificationArtifactSchema.parse(value));
for (const value of tampered) {
expect(verifyLocalSupplyChainDefaults(value)).toEqual([
"supply-chain verification provider defaults are not local FAIL_UNVERIFIED",
]);
}
});
it("rejects every schema-valid drift from the exact local vulnerability report", () => {
const currentLockfileSha256 = "1".repeat(64);
const localReport = {
schemaVersion: 1 as const,
provider: "UNCONFIGURED",
scannedLockfileSha256: currentLockfileSha256,
status: "FAIL_UNVERIFIED" as const,
findings: [],
exceptionsApplied: [],
failures: ["external vulnerability provider report is missing"],
blocking: [],
};
const tampered = [
{ ...localReport, provider: "forged-provider" },
{ ...localReport, scannedLockfileSha256: "2".repeat(64) },
{ ...localReport, status: "PASS" as const },
{ ...localReport, findings: [{ id: "forged" }] },
{ ...localReport, exceptionsApplied: [{ id: "forged" }] },
{ ...localReport, failures: [] },
{ ...localReport, blocking: ["forged"] },
].map((value) => vulnerabilityReportArtifactSchema.parse(value));
for (const value of tampered) {
expect(
compareStoredLocalVulnerabilityReport(
currentLockfileSha256,
value,
),
).toEqual([
"local vulnerability report does not match exact unconfigured defaults",
]);
}
});
}); });