fix: harden provider and promotion evidence
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
||||
RELEASE_CANDIDATE_MANIFEST_PATH,
|
||||
} from "../lib/release-candidate.ts";
|
||||
import { validatePackageScriptGraph } from "../lib/package-script-graph.ts";
|
||||
import { PROMOTED_STAGING_PATHS } from "./promotion-artifacts.ts";
|
||||
import { PROMOTED_UPLOAD_PATHS } from "./promotion-artifacts.ts";
|
||||
|
||||
const ciActionRegistrationSchema = z
|
||||
.object({
|
||||
@@ -332,7 +332,11 @@ const extractStep = z
|
||||
})
|
||||
.strict();
|
||||
const providerStep = z
|
||||
.object({ kind: z.literal("run-provider"), provider: z.enum(["vulnerability", "provenance"]) })
|
||||
.object({
|
||||
kind: z.literal("run-provider"),
|
||||
provider: z.enum(["vulnerability", "provenance"]),
|
||||
stepId: id,
|
||||
})
|
||||
.strict();
|
||||
const validateProviderStep = z
|
||||
.object({
|
||||
@@ -340,7 +344,12 @@ const validateProviderStep = z
|
||||
provider: z.enum(["vulnerability", "provenance"]),
|
||||
})
|
||||
.strict();
|
||||
const promotionStep = z.object({ kind: z.literal("verify-promotion") }).strict();
|
||||
const promotionStep = z
|
||||
.object({ kind: z.literal("verify-promotion"), stepId: id })
|
||||
.strict();
|
||||
const cleanupPromotionStep = z
|
||||
.object({ kind: z.literal("cleanup-promotion"), finalizerStepId: id })
|
||||
.strict();
|
||||
|
||||
const jobStepSchema = z.discriminatedUnion("kind", [
|
||||
checkoutStep,
|
||||
@@ -356,6 +365,7 @@ const jobStepSchema = z.discriminatedUnion("kind", [
|
||||
providerStep,
|
||||
validateProviderStep,
|
||||
promotionStep,
|
||||
cleanupPromotionStep,
|
||||
]);
|
||||
|
||||
const jobSchema = z
|
||||
@@ -756,9 +766,9 @@ function validateContractSemantics(
|
||||
merge_gate: ["checkout", "setup-node", "frozen-install", "browser-install", "run-gate", "upload"],
|
||||
release_gate: ["checkout", "setup-node", "frozen-install", "browser-install", "run-gate", "upload"],
|
||||
immutable_build: ["checkout", "setup-node", "frozen-install", "run-gate", "archive-candidate", "upload"],
|
||||
vulnerability_provider: ["checkout", "setup-node", "frozen-install", "download", "extract", "run-provider", "validate-provider-evidence", "upload"],
|
||||
provenance_provider: ["checkout", "setup-node", "frozen-install", "download", "extract", "run-provider", "validate-provider-evidence", "upload"],
|
||||
promotion: ["checkout", "setup-node", "frozen-install", "download", "download", "download", "extract", "verify-promotion", "upload"],
|
||||
vulnerability_provider: ["checkout", "setup-node", "frozen-install", "download", "run-provider", "validate-provider-evidence", "upload"],
|
||||
provenance_provider: ["checkout", "setup-node", "frozen-install", "download", "run-provider", "validate-provider-evidence", "upload"],
|
||||
promotion: ["checkout", "setup-node", "frozen-install", "download", "download", "download", "verify-promotion", "upload", "cleanup-promotion"],
|
||||
production_gate: ["checkout", "setup-node", "frozen-install", "run-gate", "upload"],
|
||||
field_gate: ["checkout", "setup-node", "frozen-install", "run-gate", "upload"],
|
||||
documentation_gate: ["checkout", "setup-node", "frozen-install", "run-gate", "upload"],
|
||||
@@ -776,8 +786,11 @@ function validateContractSemantics(
|
||||
vulnerability_provider: [
|
||||
{ name: "CANDIDATE_ARCHIVE_SHA256", value: "${{ needs.immutable_build.outputs.archive_sha256 }}" },
|
||||
{ name: "CANDIDATE_ARCHIVE_PATH", value: ".release/vulnerability-candidate/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz" },
|
||||
{ name: "CANDIDATE_DIST_SHA256", value: "${{ needs.immutable_build.outputs.dist_sha256 }}" },
|
||||
{ name: "CANDIDATE_LOCKFILE_PATH", value: ".release/verified-vulnerability/pnpm-lock.yaml" },
|
||||
{ name: "CI_RUN_ID", value: "${{ gitea.run_id }}" },
|
||||
{ name: "CI_RUN_ATTEMPT", value: "${{ gitea.run_attempt }}" },
|
||||
{ name: "EXPECTED_SOURCE_REVISION", value: "${{ gitea.sha }}" },
|
||||
{ name: "VULNERABILITY_PUBLIC_KEY_PATH", value: "${{ vars.VULNERABILITY_PUBLIC_KEY_PATH }}" },
|
||||
{ name: "VULNERABILITY_KEY_ID", value: "${{ vars.VULNERABILITY_KEY_ID }}" },
|
||||
{ name: "VULNERABILITY_PROVIDER_COMMAND", value: "${{ vars.VULNERABILITY_PROVIDER_COMMAND }}" },
|
||||
{ name: "VULNERABILITY_REPORT_PATH", value: "provider-evidence/untrusted/vulnerability-report.json" },
|
||||
{ name: "VALIDATED_PROVIDER_REPORT_PATH", value: "provider-evidence/vulnerability-report.json" },
|
||||
@@ -785,8 +798,11 @@ function validateContractSemantics(
|
||||
provenance_provider: [
|
||||
{ name: "CANDIDATE_ARCHIVE_SHA256", value: "${{ needs.immutable_build.outputs.archive_sha256 }}" },
|
||||
{ name: "CANDIDATE_ARCHIVE_PATH", value: ".release/provenance-candidate/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz" },
|
||||
{ name: "CANDIDATE_DIST_SHA256", value: "${{ needs.immutable_build.outputs.dist_sha256 }}" },
|
||||
{ name: "CANDIDATE_LOCKFILE_PATH", value: ".release/verified-provenance/pnpm-lock.yaml" },
|
||||
{ name: "CI_RUN_ID", value: "${{ gitea.run_id }}" },
|
||||
{ name: "CI_RUN_ATTEMPT", value: "${{ gitea.run_attempt }}" },
|
||||
{ name: "EXPECTED_SOURCE_REVISION", value: "${{ gitea.sha }}" },
|
||||
{ name: "PROVENANCE_PUBLIC_KEY_PATH", value: "${{ vars.PROVENANCE_PUBLIC_KEY_PATH }}" },
|
||||
{ name: "PROVENANCE_KEY_ID", value: "${{ vars.PROVENANCE_KEY_ID }}" },
|
||||
{ name: "PROVENANCE_PROVIDER_COMMAND", value: "${{ vars.PROVENANCE_PROVIDER_COMMAND }}" },
|
||||
{ name: "PROVENANCE_ATTESTATION_PATH", value: "provider-evidence/untrusted/provenance-attestation.json" },
|
||||
{ name: "VALIDATED_PROVIDER_REPORT_PATH", value: "provider-evidence/provenance-attestation.json" },
|
||||
@@ -794,13 +810,16 @@ function validateContractSemantics(
|
||||
promotion: [
|
||||
{ name: "CANDIDATE_ARCHIVE_SHA256", value: "${{ needs.immutable_build.outputs.archive_sha256 }}" },
|
||||
{ name: "CANDIDATE_ARCHIVE_PATH", value: ".release/candidate/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz" },
|
||||
{ name: "CANDIDATE_ROOT", value: "${{ gitea.workspace }}/.release/verified-candidate" },
|
||||
{ name: "CI_RUN_ID", value: "${{ gitea.run_id }}" },
|
||||
{ name: "CI_RUN_ATTEMPT", value: "${{ gitea.run_attempt }}" },
|
||||
{ name: "VULNERABILITY_REPORT_PATH", value: "${{ gitea.workspace }}/.release/vulnerability/vulnerability-report.json" },
|
||||
{ name: "PROVENANCE_ATTESTATION_PATH", value: "${{ gitea.workspace }}/.release/provenance/provenance-attestation.json" },
|
||||
{ name: "VULNERABILITY_PUBLIC_KEY_PATH", value: "${{ vars.VULNERABILITY_PUBLIC_KEY_PATH }}" },
|
||||
{ name: "VULNERABILITY_KEY_ID", value: "${{ vars.VULNERABILITY_KEY_ID }}" },
|
||||
{ name: "PROVENANCE_PUBLIC_KEY_PATH", value: "${{ vars.PROVENANCE_PUBLIC_KEY_PATH }}" },
|
||||
{ name: "PROVENANCE_KEY_ID", value: "${{ vars.PROVENANCE_KEY_ID }}" },
|
||||
{ name: "VULNERABILITY_INVOCATION_NONCE", value: "${{ needs.vulnerability_provider.outputs.invocation_nonce }}" },
|
||||
{ name: "PROVENANCE_INVOCATION_NONCE", value: "${{ needs.provenance_provider.outputs.invocation_nonce }}" },
|
||||
],
|
||||
production_gate: [],
|
||||
field_gate: [
|
||||
@@ -875,12 +894,14 @@ function validateContractSemantics(
|
||||
if (upload?.kind === "upload" && upload.always) {
|
||||
issue("promotion upload must not use always");
|
||||
}
|
||||
const cleanupIndex = order.indexOf("cleanup-promotion");
|
||||
if (
|
||||
order.indexOf("extract") < order.lastIndexOf("download") ||
|
||||
order.indexOf("verify-promotion") < order.indexOf("extract") ||
|
||||
order.indexOf("upload") < order.indexOf("verify-promotion")
|
||||
order.includes("extract") ||
|
||||
verificationIndex < order.lastIndexOf("download") ||
|
||||
uploadIndex < verificationIndex ||
|
||||
cleanupIndex !== uploadIndex + 1
|
||||
) {
|
||||
issue("promotion formula order must download, verify, then upload");
|
||||
issue("promotion formula order must download, finalize, upload, then cleanup without extraction");
|
||||
}
|
||||
}
|
||||
const immutable = contract.jobs.find(({ id }) => id === "immutable_build");
|
||||
@@ -905,7 +926,7 @@ function validateContractSemantics(
|
||||
const promotionUpload = promotion?.steps.find(
|
||||
(step) => step.kind === "upload" && step.transferId === "promoted-release",
|
||||
);
|
||||
if (!promotionUpload || promotionUpload.kind !== "upload" || JSON.stringify(promotionUpload.paths) !== JSON.stringify(PROMOTED_STAGING_PATHS)) {
|
||||
if (!promotionUpload || promotionUpload.kind !== "upload" || JSON.stringify(promotionUpload.paths) !== JSON.stringify(PROMOTED_UPLOAD_PATHS)) {
|
||||
issue("promotion upload bundle must contain the exact five typed paths");
|
||||
}
|
||||
|
||||
@@ -934,10 +955,9 @@ function validateCanonicalStepFields(
|
||||
const providerExpectations = {
|
||||
vulnerability_provider: {
|
||||
provider: "vulnerability",
|
||||
stepId: "supervise_vulnerability",
|
||||
downloadPath: ".release/vulnerability-candidate",
|
||||
archivePath: ".release/vulnerability-candidate/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz",
|
||||
targetRoot: ".release/verified-vulnerability",
|
||||
lockfilePath: ".release/verified-vulnerability/pnpm-lock.yaml",
|
||||
rawPath: "provider-evidence/untrusted/vulnerability-report.json",
|
||||
rawName: "VULNERABILITY_REPORT_PATH",
|
||||
sealedPath: "provider-evidence/vulnerability-report.json",
|
||||
@@ -945,10 +965,9 @@ function validateCanonicalStepFields(
|
||||
},
|
||||
provenance_provider: {
|
||||
provider: "provenance",
|
||||
stepId: "supervise_provenance",
|
||||
downloadPath: ".release/provenance-candidate",
|
||||
archivePath: ".release/provenance-candidate/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz",
|
||||
targetRoot: ".release/verified-provenance",
|
||||
lockfilePath: ".release/verified-provenance/pnpm-lock.yaml",
|
||||
rawPath: "provider-evidence/untrusted/provenance-attestation.json",
|
||||
rawName: "PROVENANCE_ATTESTATION_PATH",
|
||||
sealedPath: "provider-evidence/provenance-attestation.json",
|
||||
@@ -959,7 +978,6 @@ function validateCanonicalStepFields(
|
||||
const job = contract.jobs.find(({ id }) => id === jobId);
|
||||
const environment = new Map(job?.environment.map(({ name, value }) => [name, value]));
|
||||
const download = job?.steps.find(({ kind }) => kind === "download");
|
||||
const extract = job?.steps.find(({ kind }) => kind === "extract");
|
||||
const runProvider = job?.steps.find(({ kind }) => kind === "run-provider");
|
||||
const validateProvider = job?.steps.find(({ kind }) => kind === "validate-provider-evidence");
|
||||
const upload = job?.steps.find(
|
||||
@@ -967,11 +985,9 @@ function validateCanonicalStepFields(
|
||||
);
|
||||
if (
|
||||
!download || download.kind !== "download" || download.transferId !== "release-candidate" || download.path !== expected.downloadPath ||
|
||||
!extract || extract.kind !== "extract" || extract.archivePath !== expected.archivePath || extract.targetRoot !== expected.targetRoot ||
|
||||
!runProvider || runProvider.kind !== "run-provider" || runProvider.provider !== expected.provider ||
|
||||
!runProvider || runProvider.kind !== "run-provider" || runProvider.provider !== expected.provider || runProvider.stepId !== expected.stepId ||
|
||||
!validateProvider || validateProvider.kind !== "validate-provider-evidence" || validateProvider.provider !== expected.provider ||
|
||||
environment.get("CANDIDATE_ARCHIVE_PATH") !== expected.archivePath ||
|
||||
environment.get("CANDIDATE_LOCKFILE_PATH") !== expected.lockfilePath ||
|
||||
environment.get(expected.rawName) !== expected.rawPath ||
|
||||
environment.get("VALIDATED_PROVIDER_REPORT_PATH") !== expected.sealedPath ||
|
||||
!upload || upload.kind !== "upload" || JSON.stringify(upload.paths) !== JSON.stringify([expected.sealedPath])
|
||||
@@ -987,15 +1003,18 @@ function validateCanonicalStepFields(
|
||||
{ kind: "download", transferId: "vulnerability-provider-evidence", path: ".release/vulnerability" },
|
||||
{ kind: "download", transferId: "provenance-provider-evidence", path: ".release/provenance" },
|
||||
];
|
||||
const promotionExtract = promotion?.steps.find(({ kind }) => kind === "extract");
|
||||
const promotionFinalizer = promotion?.steps.find(({ kind }) => kind === "verify-promotion");
|
||||
const promotionCleanup = promotion?.steps.find(({ kind }) => kind === "cleanup-promotion");
|
||||
if (
|
||||
JSON.stringify(promotionDownloads) !== JSON.stringify(expectedDownloads) ||
|
||||
!promotionExtract ||
|
||||
promotionExtract.kind !== "extract" ||
|
||||
promotionExtract.archivePath !== ".release/candidate/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz" ||
|
||||
promotionExtract.targetRoot !== ".release/verified-candidate"
|
||||
!promotionFinalizer ||
|
||||
promotionFinalizer.kind !== "verify-promotion" ||
|
||||
promotionFinalizer.stepId !== "finalize" ||
|
||||
!promotionCleanup ||
|
||||
promotionCleanup.kind !== "cleanup-promotion" ||
|
||||
promotionCleanup.finalizerStepId !== "finalize"
|
||||
) {
|
||||
issue("promotion download and extraction fields must remain linked");
|
||||
issue("promotion download fields and finalizer/cleanup step identities must remain linked");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1008,7 +1027,7 @@ function validateJobStepKinds(
|
||||
"gate-single": new Set(["checkout", "setup-node", "frozen-install", "run-gate", "upload"]),
|
||||
immutable: new Set(["checkout", "setup-node", "frozen-install", "run-gate", "archive-candidate", "upload"]),
|
||||
provider: new Set(["checkout", "setup-node", "frozen-install", "download", "validate-candidate-archive", "extract", "run-provider", "validate-provider-evidence", "upload"]),
|
||||
promotion: new Set(["checkout", "setup-node", "frozen-install", "download", "validate-candidate-archive", "extract", "verify-promotion", "upload"]),
|
||||
promotion: new Set(["checkout", "setup-node", "frozen-install", "download", "verify-promotion", "upload", "cleanup-promotion"]),
|
||||
};
|
||||
for (const step of job.steps) {
|
||||
if (!allowed[job.kind].has(step.kind)) {
|
||||
@@ -1016,16 +1035,12 @@ function validateJobStepKinds(
|
||||
}
|
||||
}
|
||||
const kinds = job.steps.map(({ kind }) => kind);
|
||||
const extractIndex = kinds.indexOf("extract");
|
||||
if ((job.kind === "provider" || job.kind === "promotion") && extractIndex < 0) {
|
||||
issue(`verified extraction step is missing: ${job.id}`);
|
||||
}
|
||||
if (job.kind === "provider") {
|
||||
const providerIndex = kinds.indexOf("run-provider");
|
||||
const validateProviderIndex = kinds.indexOf("validate-provider-evidence");
|
||||
const uploadIndex = kinds.indexOf("upload");
|
||||
if (
|
||||
providerIndex < extractIndex ||
|
||||
providerIndex < kinds.lastIndexOf("download") ||
|
||||
validateProviderIndex < providerIndex ||
|
||||
uploadIndex < validateProviderIndex
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user