fix: enforce exact local evidence defaults
This commit is contained in:
@@ -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 = {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user