#!/usr/bin/env node // scripts/check/check-lockfile.mjs // Gate de política de lockfile (CLAUDE.md — extensão Hard Rule #1). // // Objetivo: detectar supply-chain poisoning no package-lock.json antes que código // malicioso entre no repo. Verifica: // --validate-https → toda URL "resolved" deve usar HTTPS (bloqueia http://) // --validate-integrity → todo pacote deve ter hash de integridade sha512 // --allowed-hosts npm → apenas registry.npmjs.org é host permitido // npm ls --workspaces → entradas do lockfile satisfazem cada workspace // // Complementa check-deps (Fase 2 / allowlist de nomes): aquele garante que só // nomes aprovados entram; este garante que os pacotes instalados vieram do registry // legítimo com integridade verificável. // // Referência: PLANO-QUALITY-GATES-FASE7.md, Task 7.7. // Tool: lockfile-lint v5 (node_modules/.bin/lockfile-lint). import { execFileSync } from "node:child_process"; import path from "node:path"; import fs from "node:fs"; import { pathToFileURL } from "node:url"; const ROOT = process.cwd(); /** * Returns the canonical lockfile-lint configuration used by this gate. * Exporting this object makes the policy auditable and unit-testable without * spawning a child process. * * @returns {{ * lockfilePath: string, * type: string, * validateHttps: boolean, * validateIntegrity: boolean, * allowedHosts: string[], * }} */ export function getLockfileLintConfig() { return { lockfilePath: path.join(ROOT, "package-lock.json"), type: "npm", validateHttps: true, validateIntegrity: true, // Only the official npm registry is permitted. // registry.npmjs.org resolves to the "npm" shorthand in lockfile-lint. // If the project ever adopts a scoped/private registry, add its hostname here // and document the justification. allowedHosts: ["npm"], }; } /** * Builds the argv array to pass to the lockfile-lint binary, derived from * the config returned by getLockfileLintConfig(). * * @param {ReturnType} cfg * @returns {string[]} */ export function buildLockfileLintArgs(cfg) { const args = ["--path", cfg.lockfilePath, "--type", cfg.type]; if (cfg.validateHttps) args.push("--validate-https"); if (cfg.validateIntegrity) args.push("--validate-integrity"); if (cfg.allowedHosts.length) { args.push("--allowed-hosts", ...cfg.allowedHosts); } return args; } /** * Returns the cross-platform npm command that verifies direct workspace * dependencies from package-lock.json, independent of the installed tree. * * @param {NodeJS.Platform} [platform] * @param {string | undefined} [comSpec] * @returns {{ command: string, args: string[] }} */ export function getWorkspaceDependencyCheckCommand( platform = process.platform, comSpec = process.env.ComSpec ) { const npmArgs = ["ls", "--workspaces", "--depth=0", "--package-lock-only"]; if (platform === "win32") { return { command: comSpec || "cmd.exe", args: ["/d", "/s", "/c", "npm.cmd", ...npmArgs], }; } return { command: "npm", args: npmArgs, }; } /** * Executes the workspace dependency consistency check while keeping the process * boundary injectable for deterministic success and failure tests. * * @param {{ * platform?: NodeJS.Platform, * comSpec?: string, * execFile?: typeof execFileSync, * }} [options] * @returns {{ ok: true } | { ok: false, stdout: string, stderr: string }} */ export function runWorkspaceDependencyCheck(options = {}) { const { platform = process.platform, comSpec = process.env.ComSpec, execFile = execFileSync, } = options; const workspaceCheck = getWorkspaceDependencyCheckCommand(platform, comSpec); try { execFile(workspaceCheck.command, workspaceCheck.args, { cwd: ROOT, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }); return { ok: true }; } catch (err) { return { ok: false, stdout: err.stdout ?? "", stderr: err.stderr ?? "", }; } } function main() { const cfg = getLockfileLintConfig(); if (!fs.existsSync(cfg.lockfilePath)) { console.error( `[check-lockfile] FAIL — lockfile not found: ${cfg.lockfilePath}\n` + " → Run `npm install` to generate package-lock.json" ); process.exit(1); } const bin = path.join(ROOT, "node_modules", ".bin", "lockfile-lint"); if (!fs.existsSync(bin)) { console.error( `[check-lockfile] FAIL — lockfile-lint binary not found at:\n ${bin}\n` + " → Run `npm install` to install dev dependencies" ); process.exit(1); } const args = buildLockfileLintArgs(cfg); try { const output = execFileSync(bin, args, { encoding: "utf8" }); // lockfile-lint outputs a green ✔ message on success console.log("[check-lockfile] OK —", output.trim()); } catch (err) { const stdout = err.stdout ?? ""; const stderr = err.stderr ?? ""; console.error("[check-lockfile] FAIL — lockfile-lint found policy violations:"); if (stdout) console.error(stdout); if (stderr) console.error(stderr); console.error( "\n Possible causes:\n" + " • A package was resolved from a non-HTTPS URL (http:// poisoning attempt)\n" + " • A package is missing its integrity hash (tampered or legacy entry)\n" + " • A package was resolved from a host other than registry.npmjs.org\n" + " If a scoped/private registry is intentionally used, add its hostname\n" + " to getLockfileLintConfig().allowedHosts in scripts/check/check-lockfile.mjs" ); process.exit(1); } const workspaceResult = runWorkspaceDependencyCheck(); if (workspaceResult.ok) { console.log("[check-lockfile] OK — workspace lock entries match their manifests"); } else { console.error("[check-lockfile] FAIL — workspace lock entries are inconsistent:"); if (workspaceResult.stdout) console.error(workspaceResult.stdout); if (workspaceResult.stderr) console.error(workspaceResult.stderr); console.error("\n → Regenerate the affected lock entries and verify with a clean `npm ci`"); process.exit(1); } } if (import.meta.url === pathToFileURL(process.argv[1] || "").href) main();