Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/publish-npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,11 @@ jobs:
--file "${ARTIFACT_DIRECTORY}/npm-11.18.0.tgz" \
--directory "${audit_directory}"

cd "${audit_directory}/package"
npm shrinkwrap --ignore-scripts
npm audit --omit=dev --audit-level=moderate
(
cd "${audit_directory}/package"
npm shrinkwrap --ignore-scripts
)
bun run scripts/audit-pinned-npm.ts "${audit_directory}/package"

- name: Record npm CLI checksum
if: steps.registry.outputs.release_needed == 'true'
Expand Down
99 changes: 99 additions & 0 deletions apps/cli/src/release/pinned-npm-audit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { describe, expect, it } from "vitest";

import { PINNED_NPM_AUDIT_EXPIRES_AT, PINNED_NPM_VERSION, validatePinnedNpmAudit } from "./pinned-npm-audit.js";

const knownAuditReport = {
vulnerabilities: {
"brace-expansion": {
nodes: ["node_modules/brace-expansion"],
severity: "high",
via: [
{ url: "https://github.com/advisories/GHSA-mh99-v99m-4gvg" },
{ url: "https://github.com/advisories/GHSA-rgw5-rvv9-x895" },
],
},
"ip-address": {
nodes: ["node_modules/ip-address"],
severity: "high",
via: [
{ url: "https://github.com/advisories/GHSA-mwp4-54f8-5fhr" },
{ url: "https://github.com/advisories/GHSA-4xrf-jv44-h6hh" },
{ url: "https://github.com/advisories/GHSA-22jq-vg5j-6vgg" },
],
},
tar: {
nodes: ["node_modules/tar"],
severity: "moderate",
via: [{ url: "https://github.com/advisories/GHSA-r292-9mhp-454m" }],
},
undici: {
nodes: ["node_modules/undici"],
severity: "moderate",
via: [
{ url: "https://github.com/advisories/GHSA-8xcm-r25x-g524" },
{ url: "https://github.com/advisories/GHSA-m8rv-5g2x-5cg5" },
{ url: "https://github.com/advisories/GHSA-v3r7-h72x-cjcm" },
],
},
},
};

const knownVersions = {
"node_modules/brace-expansion": "5.0.7",
"node_modules/ip-address": "10.2.0",
"node_modules/tar": "7.5.19",
"node_modules/undici": "6.27.0",
};

const validate = (
report: unknown = knownAuditReport,
versionsByNode: Readonly<Record<string, string>> = knownVersions,
now = new Date("2026-08-10T00:00:00.000Z"),
actualNpmVersion: string = PINNED_NPM_VERSION,
) =>
validatePinnedNpmAudit({
actualNpmVersion,
now,
report,
versionsByNode,
});

describe("pinned npm CLI audit policy", () => {
it("accepts only the known findings for their exact bundled versions", () => {
expect(validate()).toHaveLength(9);
});

it("rejects a newly reported advisory", () => {
const report = structuredClone(knownAuditReport);
report.vulnerabilities.undici.via.push({
url: "https://github.com/advisories/GHSA-new0-new0-new0",
});

expect(() => validate(report)).toThrow(/Unexpected npm CLI audit finding.*GHSA-new0-new0-new0/s);
});

it("rejects an allowed advisory when the installed version changes", () => {
expect(() =>
validate(knownAuditReport, {
...knownVersions,
"node_modules/tar": "7.5.20",
}),
).toThrow(/Unexpected npm CLI audit finding.*tar@7\.5\.20/s);
});

it("expires the temporary exceptions", () => {
expect(() => validate(knownAuditReport, knownVersions, new Date(PINNED_NPM_AUDIT_EXPIRES_AT))).toThrow(
/Temporary npm CLI audit exceptions expired/,
);
});

it("allows a clean report after the exception expiry", () => {
expect(validate({ vulnerabilities: {} }, {}, new Date("2027-01-01T00:00:00.000Z"))).toEqual([]);
});

it("rejects a different npm CLI version", () => {
expect(() => validate(knownAuditReport, knownVersions, undefined, "11.19.0")).toThrow(
/npm CLI version 11\.19\.0 does not match audited version 11\.18\.0/,
);
});
});
160 changes: 160 additions & 0 deletions apps/cli/src/release/pinned-npm-audit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
export const PINNED_NPM_VERSION = "11.18.0" as const;
export const PINNED_NPM_AUDIT_EXPIRES_AT = "2026-08-24T00:00:00.000Z" as const;

