fix: enforce exact local evidence defaults
This commit is contained in:
@@ -25,7 +25,9 @@ import {
|
||||
verifySupplyChainCoherence,
|
||||
} from "./lib/supply-chain.ts";
|
||||
import {
|
||||
createLocalVulnerabilityReport,
|
||||
distChecksumsText,
|
||||
LOCAL_SUPPLY_CHAIN_UNVERIFIED_DEFAULTS,
|
||||
recomputeDependencyEvidence,
|
||||
recomputeLicenseEvidence,
|
||||
} from "./lib/local-policy-evidence.ts";
|
||||
@@ -186,16 +188,9 @@ const licenseEvidence = recomputeLicenseEvidence({
|
||||
policy: licensePolicy,
|
||||
});
|
||||
|
||||
const vulnerabilityReport = {
|
||||
schemaVersion: 1,
|
||||
provider: "UNCONFIGURED",
|
||||
scannedLockfileSha256: inventory.lockfileSha256,
|
||||
status: "FAIL_UNVERIFIED",
|
||||
findings: [],
|
||||
exceptionsApplied: [],
|
||||
failures: ["external vulnerability provider report is missing"],
|
||||
blocking: [],
|
||||
};
|
||||
const vulnerabilityReport = createLocalVulnerabilityReport(
|
||||
inventory.lockfileSha256,
|
||||
);
|
||||
|
||||
const sourceFiles = [...repositoryInventory.trackedFiles];
|
||||
const sourceSetSha256 = await digestReleaseInputFiles(sourceFiles);
|
||||
@@ -301,15 +296,13 @@ const localPassed = localFailures.length === 0;
|
||||
const verification = {
|
||||
schemaVersion: 1,
|
||||
localStatus: localPassed ? "PASS" : "FAIL",
|
||||
promotionStatus: "FAIL_UNVERIFIED",
|
||||
...LOCAL_SUPPLY_CHAIN_UNVERIFIED_DEFAULTS,
|
||||
lockfileSha256: inventory.lockfileSha256,
|
||||
sourceSetSha256,
|
||||
distSha256: distDigest,
|
||||
sbomSha256: supplyChainDigest(sbom),
|
||||
dependencyDiff: dependencyPolicy.dependencyDiff,
|
||||
highRiskReview: dependencyPolicy.highRisk,
|
||||
vulnerabilityStatus: vulnerabilityReport.status,
|
||||
provenanceAttestationStatus: "FAIL_UNVERIFIED",
|
||||
failures: localFailures,
|
||||
};
|
||||
const bundleReport = {
|
||||
|
||||
@@ -2,6 +2,8 @@ import {
|
||||
dependencyDiffArtifactSchema,
|
||||
dependencyInventoryArtifactSchema,
|
||||
licenseReportArtifactSchema,
|
||||
supplyChainVerificationArtifactSchema,
|
||||
vulnerabilityReportArtifactSchema,
|
||||
} from "../contracts/release-artifacts.ts";
|
||||
import type { DistOutput } from "./release-candidate.ts";
|
||||
import {
|
||||
@@ -13,6 +15,60 @@ import {
|
||||
|
||||
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<{
|
||||
inventory: unknown;
|
||||
baseline: unknown;
|
||||
|
||||
@@ -35,8 +35,10 @@ import { digestReleaseInputFiles } from "./release-input-evidence.ts";
|
||||
import {
|
||||
compareStoredDependencyEvidence,
|
||||
compareStoredLicenseEvidence,
|
||||
compareStoredLocalVulnerabilityReport,
|
||||
recomputeDependencyEvidence,
|
||||
recomputeLicenseEvidence,
|
||||
verifyLocalSupplyChainDefaults,
|
||||
verifyStoredDistChecksums,
|
||||
} from "./local-policy-evidence.ts";
|
||||
import {
|
||||
@@ -175,6 +177,9 @@ export async function verifyLocalSupplyChainEvidence(
|
||||
) {
|
||||
failures.push("verification digest/status set is incoherent");
|
||||
}
|
||||
if (verification) {
|
||||
failures.push(...verifyLocalSupplyChainDefaults(verification));
|
||||
}
|
||||
if (inventory && sbom && provenance && verification) {
|
||||
try {
|
||||
const policy = parseRepositoryFileInventoryPolicy(
|
||||
@@ -336,7 +341,11 @@ export async function verifyArchivedLocalEvidence(input: Readonly<{
|
||||
failures,
|
||||
);
|
||||
|
||||
await validateSupportingArtifacts(repositoryRoot, failures);
|
||||
await validateSupportingArtifacts(
|
||||
repositoryRoot,
|
||||
failures,
|
||||
supplyReport.lockfileSha256,
|
||||
);
|
||||
if (buildManifest) {
|
||||
try {
|
||||
assertMatchesJsonSchema(
|
||||
@@ -423,6 +432,7 @@ export async function verifyArchivedLocalEvidence(input: Readonly<{
|
||||
async function validateSupportingArtifacts(
|
||||
repositoryRoot: string,
|
||||
failures: string[],
|
||||
lockfileSha256: string,
|
||||
): Promise<void> {
|
||||
let actualOutputs: Awaited<ReturnType<typeof collectDistOutputs>> | null =
|
||||
null;
|
||||
@@ -526,12 +536,10 @@ async function validateSupportingArtifacts(
|
||||
"local vulnerability report",
|
||||
failures,
|
||||
);
|
||||
if (
|
||||
vulnerability &&
|
||||
(vulnerability.status !== "FAIL_UNVERIFIED" ||
|
||||
vulnerability.provider !== "UNCONFIGURED")
|
||||
) {
|
||||
failures.push("local vulnerability report may not satisfy promotion");
|
||||
if (vulnerability) {
|
||||
failures.push(
|
||||
...compareStoredLocalVulnerabilityReport(lockfileSha256, vulnerability),
|
||||
);
|
||||
}
|
||||
const provenance = await parseArtifact(
|
||||
repositoryRoot,
|
||||
|
||||
@@ -2,12 +2,18 @@ import { readFile } from "node:fs/promises";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
supplyChainVerificationArtifactSchema,
|
||||
vulnerabilityReportArtifactSchema,
|
||||
} from "../../scripts/contracts/release-artifacts.ts";
|
||||
import {
|
||||
compareStoredDependencyEvidence,
|
||||
compareStoredLicenseEvidence,
|
||||
compareStoredLocalVulnerabilityReport,
|
||||
distChecksumsText,
|
||||
recomputeDependencyEvidence,
|
||||
recomputeLicenseEvidence,
|
||||
verifyLocalSupplyChainDefaults,
|
||||
verifyStoredDistChecksums,
|
||||
} from "../../scripts/lib/local-policy-evidence.ts";
|
||||
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",
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user