207 lines
6.2 KiB
TypeScript
207 lines
6.2 KiB
TypeScript
import {
|
|
dependencyDiffArtifactSchema,
|
|
dependencyInventoryArtifactSchema,
|
|
licenseReportArtifactSchema,
|
|
supplyChainVerificationArtifactSchema,
|
|
vulnerabilityReportArtifactSchema,
|
|
} from "../contracts/release-artifacts.ts";
|
|
import type { DistOutput } from "./release-candidate.ts";
|
|
import {
|
|
diffDependencyInventories,
|
|
supplyChainDigest,
|
|
validateDependencyReview,
|
|
validateLicensePolicy,
|
|
} from "./supply-chain.ts";
|
|
|
|
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;
|
|
baselineApproval: unknown;
|
|
dependencyChangeEvidence: unknown;
|
|
skipBaseline?: boolean;
|
|
}>) {
|
|
const inventory = dependencyInventoryArtifactSchema.parse(input.inventory);
|
|
const baseline = asDocument(input.baseline);
|
|
const approval = asDocument(input.baselineApproval);
|
|
const dependencyChangeEvidence = asDocument(input.dependencyChangeEvidence);
|
|
const failures: string[] = [];
|
|
const skipBaseline = input.skipBaseline === true;
|
|
let diff: ReturnType<typeof diffDependencyInventories> = Object.freeze({
|
|
added: Object.freeze([]),
|
|
removed: Object.freeze([]),
|
|
changed: Object.freeze([]),
|
|
upgrades: Object.freeze([]),
|
|
});
|
|
let review: ReturnType<typeof validateDependencyReview> = Object.freeze({
|
|
passed: skipBaseline,
|
|
highRisk: Object.freeze([]),
|
|
failures: Object.freeze(
|
|
skipBaseline ? [] : ["dependency baseline unavailable"],
|
|
),
|
|
});
|
|
const baselineDigest = baseline ? supplyChainDigest(baseline) : null;
|
|
|
|
if (baseline && approval) {
|
|
diff = diffDependencyInventories(baseline, inventory);
|
|
review = validateDependencyReview(
|
|
diff,
|
|
inventory,
|
|
dependencyChangeEvidence ?? {},
|
|
);
|
|
if (
|
|
approval.schemaVersion !== 1 ||
|
|
approval.snapshotDigest !== baselineDigest ||
|
|
typeof approval.owner !== "string" ||
|
|
approval.owner.trim().length === 0
|
|
) {
|
|
failures.push("dependency baseline approval digest mismatch");
|
|
}
|
|
} else if (!skipBaseline) {
|
|
failures.push("dependency baseline and approval are required");
|
|
}
|
|
failures.push(...review.failures);
|
|
|
|
const report = dependencyDiffArtifactSchema.parse({
|
|
schemaVersion: 2,
|
|
baselineDigest,
|
|
currentDigest: supplyChainDigest(inventory),
|
|
...diff,
|
|
highRisk: review.highRisk,
|
|
reviewFailures: review.failures,
|
|
});
|
|
return Object.freeze({
|
|
report,
|
|
dependencyDiff: diff,
|
|
highRisk: review.highRisk,
|
|
failures: Object.freeze(failures),
|
|
});
|
|
}
|
|
|
|
export function compareStoredDependencyEvidence(
|
|
recomputed: ReturnType<typeof recomputeDependencyEvidence>,
|
|
stored: unknown,
|
|
): string[] {
|
|
const parsed = dependencyDiffArtifactSchema.safeParse(stored);
|
|
if (
|
|
!parsed.success ||
|
|
supplyChainDigest(parsed.data) !== supplyChainDigest(recomputed.report)
|
|
) {
|
|
return ["stored dependency diff does not match recomputed policy evidence"];
|
|
}
|
|
return [...recomputed.failures];
|
|
}
|
|
|
|
export function recomputeLicenseEvidence(input: Readonly<{
|
|
inventory: unknown;
|
|
policy: unknown;
|
|
}>) {
|
|
const inventory = dependencyInventoryArtifactSchema.parse(input.inventory);
|
|
const policy = asDocument(input.policy) ?? {};
|
|
const result = validateLicensePolicy(inventory, policy);
|
|
const report = licenseReportArtifactSchema.parse({
|
|
schemaVersion: 1,
|
|
status: result.passed ? "PASS" : "FAIL",
|
|
dependencyCount: inventory.dependencyCount,
|
|
results: result.results,
|
|
failures: result.failures,
|
|
});
|
|
return Object.freeze({
|
|
report,
|
|
failures: result.failures,
|
|
});
|
|
}
|
|
|
|
export function compareStoredLicenseEvidence(
|
|
recomputed: ReturnType<typeof recomputeLicenseEvidence>,
|
|
stored: unknown,
|
|
): string[] {
|
|
const parsed = licenseReportArtifactSchema.safeParse(stored);
|
|
if (
|
|
!parsed.success ||
|
|
supplyChainDigest(parsed.data) !== supplyChainDigest(recomputed.report)
|
|
) {
|
|
return ["stored license report does not match recomputed policy evidence"];
|
|
}
|
|
return [...recomputed.failures];
|
|
}
|
|
|
|
export function distChecksumsText(outputs: readonly DistOutput[]): string {
|
|
return `${[...outputs]
|
|
.sort((left, right) => left.path.localeCompare(right.path))
|
|
.map((output) => `${output.sha256} ${output.path}`)
|
|
.join("\n")}\n`;
|
|
}
|
|
|
|
export function verifyStoredDistChecksums(
|
|
outputs: readonly DistOutput[],
|
|
stored: string,
|
|
): string[] {
|
|
return stored === distChecksumsText(outputs)
|
|
? []
|
|
: ["stored dist checksums do not match current outputs"];
|
|
}
|
|
|
|
function asDocument(value: unknown): Document | null {
|
|
return value !== null &&
|
|
typeof value === "object" &&
|
|
!Array.isArray(value)
|
|
? (value as Document)
|
|
: null;
|
|
}
|