58 lines
2.7 KiB
JavaScript
58 lines
2.7 KiB
JavaScript
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, now = Date.now()) {
|
|
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;
|
|
const scriptException = policy.allowInstallScripts[name];
|
|
const scriptAllowed = scriptException?.versions.includes(entry.version) && Date.parse(scriptException.expiresAt) > now;
|
|
if (entry.hasInstallScript && !scriptAllowed) {
|
|
failures.push(`${name}@${entry.version ?? "unknown"}: lock entry declares hasInstallScript; add a reviewed, version-pinned 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}`);
|
|
}
|
|
}
|