129 lines
3.6 KiB
TypeScript
129 lines
3.6 KiB
TypeScript
import { createPublicKey } from "node:crypto";
|
|
import { readFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
|
|
import {
|
|
evaluatePromotionEvidence,
|
|
type ProviderTrust,
|
|
} from "./provider-evidence.ts";
|
|
import {
|
|
RELEASE_CANDIDATE_MANIFEST_PATH,
|
|
releaseCandidateManifestSchema,
|
|
verifyReleaseCandidate,
|
|
} from "./release-candidate.ts";
|
|
import { verifyArchivedLocalEvidence } from "./local-release-evidence.ts";
|
|
|
|
type LocalEvidenceVerifier = typeof verifyArchivedLocalEvidence;
|
|
|
|
export type VerifyPromotionInputsOptions = Readonly<{
|
|
environment?: NodeJS.ProcessEnv;
|
|
repositoryRoot?: string;
|
|
verifyLocalEvidence?: LocalEvidenceVerifier;
|
|
}>;
|
|
|
|
export async function verifyPromotionInputs(
|
|
options: VerifyPromotionInputsOptions = {},
|
|
) {
|
|
const environment = options.environment ?? process.env;
|
|
const repositoryRoot = path.resolve(options.repositoryRoot ?? process.cwd());
|
|
const manifestDocument = await requiredJson(
|
|
repositoryRoot,
|
|
RELEASE_CANDIDATE_MANIFEST_PATH,
|
|
);
|
|
const manifest = releaseCandidateManifestSchema.parse(manifestDocument);
|
|
const candidate = await verifyReleaseCandidate(
|
|
manifestDocument,
|
|
repositoryRoot,
|
|
);
|
|
const localEvidence = await (
|
|
options.verifyLocalEvidence ?? verifyArchivedLocalEvidence
|
|
)({ repositoryRoot, candidate: manifest });
|
|
const vulnerabilityReport = await optionalJson(
|
|
repositoryRoot,
|
|
environment.VULNERABILITY_REPORT_PATH,
|
|
);
|
|
const provenanceAttestation = await optionalJson(
|
|
repositoryRoot,
|
|
environment.PROVENANCE_ATTESTATION_PATH,
|
|
);
|
|
const result = evaluatePromotionEvidence({
|
|
candidate: manifest,
|
|
currentDistSha256: candidate.currentDistSha256 ?? "",
|
|
localStatus: localEvidence.status,
|
|
vulnerabilityReport,
|
|
provenanceAttestation,
|
|
vulnerabilityTrust: await readTrust(
|
|
repositoryRoot,
|
|
environment.VULNERABILITY_PUBLIC_KEY_PATH,
|
|
environment.VULNERABILITY_KEY_ID,
|
|
),
|
|
provenanceTrust: await readTrust(
|
|
repositoryRoot,
|
|
environment.PROVENANCE_PUBLIC_KEY_PATH,
|
|
environment.PROVENANCE_KEY_ID,
|
|
),
|
|
});
|
|
const failures = [
|
|
...candidate.failures,
|
|
...localEvidence.failures,
|
|
...result.failures,
|
|
];
|
|
return Object.freeze({
|
|
schemaVersion: 1 as const,
|
|
status:
|
|
failures.length === 0 && result.status === "PASS"
|
|
? ("PASS" as const)
|
|
: ("FAIL_UNVERIFIED" as const),
|
|
vulnerabilityStatus: result.vulnerabilityStatus,
|
|
provenanceAttestationStatus: result.provenanceAttestationStatus,
|
|
lockfileSha256: manifest.lockfileSha256,
|
|
distSha256: manifest.distSha256,
|
|
failures: Object.freeze(failures),
|
|
});
|
|
}
|
|
|
|
async function readTrust(
|
|
repositoryRoot: string,
|
|
publicKeyPath: string | undefined,
|
|
keyId: string | undefined,
|
|
): Promise<ProviderTrust | null> {
|
|
if (!publicKeyPath || !keyId?.trim()) return null;
|
|
try {
|
|
return Object.freeze({
|
|
keyId,
|
|
publicKey: createPublicKey(
|
|
await readFile(path.resolve(repositoryRoot, publicKeyPath), "utf8"),
|
|
),
|
|
});
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function optionalJson(
|
|
repositoryRoot: string,
|
|
file: string | undefined,
|
|
): Promise<unknown> {
|
|
if (!file) return null;
|
|
try {
|
|
return JSON.parse(
|
|
await readFile(path.resolve(repositoryRoot, file), "utf8"),
|
|
) as unknown;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function requiredJson(
|
|
repositoryRoot: string,
|
|
file: string,
|
|
): Promise<Record<string, unknown>> {
|
|
const value: unknown = JSON.parse(
|
|
await readFile(path.join(repositoryRoot, file), "utf8"),
|
|
);
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
throw new TypeError(`${file} must be a JSON object`);
|
|
}
|
|
return value as Record<string, unknown>;
|
|
}
|