fix: harden provider and promotion evidence
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { constants } from "node:fs";
|
||||
import { access, lstat, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { access, appendFile, lstat, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
provenanceProviderAttestationSchema,
|
||||
vulnerabilityProviderReportSchema,
|
||||
} from "./lib/provider-evidence.ts";
|
||||
import { validateProviderUpload } from "./lib/provider-upload-validator.ts";
|
||||
import { readBoundedRegularFile } from "./lib/ci-artifact-validator.ts";
|
||||
import { readProviderTrust } from "./lib/promotion-verifier.ts";
|
||||
import { superviseProviderEvidence } from "./lib/provider-supervisor.ts";
|
||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||
import {
|
||||
assertSafePublishLeaf,
|
||||
@@ -29,22 +31,37 @@ const reportPath =
|
||||
? process.env.VULNERABILITY_REPORT_PATH
|
||||
: process.env.PROVENANCE_ATTESTATION_PATH;
|
||||
const sealedPath = process.env.VALIDATED_PROVIDER_REPORT_PATH;
|
||||
const candidateLockfile = process.env.CANDIDATE_LOCKFILE_PATH;
|
||||
const archivePath = process.env.CANDIDATE_ARCHIVE_PATH;
|
||||
const archiveSha256 = process.env.CANDIDATE_ARCHIVE_SHA256;
|
||||
const candidateDistSha256 = process.env.CANDIDATE_DIST_SHA256;
|
||||
const publicKeyPath = kind === "vulnerability"
|
||||
? process.env.VULNERABILITY_PUBLIC_KEY_PATH
|
||||
: process.env.PROVENANCE_PUBLIC_KEY_PATH;
|
||||
const keyId = kind === "vulnerability"
|
||||
? process.env.VULNERABILITY_KEY_ID
|
||||
: process.env.PROVENANCE_KEY_ID;
|
||||
const runId = process.env.GITEA_RUN_ID ?? process.env.GITHUB_RUN_ID ?? process.env.CI_RUN_ID;
|
||||
const runAttemptSource = process.env.GITEA_RUN_ATTEMPT ??
|
||||
process.env.GITHUB_RUN_ATTEMPT ?? process.env.CI_RUN_ATTEMPT;
|
||||
const sourceRevision = process.env.EXPECTED_SOURCE_REVISION ?? process.env.VITE_COMMIT_SHA;
|
||||
if (
|
||||
!command ||
|
||||
!reportPath ||
|
||||
!sealedPath ||
|
||||
!candidateLockfile ||
|
||||
!archivePath ||
|
||||
!archiveSha256 ||
|
||||
!candidateDistSha256
|
||||
!publicKeyPath ||
|
||||
!keyId ||
|
||||
!runId ||
|
||||
!runAttemptSource ||
|
||||
!sourceRevision
|
||||
) {
|
||||
process.stderr.write("Provider supervisor environment is incomplete\n");
|
||||
process.exit(2);
|
||||
}
|
||||
const runAttempt = Number(runAttemptSource);
|
||||
if (!Number.isInteger(runAttempt) || runAttempt < 1 || runAttempt > 1_000) {
|
||||
throw new TypeError("provider supervisor run attempt is invalid");
|
||||
}
|
||||
|
||||
const workspaceRoot = process.cwd();
|
||||
const reportAbsolute = path.resolve(reportPath);
|
||||
@@ -62,22 +79,30 @@ await prepareMissingProviderOutput(workspaceRoot, sealedAbsolute, sealedPath, "s
|
||||
await access("/usr/bin/bwrap", constants.X_OK).catch(() => {
|
||||
throw new Error("provider sandbox unavailable: /usr/bin/bwrap is required");
|
||||
});
|
||||
|
||||
const childEnvironment = createProviderEnvironment(kind, reportPath, {
|
||||
candidateLockfile,
|
||||
archivePath,
|
||||
archiveSha256,
|
||||
candidateDistSha256,
|
||||
});
|
||||
await runProviderInSandbox(command, childEnvironment, rawDirectory, workspaceRoot);
|
||||
const parsed = await validateProviderUpload({
|
||||
const trust = await readProviderTrust(workspaceRoot, publicKeyPath, keyId);
|
||||
if (!trust) throw new TypeError("provider supervisor trust key is invalid");
|
||||
const supervised = await superviseProviderEvidence({
|
||||
kind,
|
||||
candidateRoot: path.dirname(path.resolve(candidateLockfile)),
|
||||
archivePath,
|
||||
expectedArchiveSha256: archiveSha256,
|
||||
reportPath,
|
||||
workspaceRoot,
|
||||
expectedDistSha256: candidateDistSha256,
|
||||
expectedRun: { id: runId, attempt: runAttempt, sourceRevision },
|
||||
trust,
|
||||
executeProvider: async ({ candidateRoot, environment }) => {
|
||||
const childEnvironment = createProviderEnvironment(kind, reportPath, environment);
|
||||
await runProviderInSandbox(
|
||||
command,
|
||||
childEnvironment,
|
||||
rawDirectory,
|
||||
workspaceRoot,
|
||||
candidateRoot,
|
||||
);
|
||||
},
|
||||
captureReport: () =>
|
||||
readBoundedRegularFile({
|
||||
root: workspaceRoot,
|
||||
relativePath: path.relative(workspaceRoot, reportAbsolute).replaceAll(path.sep, "/"),
|
||||
maxBytes: 8_388_608,
|
||||
}),
|
||||
});
|
||||
await assertSafePublishLeaf(sealedAbsolute, sealedPath);
|
||||
await writeValidatedJsonArtifact({
|
||||
@@ -86,19 +111,21 @@ await writeValidatedJsonArtifact({
|
||||
kind === "vulnerability"
|
||||
? vulnerabilityProviderReportSchema
|
||||
: provenanceProviderAttestationSchema,
|
||||
value: parsed,
|
||||
value: supervised.evidence,
|
||||
});
|
||||
if (process.env.GITHUB_OUTPUT) {
|
||||
await appendFile(
|
||||
process.env.GITHUB_OUTPUT,
|
||||
`invocation_nonce=${supervised.invocationNonce}\n`,
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
process.stdout.write(`${kind} provider supervised validation: PASS\n`);
|
||||
|
||||
function createProviderEnvironment(
|
||||
providerKind: "vulnerability" | "provenance",
|
||||
rawReportPath: string,
|
||||
candidate: Readonly<{
|
||||
candidateLockfile: string;
|
||||
archivePath: string;
|
||||
archiveSha256: string;
|
||||
candidateDistSha256: string;
|
||||
}>,
|
||||
bindings: Readonly<Record<string, string>>,
|
||||
): NodeJS.ProcessEnv {
|
||||
const environment: NodeJS.ProcessEnv = {
|
||||
PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin",
|
||||
@@ -107,10 +134,7 @@ function createProviderEnvironment(
|
||||
CI: "true",
|
||||
GITHUB_ENV: "/tmp/github-env",
|
||||
GITHUB_PATH: "/tmp/github-path",
|
||||
CANDIDATE_LOCKFILE_PATH: candidate.candidateLockfile,
|
||||
CANDIDATE_ARCHIVE_PATH: candidate.archivePath,
|
||||
CANDIDATE_ARCHIVE_SHA256: candidate.archiveSha256,
|
||||
CANDIDATE_DIST_SHA256: candidate.candidateDistSha256,
|
||||
...bindings,
|
||||
...(providerKind === "vulnerability"
|
||||
? { VULNERABILITY_REPORT_PATH: rawReportPath }
|
||||
: { PROVENANCE_ATTESTATION_PATH: rawReportPath }),
|
||||
@@ -132,6 +156,7 @@ async function runProviderInSandbox(
|
||||
environment: NodeJS.ProcessEnv,
|
||||
rawDirectory: string,
|
||||
workspaceRoot: string,
|
||||
candidateRoot: string,
|
||||
): Promise<void> {
|
||||
const scratch = await mkdtemp(path.join(tmpdir(), "ci-provider-sandbox-"));
|
||||
try {
|
||||
@@ -177,6 +202,7 @@ async function runProviderInSandbox(
|
||||
}
|
||||
arguments_.push(
|
||||
"--bind", rawDirectory, rawDirectory,
|
||||
"--ro-bind", candidateRoot, "/candidate",
|
||||
"--chdir", workspaceRoot,
|
||||
"/bin/sh", "-eu", "-c", command,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user