type AuditException = {
readonly advisoryId: string;
readonly packageName: string;
readonly version: string;
};

// npm 11.18.0 bundles these versions, and no released Node 22-compatible npm
// CLI contains all upstream fixes yet. This policy is limited to the official
// pinned CLI used to stage a checksummed Artiflow tarball and expires below.
const auditExceptions: ReadonlyArray<AuditException> = [
{
advisoryId: "GHSA-mh99-v99m-4gvg",
packageName: "brace-expansion",
version: "5.0.7",
},
{
advisoryId: "GHSA-rgw5-rvv9-x895",
packageName: "brace-expansion",
version: "5.0.7",
},
{
advisoryId: "GHSA-mwp4-54f8-5fhr",
packageName: "ip-address",
version: "10.2.0",
},
{
advisoryId: "GHSA-4xrf-jv44-h6hh",
packageName: "ip-address",
version: "10.2.0",
},
{
advisoryId: "GHSA-22jq-vg5j-6vgg",
packageName: "ip-address",
version: "10.2.0",
},
{
advisoryId: "GHSA-r292-9mhp-454m",
packageName: "tar",
version: "7.5.19",
},
{
advisoryId: "GHSA-8xcm-r25x-g524",
packageName: "undici",
version: "6.27.0",
},
{
advisoryId: "GHSA-m8rv-5g2x-5cg5",
packageName: "undici",
version: "6.27.0",
},
{
advisoryId: "GHSA-v3r7-h72x-cjcm",
packageName: "undici",
version: "6.27.0",
},
];

const severityRank = {
critical: 4,
high: 3,
low: 1,
moderate: 2,
} as const;

const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
typeof value === "object" && value !== null && !Array.isArray(value);

const exceptionKey = ({ advisoryId, packageName, version }: AuditException) =>
`${packageName}@${version}:${advisoryId}`;

const exceptionKeys = new Set(auditExceptions.map(exceptionKey));

const advisoryIdFrom = (value: unknown): string | undefined => {
if (!isRecord(value) || typeof value.url !== "string") return undefined;

try {
const advisoryId = new URL(value.url).pathname.split("/").at(-1);
return advisoryId?.match(/^GHSA-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}$/i)?.[0];
} catch {
return undefined;
}
};

export const validatePinnedNpmAudit = ({
actualNpmVersion,
now,
report,
versionsByNode,
}: {
readonly actualNpmVersion: string;
readonly now: Date;
readonly report: unknown;
readonly versionsByNode: Readonly<Record<string, string>>;
}): ReadonlyArray<string> => {
if (actualNpmVersion !== PINNED_NPM_VERSION) {
throw new Error(`npm CLI version ${actualNpmVersion} does not match audited version ${PINNED_NPM_VERSION}.`);
}

if (!isRecord(report) || !isRecord(report.vulnerabilities)) {
throw new Error("npm audit returned a malformed vulnerabilities report.");
}

const accepted: Array<string> = [];
const unexpected: Array<string> = [];

for (const [packageName, value] of Object.entries(report.vulnerabilities)) {
if (!isRecord(value) || typeof value.severity !== "string") {
throw new Error(`npm audit returned a malformed finding for ${packageName}.`);
}

const rank = severityRank[value.severity as keyof typeof severityRank];
if (rank === undefined) {
throw new Error(`npm audit returned an unknown severity for ${packageName}: ${value.severity}.`);
}
if (rank < severityRank.moderate) continue;

if (!Array.isArray(value.nodes) || value.nodes.length === 0) {
throw new Error(`npm audit did not identify an installed node for ${packageName}.`);
}
if (!Array.isArray(value.via) || value.via.length === 0) {
throw new Error(`npm audit did not identify an advisory for ${packageName}.`);
}

const advisoryIds = value.via.map(advisoryIdFrom);
if (advisoryIds.some((advisoryId) => advisoryId === undefined)) {
throw new Error(`npm audit returned an indirect or malformed advisory for ${packageName}.`);
}

for (const node of value.nodes) {
if (typeof node !== "string") {
throw new Error(`npm audit returned a malformed installed node for ${packageName}.`);
}

const version = versionsByNode[node];
if (version === undefined) {
throw new Error(`Could not resolve the installed version for ${packageName} at ${node}.`);
}

for (const advisoryId of advisoryIds as ReadonlyArray<string>) {
const finding = { advisoryId, packageName, version };
const summary = `${advisoryId} ${packageName}@${version} (${node})`;
if (exceptionKeys.has(exceptionKey(finding))) accepted.push(summary);
else unexpected.push(summary);
}
}
}

if (unexpected.length > 0) {
throw new Error(`Unexpected npm CLI audit finding(s):\n${unexpected.sort().join("\n")}`);
}

if (accepted.length > 0 && now.getTime() >= Date.parse(PINNED_NPM_AUDIT_EXPIRES_AT)) {
throw new Error(`Temporary npm CLI audit exceptions expired at ${PINNED_NPM_AUDIT_EXPIRES_AT}.`);
}

return accepted.sort();
};
99 changes: 99 additions & 0 deletions scripts/audit-pinned-npm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { resolve, sep } from "node:path";

