fix: harden provider and promotion evidence
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
dependencyDiffArtifactSchema,
|
||||
dependencyInventoryArtifactSchema,
|
||||
licenseReportArtifactSchema,
|
||||
localEvidenceAssessmentArtifactSchema,
|
||||
provenanceArtifactSchema,
|
||||
releaseManifestArtifactSchema,
|
||||
releaseVerificationArtifactSchema,
|
||||
@@ -28,8 +29,15 @@ import {
|
||||
verifyBuildManifestOutputs,
|
||||
} from "./build-manifest-outputs.ts";
|
||||
import { assertMatchesJsonSchema } from "./json-schema.ts";
|
||||
import type { ReleaseCandidateManifest } from "./release-candidate.ts";
|
||||
import { collectDistOutputs, distSha256 } from "./release-candidate.ts";
|
||||
import {
|
||||
LOCAL_EVIDENCE_ASSESSMENT_PATH,
|
||||
RELEASE_CANDIDATE_EVIDENCE_PATHS,
|
||||
RELEASE_CANDIDATE_MANIFEST_PATH,
|
||||
collectDistOutputs,
|
||||
distSha256,
|
||||
releaseCandidateManifestSchema,
|
||||
type ReleaseCandidateManifest,
|
||||
} from "./release-candidate.ts";
|
||||
import { verifyReleaseRuntimeCoherence } from "./release-runtime-coherence.ts";
|
||||
import { digestReleaseInputFiles } from "./release-input-evidence.ts";
|
||||
import {
|
||||
@@ -290,7 +298,503 @@ export async function verifyLocalSupplyChainEvidence(
|
||||
});
|
||||
}
|
||||
|
||||
export const LOCAL_EVIDENCE_VERIFIER_ID =
|
||||
"clean-architecture-frontend-template/local-evidence-verifier";
|
||||
export const LOCAL_EVIDENCE_VERIFIER_VERSION = "1";
|
||||
export const LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS = Object.freeze([
|
||||
"scripts/contracts/release-artifacts.ts",
|
||||
"scripts/create-release-candidate.ts",
|
||||
"scripts/generate-supply-chain.ts",
|
||||
"scripts/lib/build-manifest-outputs.ts",
|
||||
"scripts/lib/json-schema.ts",
|
||||
"scripts/lib/local-policy-evidence.ts",
|
||||
"scripts/lib/local-release-evidence.ts",
|
||||
"scripts/lib/release-candidate.ts",
|
||||
"scripts/lib/release-input-evidence.ts",
|
||||
"scripts/lib/release-runtime-coherence.ts",
|
||||
"scripts/lib/repository-file-inventory.ts",
|
||||
"scripts/lib/secret-scan-evaluator.ts",
|
||||
"scripts/lib/secret-scan-policy.ts",
|
||||
"scripts/lib/supply-chain.ts",
|
||||
"scripts/lib/validated-json-artifact.ts",
|
||||
"src/contracts/release-artifacts.ts",
|
||||
] as const);
|
||||
export const LOCAL_EVIDENCE_POLICY_INPUT_PATHS = Object.freeze([
|
||||
"config/security/dependency-baseline.approval.json",
|
||||
"config/security/dependency-baseline.json",
|
||||
"config/security/dependency-change-evidence.json",
|
||||
"config/security/dependency-policy.json",
|
||||
"config/security/secret-scan-policy.json",
|
||||
"config/security/vulnerability-exceptions.json",
|
||||
"config/security/vulnerability-policy.json",
|
||||
"schemas/artifacts/build-manifest.schema.json",
|
||||
"schemas/artifacts/dependency-inventory.schema.json",
|
||||
"schemas/artifacts/supply-chain-verification.schema.json",
|
||||
...LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS,
|
||||
] as const);
|
||||
|
||||
export async function createLocalEvidenceAssessment(
|
||||
repositoryRoot = process.cwd(),
|
||||
): Promise<z.infer<typeof localEvidenceAssessmentArtifactSchema>> {
|
||||
const root = path.resolve(repositoryRoot);
|
||||
const outputs = await collectDistOutputs(root);
|
||||
const evidencePaths = RELEASE_CANDIDATE_EVIDENCE_PATHS.filter(
|
||||
(memberPath) => memberPath !== LOCAL_EVIDENCE_ASSESSMENT_PATH,
|
||||
);
|
||||
const evidenceInputs = (
|
||||
await Promise.all([
|
||||
...outputs.map(async ({ path: memberPath }) => digestInput(root, memberPath)),
|
||||
...evidencePaths.map((memberPath) => digestInput(root, memberPath)),
|
||||
])
|
||||
).sort((left, right) => asciiCompare(left.path, right.path));
|
||||
const lockfile = evidenceInputs.find(({ path: memberPath }) => memberPath === "pnpm-lock.yaml");
|
||||
const sbom = evidenceInputs.find(
|
||||
({ path: memberPath }) => memberPath === "artifacts/release/sbom.cdx.json",
|
||||
);
|
||||
if (!lockfile || !sbom) throw new Error("local assessment candidate inputs are incomplete");
|
||||
const candidate: ReleaseCandidateManifest = {
|
||||
schemaVersion: 1,
|
||||
distSha256: distSha256(outputs),
|
||||
lockfileSha256: lockfile.sha256,
|
||||
bundleSha256: supplyChainDigest(evidenceInputs),
|
||||
files: evidenceInputs,
|
||||
};
|
||||
const evaluated = await evaluateProducerLocalChecks(root, candidate);
|
||||
const [build, release, provenance, supply, sbomDocument, policyInputs] = await Promise.all([
|
||||
readJson(root, "artifacts/release/build-manifest.json").then((value) =>
|
||||
buildManifestArtifactSchema.parse(value),
|
||||
),
|
||||
readJson(root, "dist/release-manifest.json").then((value) =>
|
||||
releaseManifestArtifactSchema.parse(value),
|
||||
),
|
||||
readJson(root, "artifacts/release/provenance.json").then((value) =>
|
||||
provenanceArtifactSchema.parse(value),
|
||||
),
|
||||
readJson(root, "artifacts/security/supply-chain-verification.json").then((value) =>
|
||||
supplyChainVerificationArtifactSchema.parse(value),
|
||||
),
|
||||
readJson(root, "artifacts/release/sbom.cdx.json").then((value) =>
|
||||
sbomArtifactSchema.parse(value),
|
||||
),
|
||||
Promise.all(
|
||||
LOCAL_EVIDENCE_POLICY_INPUT_PATHS.map((policyPath) =>
|
||||
digestInput(root, policyPath),
|
||||
),
|
||||
),
|
||||
]);
|
||||
const identityFailures: string[] = [];
|
||||
if (build.commitSha !== release.commitSha) {
|
||||
identityFailures.push("producer build/release source revision mismatch");
|
||||
}
|
||||
if (
|
||||
provenance.predicate.materials.sourceSetSha256 !== supply.sourceSetSha256
|
||||
) {
|
||||
identityFailures.push("producer provenance/supply source-set mismatch");
|
||||
}
|
||||
if (supply.distSha256 !== candidate.distSha256) {
|
||||
identityFailures.push("producer supply/candidate dist digest mismatch");
|
||||
}
|
||||
if (supply.lockfileSha256 !== candidate.lockfileSha256) {
|
||||
identityFailures.push("producer supply/candidate lockfile digest mismatch");
|
||||
}
|
||||
if (supply.sbomSha256 !== supplyChainDigest(sbomDocument)) {
|
||||
identityFailures.push("producer supply/candidate SBOM digest mismatch");
|
||||
}
|
||||
const checks = {
|
||||
...evaluated.checks,
|
||||
...(identityFailures.some((failure) => failure.includes("build/release"))
|
||||
? { release: "FAIL" as const }
|
||||
: {}),
|
||||
...(identityFailures.some((failure) => !failure.includes("build/release"))
|
||||
? { supplyChain: "FAIL" as const }
|
||||
: {}),
|
||||
};
|
||||
const failures = [...evaluated.failures, ...identityFailures];
|
||||
const status = failures.length === 0 && Object.values(checks).every(
|
||||
(check) => check === "PASS",
|
||||
)
|
||||
? ("PASS" as const)
|
||||
: ("FAIL" as const);
|
||||
const verifierSources = policyInputs.filter(({ path: policyPath }) =>
|
||||
(LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS as readonly string[]).includes(policyPath),
|
||||
);
|
||||
if (verifierSources.length !== LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS.length) {
|
||||
throw new Error("local assessment verifier source set is incomplete");
|
||||
}
|
||||
return localEvidenceAssessmentArtifactSchema.parse({
|
||||
schemaVersion: 1,
|
||||
artifactType: "local-evidence-assessment",
|
||||
generatedAt: build.generatedAt,
|
||||
status,
|
||||
verifier: {
|
||||
id: LOCAL_EVIDENCE_VERIFIER_ID,
|
||||
version: LOCAL_EVIDENCE_VERIFIER_VERSION,
|
||||
sourceSha256: supplyChainDigest(verifierSources),
|
||||
},
|
||||
source: {
|
||||
revision: build.commitSha,
|
||||
sourceSetSha256: supply.sourceSetSha256,
|
||||
},
|
||||
candidate: {
|
||||
distSha256: candidate.distSha256,
|
||||
lockfileSha256: candidate.lockfileSha256,
|
||||
sbomSha256: sbom.sha256,
|
||||
},
|
||||
policyInputs,
|
||||
evidenceInputs,
|
||||
checks,
|
||||
failures,
|
||||
});
|
||||
}
|
||||
|
||||
type LocalCheckName =
|
||||
| "release"
|
||||
| "supplyChain"
|
||||
| "dependencyPolicy"
|
||||
| "licensePolicy"
|
||||
| "vulnerabilityPolicy"
|
||||
| "secretScan";
|
||||
|
||||
async function evaluateProducerLocalChecks(
|
||||
root: string,
|
||||
candidate: ReleaseCandidateManifest,
|
||||
): Promise<Readonly<{
|
||||
checks: Readonly<Record<LocalCheckName, "PASS" | "FAIL">>;
|
||||
failures: readonly string[];
|
||||
}>> {
|
||||
const checks: Record<LocalCheckName, "PASS" | "FAIL"> = {
|
||||
release: "PASS",
|
||||
supplyChain: "PASS",
|
||||
dependencyPolicy: "PASS",
|
||||
licensePolicy: "PASS",
|
||||
vulnerabilityPolicy: "PASS",
|
||||
secretScan: "PASS",
|
||||
};
|
||||
const failures: string[] = [];
|
||||
const evaluate = async (
|
||||
check: LocalCheckName,
|
||||
operation: () => Promise<readonly string[]>,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const diagnostics = await operation();
|
||||
if (diagnostics.length > 0) {
|
||||
checks[check] = "FAIL";
|
||||
failures.push(...diagnostics.map((failure) => `${check}:${failure}`));
|
||||
}
|
||||
} catch (error) {
|
||||
checks[check] = "FAIL";
|
||||
failures.push(
|
||||
`${check}:${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
await evaluate("release", async () => {
|
||||
const [build, release, stored] = await Promise.all([
|
||||
readJson(root, "artifacts/release/build-manifest.json").then((value) =>
|
||||
buildManifestArtifactSchema.parse(value),
|
||||
),
|
||||
readJson(root, "dist/release-manifest.json").then((value) =>
|
||||
releaseManifestArtifactSchema.parse(value),
|
||||
),
|
||||
readJson(root, "artifacts/release/verification.json").then((value) =>
|
||||
releaseVerificationArtifactSchema.parse(value),
|
||||
),
|
||||
]);
|
||||
const diagnostics: string[] = [];
|
||||
if (
|
||||
build.commitSha !== release.commitSha ||
|
||||
build.buildId !== release.buildId ||
|
||||
build.releaseId !== release.releaseId ||
|
||||
build.generatedAt !== release.builtAt
|
||||
) {
|
||||
diagnostics.push("build/release identity mismatch");
|
||||
}
|
||||
if (
|
||||
!stored.passed ||
|
||||
!stored.artifact.checked ||
|
||||
!stored.artifact.compatible ||
|
||||
stored.artifact.mismatches.length > 0 ||
|
||||
stored.artifact.releaseId !== release.releaseId ||
|
||||
stored.generatedAt !== release.builtAt ||
|
||||
stored.fixtures.length === 0 ||
|
||||
stored.fixtures.some((fixture) => !fixture.passed)
|
||||
) {
|
||||
diagnostics.push("stored release verification is not a coherent PASS");
|
||||
}
|
||||
return diagnostics;
|
||||
});
|
||||
await evaluate("supplyChain", async () => {
|
||||
const [supply, coherence] = await Promise.all([
|
||||
readJson(root, "artifacts/security/supply-chain-verification.json").then((value) =>
|
||||
supplyChainVerificationArtifactSchema.parse(value),
|
||||
),
|
||||
readJson(root, "artifacts/security/supply-chain-coherence.json").then((value) =>
|
||||
supplyChainCoherenceReportSchema.parse(value),
|
||||
),
|
||||
]);
|
||||
const diagnostics: string[] = [];
|
||||
if (
|
||||
supply.localStatus !== "PASS" ||
|
||||
supply.failures.length > 0 ||
|
||||
supply.distSha256 !== candidate.distSha256 ||
|
||||
supply.lockfileSha256 !== candidate.lockfileSha256 ||
|
||||
coherence.status !== "PASS" ||
|
||||
coherence.failures.length > 0 ||
|
||||
coherence.distSha256 !== candidate.distSha256 ||
|
||||
coherence.lockfileSha256 !== candidate.lockfileSha256
|
||||
) {
|
||||
diagnostics.push("stored supply-chain evidence is not a coherent PASS");
|
||||
}
|
||||
return diagnostics;
|
||||
});
|
||||
await evaluate("dependencyPolicy", async () => {
|
||||
const [inventory, stored] = await Promise.all([
|
||||
readJson(root, "artifacts/release/dependency-inventory.json").then((value) =>
|
||||
dependencyInventoryArtifactSchema.parse(value),
|
||||
),
|
||||
readJson(root, "artifacts/security/dependency-diff.json").then((value) =>
|
||||
dependencyDiffArtifactSchema.parse(value),
|
||||
),
|
||||
]);
|
||||
const recomputed = recomputeDependencyEvidence({
|
||||
inventory,
|
||||
baseline: await optionalReadJson(root, "config/security/dependency-baseline.json"),
|
||||
baselineApproval: await optionalReadJson(
|
||||
root,
|
||||
"config/security/dependency-baseline.approval.json",
|
||||
),
|
||||
dependencyChangeEvidence: await readJson(
|
||||
root,
|
||||
"config/security/dependency-change-evidence.json",
|
||||
),
|
||||
});
|
||||
return compareStoredDependencyEvidence(recomputed, stored);
|
||||
});
|
||||
await evaluate("licensePolicy", async () => {
|
||||
const [inventory, stored, policy] = await Promise.all([
|
||||
readJson(root, "artifacts/release/dependency-inventory.json").then((value) =>
|
||||
dependencyInventoryArtifactSchema.parse(value),
|
||||
),
|
||||
readJson(root, "artifacts/security/license-report.json").then((value) =>
|
||||
licenseReportArtifactSchema.parse(value),
|
||||
),
|
||||
readJson(root, "config/security/dependency-policy.json"),
|
||||
]);
|
||||
return compareStoredLicenseEvidence(
|
||||
recomputeLicenseEvidence({ inventory, policy }),
|
||||
stored,
|
||||
);
|
||||
});
|
||||
await evaluate("vulnerabilityPolicy", async () => {
|
||||
const vulnerability = vulnerabilityReportArtifactSchema.parse(
|
||||
await readJson(root, "artifacts/security/vulnerability-report.json"),
|
||||
);
|
||||
return compareStoredLocalVulnerabilityReport(
|
||||
candidate.lockfileSha256,
|
||||
vulnerability,
|
||||
);
|
||||
});
|
||||
await evaluate("secretScan", async () => {
|
||||
const evaluation = await evaluateRepositorySecretScan({ repositoryRoot: root });
|
||||
return verifyStoredSecretScan(
|
||||
evaluation,
|
||||
await readJson(root, "artifacts/security/scan.sarif"),
|
||||
);
|
||||
});
|
||||
return Object.freeze({ checks: Object.freeze(checks), failures: Object.freeze(failures) });
|
||||
}
|
||||
|
||||
async function digestInput(
|
||||
repositoryRoot: string,
|
||||
memberPath: string,
|
||||
): Promise<Readonly<{ path: string; bytes: number; sha256: string }>> {
|
||||
const absolute = path.resolve(repositoryRoot, memberPath);
|
||||
const relative = path.relative(repositoryRoot, absolute);
|
||||
if (
|
||||
relative === "" ||
|
||||
relative === ".." ||
|
||||
relative.startsWith(`..${path.sep}`) ||
|
||||
path.isAbsolute(relative)
|
||||
) {
|
||||
throw new TypeError(`local assessment input escapes repository: ${memberPath}`);
|
||||
}
|
||||
const bytes = await readFile(absolute);
|
||||
return Object.freeze({
|
||||
path: memberPath,
|
||||
bytes: bytes.byteLength,
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
});
|
||||
}
|
||||
|
||||
function asciiCompare(left: string, right: string): number {
|
||||
return left < right ? -1 : left > right ? 1 : 0;
|
||||
}
|
||||
|
||||
export async function verifyArchivedLocalEvidence(input: Readonly<{
|
||||
extractionRoot: string;
|
||||
expectedManifest: ReleaseCandidateManifest;
|
||||
}>): Promise<Readonly<{
|
||||
status: "PASS" | "FAIL";
|
||||
identity: null | Readonly<{
|
||||
sourceRevision: string;
|
||||
sourceSetSha256: string;
|
||||
assessmentSha256: string;
|
||||
}>;
|
||||
failures: readonly string[];
|
||||
}>> {
|
||||
const extractionRoot = path.resolve(input.extractionRoot);
|
||||
const failures: string[] = [];
|
||||
let extractedManifest: ReleaseCandidateManifest | null = null;
|
||||
try {
|
||||
extractedManifest = releaseCandidateManifestSchema.parse(
|
||||
await readJson(extractionRoot, RELEASE_CANDIDATE_MANIFEST_PATH),
|
||||
);
|
||||
} catch {
|
||||
failures.push("extracted release candidate manifest is missing or invalid");
|
||||
}
|
||||
if (
|
||||
extractedManifest &&
|
||||
JSON.stringify(extractedManifest) !== JSON.stringify(input.expectedManifest)
|
||||
) {
|
||||
failures.push("caller expectedManifest differs from extracted manifest");
|
||||
}
|
||||
|
||||
let assessment: z.infer<typeof localEvidenceAssessmentArtifactSchema> | null = null;
|
||||
let assessmentSha256 = "";
|
||||
let assessmentBytes: Buffer | null = null;
|
||||
try {
|
||||
assessmentBytes = await readFile(
|
||||
path.join(extractionRoot, LOCAL_EVIDENCE_ASSESSMENT_PATH),
|
||||
);
|
||||
assessmentSha256 = createHash("sha256").update(assessmentBytes).digest("hex");
|
||||
assessment = localEvidenceAssessmentArtifactSchema.parse(
|
||||
JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(assessmentBytes)) as unknown,
|
||||
);
|
||||
} catch {
|
||||
failures.push("local evidence assessment is missing or invalid");
|
||||
}
|
||||
|
||||
if (assessment && extractedManifest) {
|
||||
const assessmentMember = extractedManifest.files.find(
|
||||
({ path: memberPath }) => memberPath === LOCAL_EVIDENCE_ASSESSMENT_PATH,
|
||||
);
|
||||
if (
|
||||
!assessmentMember ||
|
||||
assessmentMember.bytes !== assessmentBytes?.byteLength ||
|
||||
assessmentMember.sha256 !== assessmentSha256
|
||||
) {
|
||||
failures.push("local assessment manifest binding mismatch");
|
||||
}
|
||||
if (
|
||||
assessment.verifier.id !== LOCAL_EVIDENCE_VERIFIER_ID ||
|
||||
assessment.verifier.version !== LOCAL_EVIDENCE_VERIFIER_VERSION
|
||||
) {
|
||||
failures.push("local assessment verifier identity mismatch");
|
||||
}
|
||||
const policyPaths = assessment.policyInputs.map(({ path: policyPath }) => policyPath);
|
||||
if (JSON.stringify(policyPaths) !== JSON.stringify(LOCAL_EVIDENCE_POLICY_INPUT_PATHS)) {
|
||||
failures.push("local assessment policyInputs exact set mismatch");
|
||||
}
|
||||
const verifierSources = assessment.policyInputs.filter(({ path: policyPath }) =>
|
||||
(LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS as readonly string[]).includes(policyPath),
|
||||
);
|
||||
if (
|
||||
verifierSources.length !== LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS.length ||
|
||||
supplyChainDigest(verifierSources) !== assessment.verifier.sourceSha256
|
||||
) {
|
||||
failures.push("local assessment verifier-source digest mismatch");
|
||||
}
|
||||
|
||||
const expectedEvidenceInputs = extractedManifest.files.filter(
|
||||
({ path: memberPath }) => memberPath !== LOCAL_EVIDENCE_ASSESSMENT_PATH,
|
||||
);
|
||||
if (JSON.stringify(assessment.evidenceInputs) !== JSON.stringify(expectedEvidenceInputs)) {
|
||||
failures.push("local assessment evidenceInputs exact member binding mismatch");
|
||||
}
|
||||
const sbom = extractedManifest.files.find(
|
||||
({ path: memberPath }) => memberPath === "artifacts/release/sbom.cdx.json",
|
||||
);
|
||||
if (
|
||||
assessment.candidate.distSha256 !== extractedManifest.distSha256 ||
|
||||
assessment.candidate.lockfileSha256 !== extractedManifest.lockfileSha256 ||
|
||||
!sbom ||
|
||||
assessment.candidate.sbomSha256 !== sbom.sha256
|
||||
) {
|
||||
failures.push("local assessment candidate digest binding mismatch");
|
||||
}
|
||||
if (assessment.status !== "PASS" || Object.values(assessment.checks).includes("FAIL")) {
|
||||
failures.push("local evidence assessment is not PASS");
|
||||
}
|
||||
|
||||
const identities = await readArchivedIdentities(extractionRoot, failures);
|
||||
if (
|
||||
identities.buildRevision !== assessment.source.revision ||
|
||||
identities.releaseRevision !== assessment.source.revision
|
||||
) {
|
||||
failures.push("local assessment source revision identity mismatch");
|
||||
}
|
||||
if (
|
||||
identities.provenanceSourceSetSha256 !== assessment.source.sourceSetSha256 ||
|
||||
identities.supplySourceSetSha256 !== assessment.source.sourceSetSha256
|
||||
) {
|
||||
failures.push("local assessment source-set identity mismatch");
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueFailures = Object.freeze([...new Set(failures)]);
|
||||
const passingAssessment = uniqueFailures.length === 0 ? assessment : null;
|
||||
return Object.freeze({
|
||||
status: passingAssessment ? "PASS" : "FAIL",
|
||||
identity: passingAssessment
|
||||
? Object.freeze({
|
||||
sourceRevision: passingAssessment.source.revision,
|
||||
sourceSetSha256: passingAssessment.source.sourceSetSha256,
|
||||
assessmentSha256,
|
||||
})
|
||||
: null,
|
||||
failures: uniqueFailures,
|
||||
});
|
||||
}
|
||||
|
||||
async function readArchivedIdentities(
|
||||
extractionRoot: string,
|
||||
failures: string[],
|
||||
): Promise<Readonly<{
|
||||
buildRevision: unknown;
|
||||
releaseRevision: unknown;
|
||||
provenanceSourceSetSha256: unknown;
|
||||
supplySourceSetSha256: unknown;
|
||||
}>> {
|
||||
try {
|
||||
const [buildDocument, releaseDocument, provenanceDocument, supplyDocument] = await Promise.all([
|
||||
readJson(extractionRoot, "artifacts/release/build-manifest.json"),
|
||||
readJson(extractionRoot, "dist/release-manifest.json"),
|
||||
readJson(extractionRoot, "artifacts/release/provenance.json"),
|
||||
readJson(extractionRoot, "artifacts/security/supply-chain-verification.json"),
|
||||
]);
|
||||
const build = buildManifestArtifactSchema.parse(buildDocument);
|
||||
const release = releaseManifestArtifactSchema.parse(releaseDocument);
|
||||
const provenance = provenanceArtifactSchema.parse(provenanceDocument);
|
||||
const supply = supplyChainVerificationArtifactSchema.parse(supplyDocument);
|
||||
return Object.freeze({
|
||||
buildRevision: build.commitSha,
|
||||
releaseRevision: release.commitSha,
|
||||
provenanceSourceSetSha256: provenance.predicate.materials.sourceSetSha256,
|
||||
supplySourceSetSha256: supply.sourceSetSha256,
|
||||
});
|
||||
} catch {
|
||||
failures.push("archived source/build/provenance identities are missing or invalid");
|
||||
return Object.freeze({
|
||||
buildRevision: null,
|
||||
releaseRevision: null,
|
||||
provenanceSourceSetSha256: null,
|
||||
supplySourceSetSha256: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function assessLocalEvidenceForProducer(input: Readonly<{
|
||||
repositoryRoot?: string;
|
||||
candidate: ReleaseCandidateManifest;
|
||||
}>): Promise<Readonly<{
|
||||
|
||||
Reference in New Issue
Block a user