fix: close release provenance and privacy gaps
This commit is contained in:
@@ -6,9 +6,11 @@ import {
|
||||
symlink,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { canonicalJsonDigest } from "../../src/security/canonical-digest.ts";
|
||||
|
||||
// @ts-expect-error executable release modules intentionally have no browser declarations
|
||||
import { enumerateClosure } from "../../scripts/release/enumerate-closure.mjs";
|
||||
@@ -16,10 +18,24 @@ import { enumerateClosure } from "../../scripts/release/enumerate-closure.mjs";
|
||||
import * as privacyRunner from "../../scripts/release/run-privacy-workers.mjs";
|
||||
// @ts-expect-error executable release modules intentionally have no browser declarations
|
||||
import { reconcileScan } from "../../scripts/release/reconcile-scan.mjs";
|
||||
// @ts-expect-error container worker is executable JavaScript
|
||||
import { inspectObject } from "../../containers/privacy-worker-a/worker.mjs";
|
||||
// @ts-expect-error container matcher is executable JavaScript
|
||||
import { matchReports } from "../../containers/privacy-matcher/matcher.mjs";
|
||||
|
||||
const { buildWorkerCommand, validateScanObjectDescriptor } = privacyRunner;
|
||||
const {
|
||||
buildWorkerCommand,
|
||||
validateScanObjectDescriptor,
|
||||
requireLiveContainerValidation,
|
||||
} = privacyRunner;
|
||||
|
||||
const h = (c: string) => c.repeat(64);
|
||||
const sourceClosure = (members: unknown[] = []) => {
|
||||
return { schema: "portfolio-publication-closure/v1", members };
|
||||
};
|
||||
const publicClosure = (members: unknown[] = []) => {
|
||||
return { schema: "portfolio-public-object-closure/v1", members };
|
||||
};
|
||||
|
||||
function report(
|
||||
workerId: "worker-a" | "worker-b",
|
||||
@@ -56,6 +72,124 @@ function matcher(override: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe("isolated privacy closure and reconciliation", () => {
|
||||
test("fails closed while live scanners are pending and for every non-empty public object closure", () => {
|
||||
expect(() =>
|
||||
requireLiveContainerValidation({
|
||||
schema: "portfolio-live-container-validation/v1",
|
||||
status: "pending",
|
||||
reason: "not verified",
|
||||
}),
|
||||
).toThrow(/pending|verified/i);
|
||||
const canary = Buffer.from("PRIVATE-PROJECT-NAME\nTOKEN=secret-canary");
|
||||
const inputDigest = createHash("sha256").update(canary).digest("hex");
|
||||
expect(
|
||||
inspectObject(
|
||||
{
|
||||
schema: "portfolio-privacy-worker-request/v1",
|
||||
workerId: "worker-a",
|
||||
imageDigest: h("a"),
|
||||
inputDigest,
|
||||
checkId: "secret",
|
||||
},
|
||||
canary,
|
||||
).verdict,
|
||||
).not.toBe("pass");
|
||||
const pythonReport = report("worker-b", {
|
||||
inputDigest,
|
||||
checkId: "secret",
|
||||
verdict: "unsupported",
|
||||
});
|
||||
expect(() =>
|
||||
matchReports({
|
||||
reports: [
|
||||
inspectObject(
|
||||
{
|
||||
schema: "portfolio-privacy-worker-request/v1",
|
||||
workerId: "worker-a",
|
||||
imageDigest: h("a"),
|
||||
inputDigest,
|
||||
checkId: "secret",
|
||||
},
|
||||
canary,
|
||||
),
|
||||
pythonReport,
|
||||
],
|
||||
rules: [{ id: "private-name-v1", deniedMemberDigests: [] }],
|
||||
ruleSetDigest: h("4"),
|
||||
}),
|
||||
).toThrow(/pass|unsupported|agree/i);
|
||||
const source = sourceClosure();
|
||||
const publicObjectClosure = publicClosure([
|
||||
{
|
||||
objectId: "private-leak.txt",
|
||||
inputDigest,
|
||||
requiredChecks: ["secret"],
|
||||
},
|
||||
]);
|
||||
expect(() =>
|
||||
reconcileScan({
|
||||
sourceClosure: source,
|
||||
publicObjectClosure,
|
||||
reports: [
|
||||
report("worker-a", { inputDigest, checkId: "secret" }),
|
||||
report("worker-b", { inputDigest, checkId: "secret" }),
|
||||
],
|
||||
matcherResults: [matcher({ inputDigest, checkId: "secret" })],
|
||||
candidate: {
|
||||
schema: "portfolio-candidate-release-manifest/v1",
|
||||
buildId: "r1",
|
||||
sourceClosureDigest: canonicalJsonDigest(
|
||||
"portfolio-publication-closure/v1",
|
||||
{ schema: source.schema, members: source.members },
|
||||
),
|
||||
publicObjectClosureDigest: canonicalJsonDigest(
|
||||
"portfolio-public-object-closure/v1",
|
||||
publicObjectClosure,
|
||||
),
|
||||
},
|
||||
}),
|
||||
).toThrow(/unavailable|non-empty|scanner/i);
|
||||
});
|
||||
|
||||
test("derives exact scan coverage from canonical closures instead of caller expectedChecks", () => {
|
||||
const source = sourceClosure();
|
||||
const publicObjectClosure = publicClosure();
|
||||
const candidate = {
|
||||
schema: "portfolio-candidate-release-manifest/v1",
|
||||
buildId: "r1",
|
||||
sourceClosureDigest: canonicalJsonDigest(
|
||||
"portfolio-publication-closure/v1",
|
||||
source,
|
||||
),
|
||||
publicObjectClosureDigest: canonicalJsonDigest(
|
||||
"portfolio-public-object-closure/v1",
|
||||
publicObjectClosure,
|
||||
),
|
||||
};
|
||||
const result = reconcileScan({
|
||||
sourceClosure: source,
|
||||
publicObjectClosure,
|
||||
reports: [],
|
||||
matcherResults: [],
|
||||
candidate,
|
||||
});
|
||||
expect(result.evidence).toMatchObject({
|
||||
coverageCount: 0,
|
||||
passedCount: 0,
|
||||
sourceClosureDigest: candidate.sourceClosureDigest,
|
||||
publicObjectClosureDigest: candidate.publicObjectClosureDigest,
|
||||
});
|
||||
expect(() =>
|
||||
reconcileScan({
|
||||
sourceClosure: source,
|
||||
publicObjectClosure,
|
||||
expectedChecks: [{ inputDigest: h("1"), checkId: "bytes" }],
|
||||
reports: [],
|
||||
matcherResults: [],
|
||||
candidate,
|
||||
}),
|
||||
).toThrow(/missing|extra|unknown|caller|expectedChecks/i);
|
||||
});
|
||||
test("enumerates an exact byte-sorted closure and rejects unsafe filesystem members", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "portfolio-closure-"));
|
||||
await mkdir(join(root, "nested"));
|
||||
@@ -142,14 +276,25 @@ describe("isolated privacy closure and reconciliation", () => {
|
||||
).toThrowError(expect.not.stringContaining(secret));
|
||||
});
|
||||
|
||||
test("accepts only ordered heterogeneous agreement for every expected check", () => {
|
||||
test("accepts only the exact empty public object closure while scanners are unavailable", () => {
|
||||
const source = sourceClosure();
|
||||
const publicObjectClosure = publicClosure();
|
||||
const result = reconcileScan({
|
||||
expectedChecks: [{ inputDigest: h("1"), checkId: "bytes" }],
|
||||
reports: [report("worker-a"), report("worker-b")],
|
||||
matcherResults: [matcher()],
|
||||
sourceClosure: source,
|
||||
publicObjectClosure,
|
||||
reports: [],
|
||||
matcherResults: [],
|
||||
candidate: {
|
||||
schema: "portfolio-candidate-release-manifest/v1",
|
||||
buildId: "r1",
|
||||
sourceClosureDigest: canonicalJsonDigest(
|
||||
"portfolio-publication-closure/v1",
|
||||
source,
|
||||
),
|
||||
publicObjectClosureDigest: canonicalJsonDigest(
|
||||
"portfolio-public-object-closure/v1",
|
||||
publicObjectClosure,
|
||||
),
|
||||
},
|
||||
});
|
||||
expect(result.evidenceDigest).toMatch(/^[a-f0-9]{64}$/);
|
||||
@@ -217,13 +362,24 @@ describe("isolated privacy closure and reconciliation", () => {
|
||||
});
|
||||
|
||||
test("writes canonical evidence and candidate objects content-addressed without overwrite", async () => {
|
||||
const source = sourceClosure();
|
||||
const publicObjectClosure = publicClosure();
|
||||
const result = reconcileScan({
|
||||
expectedChecks: [{ inputDigest: h("1"), checkId: "bytes" }],
|
||||
reports: [report("worker-a"), report("worker-b")],
|
||||
matcherResults: [matcher()],
|
||||
sourceClosure: source,
|
||||
publicObjectClosure,
|
||||
reports: [],
|
||||
matcherResults: [],
|
||||
candidate: {
|
||||
schema: "portfolio-candidate-release-manifest/v1",
|
||||
buildId: "r1",
|
||||
sourceClosureDigest: canonicalJsonDigest(
|
||||
"portfolio-publication-closure/v1",
|
||||
source,
|
||||
),
|
||||
publicObjectClosureDigest: canonicalJsonDigest(
|
||||
"portfolio-public-object-closure/v1",
|
||||
publicObjectClosure,
|
||||
),
|
||||
},
|
||||
});
|
||||
const root = await mkdtemp(join(tmpdir(), "portfolio-evidence-"));
|
||||
|
||||
Reference in New Issue
Block a user