Add pre-install npm dependency guard
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { DEFAULT_POLICY } from "../lib/constants.js";
|
||||
import { verifyDirectPackage } from "../lib/verify.js";
|
||||
import { jsonResponse, packageDocument } from "./helpers.js";
|
||||
|
||||
const item = { name: "alpha", version: "1.2.3", dev: false };
|
||||
const now = () => Date.parse("2025-01-01T00:00:00Z");
|
||||
|
||||
test("verifies SLSA source repository, tag ref, and existing commit", async () => {
|
||||
const commit = "abcdef1234567890abcdef1234567890abcdef12";
|
||||
const statement = {
|
||||
predicate: {
|
||||
buildDefinition: {
|
||||
externalParameters: { workflow: { repository: "https://github.com/example/alpha", ref: "refs/tags/v1.2.3" } },
|
||||
resolvedDependencies: [{ uri: "git+https://github.com/example/alpha@refs/tags/v1.2.3", digest: { gitCommit: commit } }],
|
||||
},
|
||||
},
|
||||
};
|
||||
const document = packageDocument({ version: { dist: {
|
||||
signatures: [{ keyid: "key", sig: "sig" }],
|
||||
attestations: {
|
||||
url: "https://registry.npmjs.org/-/npm/v1/attestations/alpha@1.2.3",
|
||||
provenance: { predicateType: "https://slsa.dev/provenance/v1" },
|
||||
},
|
||||
} } });
|
||||
const fetch = async (url) => {
|
||||
if (url.includes("attestations")) return jsonResponse({ attestations: [{ predicateType: "https://slsa.dev/provenance/v1", bundle: { dsseEnvelope: { payload: Buffer.from(JSON.stringify(statement)).toString("base64") } } }] });
|
||||
throw new Error(`unexpected URL ${url}`);
|
||||
};
|
||||
const result = await verifyDirectPackage(item, document, structuredClone(DEFAULT_POLICY), {
|
||||
fetchImpl: fetch,
|
||||
remoteRefs: async () => [{ sha: commit, ref: "refs/tags/v1.2.3" }],
|
||||
now,
|
||||
timeoutMs: 100,
|
||||
});
|
||||
assert.deepEqual(result.failures, []);
|
||||
assert.equal(result.provenance, "slsa");
|
||||
});
|
||||
|
||||
test("accepts branch-built provenance only when a version tag pins its commit", async () => {
|
||||
const commit = "abcdef1234567890abcdef1234567890abcdef12";
|
||||
const statement = {
|
||||
predicate: {
|
||||
buildDefinition: {
|
||||
externalParameters: { workflow: { repository: "https://github.com/example/alpha", ref: "refs/heads/main" } },
|
||||
resolvedDependencies: [{ uri: "git+https://github.com/example/alpha@refs/heads/main", digest: { gitCommit: commit } }],
|
||||
},
|
||||
},
|
||||
};
|
||||
const document = packageDocument({ version: { dist: {
|
||||
signatures: [{ keyid: "key", sig: "sig" }],
|
||||
attestations: {
|
||||
url: "https://registry.npmjs.org/-/npm/v1/attestations/alpha@1.2.3",
|
||||
provenance: { predicateType: "https://slsa.dev/provenance/v1" },
|
||||
},
|
||||
} } });
|
||||
const result = await verifyDirectPackage(item, document, structuredClone(DEFAULT_POLICY), {
|
||||
fetchImpl: async () => jsonResponse({ attestations: [{ predicateType: "https://slsa.dev/provenance/v1", bundle: { dsseEnvelope: { payload: Buffer.from(JSON.stringify(statement)).toString("base64") } } }] }),
|
||||
remoteRefs: async () => [{ sha: commit, ref: "refs/tags/v1.2.3" }],
|
||||
now,
|
||||
timeoutMs: 100,
|
||||
});
|
||||
assert.deepEqual(result.failures, []);
|
||||
});
|
||||
|
||||
test("rejects branch-built provenance when no version tag pins its commit", async () => {
|
||||
const commit = "abcdef1234567890abcdef1234567890abcdef12";
|
||||
const statement = {
|
||||
predicate: {
|
||||
buildDefinition: {
|
||||
externalParameters: { workflow: { repository: "https://github.com/example/alpha", ref: "refs/heads/main" } },
|
||||
resolvedDependencies: [{ uri: "git+https://github.com/example/alpha@refs/heads/main", digest: { gitCommit: commit } }],
|
||||
},
|
||||
},
|
||||
};
|
||||
const document = packageDocument({ version: { dist: {
|
||||
signatures: [{ keyid: "key", sig: "sig" }],
|
||||
attestations: {
|
||||
url: "https://registry.npmjs.org/-/npm/v1/attestations/alpha@1.2.3",
|
||||
provenance: { predicateType: "https://slsa.dev/provenance/v1" },
|
||||
},
|
||||
} } });
|
||||
const result = await verifyDirectPackage(item, document, structuredClone(DEFAULT_POLICY), {
|
||||
fetchImpl: async () => jsonResponse({ attestations: [{ predicateType: "https://slsa.dev/provenance/v1", bundle: { dsseEnvelope: { payload: Buffer.from(JSON.stringify(statement)).toString("base64") } } }] }),
|
||||
remoteRefs: async () => [],
|
||||
now,
|
||||
timeoutMs: 100,
|
||||
});
|
||||
assert.ok(result.failures.some((message) => message.includes("no Git tag containing 1.2.3")));
|
||||
});
|
||||
|
||||
test("fallback accepts an annotated version tag dereferenced to gitHead", async () => {
|
||||
const document = packageDocument();
|
||||
const gitHead = document.versions["1.2.3"].gitHead;
|
||||
const result = await verifyDirectPackage(item, document, structuredClone(DEFAULT_POLICY), {
|
||||
remoteRefs: async () => [{ sha: gitHead, ref: "refs/tags/v1.2.3^{}" }],
|
||||
now,
|
||||
timeoutMs: 100,
|
||||
});
|
||||
assert.deepEqual(result.failures, []);
|
||||
assert.equal(result.provenance, "gitHead-tag");
|
||||
});
|
||||
|
||||
test("rejects direct lifecycle scripts and untrusted maintainers", async () => {
|
||||
const document = packageDocument({ version: { scripts: { postinstall: "node setup.js" }, maintainers: [{ name: "mallory" }] } });
|
||||
const policy = structuredClone(DEFAULT_POLICY);
|
||||
policy.trustedMaintainers.alpha = ["alice"];
|
||||
const result = await verifyDirectPackage(item, document, policy, {
|
||||
remoteRefs: async () => [{ sha: document.versions["1.2.3"].gitHead, ref: "refs/tags/alpha-1.2.3" }],
|
||||
now,
|
||||
timeoutMs: 100,
|
||||
});
|
||||
assert.ok(result.failures.some((message) => message.includes("lifecycle hook")));
|
||||
assert.ok(result.failures.some((message) => message.includes("registry maintainer set changed")));
|
||||
});
|
||||
|
||||
test("rejects a missing previously trusted maintainer", async () => {
|
||||
const document = packageDocument({ version: { maintainers: [{ name: "alice" }] } });
|
||||
const policy = structuredClone(DEFAULT_POLICY);
|
||||
policy.trustedMaintainers.alpha = ["alice", "bob"];
|
||||
const gitHead = document.versions["1.2.3"].gitHead;
|
||||
const result = await verifyDirectPackage(item, document, policy, {
|
||||
remoteRefs: async () => [{ sha: gitHead, ref: "refs/tags/v1.2.3" }],
|
||||
now,
|
||||
timeoutMs: 100,
|
||||
});
|
||||
assert.ok(result.failures.some((message) => message.includes("registry maintainer set changed")));
|
||||
});
|
||||
|
||||
test("accepts only a version-pinned unexpired source evidence exception", async () => {
|
||||
const document = packageDocument({ version: { gitHead: null } });
|
||||
const policy = structuredClone(DEFAULT_POLICY);
|
||||
policy.sourceEvidenceExceptions.alpha = {
|
||||
version: "1.2.3",
|
||||
reason: "Upstream does not publish version tags for this package.",
|
||||
expiresAt: "2025-02-01T00:00:00Z",
|
||||
};
|
||||
const result = await verifyDirectPackage(item, document, policy, { now, timeoutMs: 100 });
|
||||
assert.deepEqual(result.failures, []);
|
||||
assert.equal(result.provenance, "policy-exception");
|
||||
assert.ok(result.warnings.some((message) => message.includes("reviewed sourceEvidenceExceptions")));
|
||||
});
|
||||
|
||||
test("rejects an expired source evidence exception", async () => {
|
||||
const document = packageDocument({ version: { gitHead: null } });
|
||||
const policy = structuredClone(DEFAULT_POLICY);
|
||||
policy.sourceEvidenceExceptions.alpha = {
|
||||
version: "1.2.3",
|
||||
reason: "Upstream does not publish version tags for this package.",
|
||||
expiresAt: "2024-12-31T00:00:00Z",
|
||||
};
|
||||
const result = await verifyDirectPackage(item, document, policy, { now, timeoutMs: 100 });
|
||||
assert.ok(result.failures.some((message) => message.includes("gitHead is missing")));
|
||||
});
|
||||
Reference in New Issue
Block a user