import {
PINNED_NPM_AUDIT_EXPIRES_AT,
PINNED_NPM_VERSION,
validatePinnedNpmAudit,
} from "../apps/cli/src/release/pinned-npm-audit.js";

const requestedDirectory = process.argv[2];
if (requestedDirectory === undefined) {
throw new Error("Usage: bun run scripts/audit-pinned-npm.ts <extracted-npm-directory>");
}

const auditDirectory = resolve(requestedDirectory);
const readJson = (path: string): unknown => JSON.parse(readFileSync(path, "utf8"));
const packageManifest = readJson(resolve(auditDirectory, "package.json"));

if (
typeof packageManifest !== "object" ||
packageManifest === null ||
!("name" in packageManifest) ||
!("version" in packageManifest) ||
packageManifest.name !== "npm" ||
typeof packageManifest.version !== "string"
) {
throw new Error("Extracted npm CLI has an invalid package manifest.");
}

const audit = spawnSync("npm", ["audit", "--omit=dev", "--audit-level=moderate", "--json"], {
cwd: auditDirectory,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});

if (audit.error !== undefined) {
throw new Error(`Could not execute npm audit: ${audit.error.message}`);
}
if (audit.status !== 0 && audit.status !== 1) {
throw new Error(`npm audit failed to produce a vulnerability report:\n${audit.stderr}`);
}

const jsonStart = audit.stdout.indexOf("{");
if (jsonStart === -1) {
throw new Error(`Could not parse npm audit output:\n${audit.stdout}\n${audit.stderr}`);
}

const report = JSON.parse(audit.stdout.slice(jsonStart)) as unknown;
const versionsByNode: Record<string, string> = {};

if (typeof report === "object" && report !== null && "vulnerabilities" in report) {
const vulnerabilities = report.vulnerabilities;
if (typeof vulnerabilities === "object" && vulnerabilities !== null) {
for (const value of Object.values(vulnerabilities)) {
if (typeof value !== "object" || value === null || !("nodes" in value) || !Array.isArray(value.nodes)) {
continue;
}

for (const node of value.nodes) {
if (typeof node !== "string") continue;

const nodeDirectory = resolve(auditDirectory, node);
if (!nodeDirectory.startsWith(`${auditDirectory}${sep}`)) {
throw new Error(`npm audit reported a node outside the extracted package: ${node}.`);
}

const nodeManifest = readJson(resolve(nodeDirectory, "package.json"));
if (
typeof nodeManifest !== "object" ||
nodeManifest === null ||
!("version" in nodeManifest) ||
typeof nodeManifest.version !== "string"
) {
throw new Error(`Installed npm dependency has an invalid package manifest: ${node}.`);
}
versionsByNode[node] = nodeManifest.version;
}
}
}
}

const accepted = validatePinnedNpmAudit({
actualNpmVersion: packageManifest.version,
now: new Date(),
report,
versionsByNode,
});

if (accepted.length === 0) {
console.log(`Pinned npm CLI ${PINNED_NPM_VERSION} audit passed without exceptions.`);
} else {
console.warn(
[
`Pinned npm CLI ${PINNED_NPM_VERSION} audit accepted temporary exceptions until ${PINNED_NPM_AUDIT_EXPIRES_AT}:`,
...accepted.map((finding) => `- ${finding}`),
].join("\n"),
);
}