Add pre-install npm dependency guard

This commit is contained in:
2026-08-27 12:46:55 +01:00
parent 7574b31b8c
commit f28b9ee87d
22 changed files with 1321 additions and 137 deletions
+55
View File
@@ -0,0 +1,55 @@
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { packageNameFromLockPath } from "./util.js";
export async function readProject(projectPath) {
const manifest = await readJson(join(projectPath, "package.json"), "package.json");
const lock = await readJson(join(projectPath, "package-lock.json"), "package-lock.json");
if (![2, 3].includes(lock.lockfileVersion)) throw new Error("package-lock.json must use lockfileVersion 2 or 3");
if (!lock.packages || typeof lock.packages !== "object") throw new Error("package-lock.json has no packages map");
const requested = { ...(manifest.dependencies ?? {}), ...(manifest.devDependencies ?? {}) };
const direct = [];
for (const name of Object.keys(requested).sort()) {
const entry = lock.packages[`node_modules/${name}`];
if (!entry || typeof entry.version !== "string" || entry.link) {
throw new Error(`cannot resolve exact direct version for ${name} from package-lock.json`);
}
direct.push({ name, version: entry.version, dev: name in (manifest.devDependencies ?? {}) });
}
return { manifest, lock, direct };
}
export function inspectLock(lock, policy) {
const failures = [];
let registryEntries = 0;
for (const [lockPath, entry] of Object.entries(lock.packages)) {
if (!lockPath || entry.link) continue;
const name = packageNameFromLockPath(lockPath, entry) ?? lockPath;
if (entry.hasInstallScript && !policy.allowInstallScripts.includes(name)) {
failures.push(`${name}: lock entry declares hasInstallScript; add a reviewed allowInstallScripts exception if intentional`);
}
if (!lockPath.includes("node_modules/")) continue;
registryEntries += 1;
if (typeof entry.integrity !== "string" || !/^sha(256|384|512)-[A-Za-z0-9+/=]+$/.test(entry.integrity)) {
failures.push(`${name}: lock entry has no valid sha256/sha384/sha512 integrity`);
}
try {
const resolved = new URL(entry.resolved);
if (resolved.protocol !== "https:" || resolved.hostname !== "registry.npmjs.org") {
failures.push(`${name}: lock entry is not resolved from https://registry.npmjs.org`);
}
} catch {
failures.push(`${name}: lock entry has no valid registry resolved URL`);
}
}
return { failures, registryEntries };
}
async function readJson(file, label) {
try {
return JSON.parse(await readFile(file, "utf8"));
} catch (error) {
throw new Error(`cannot parse ${label}: ${error.message}`);
}
}