cb360b4
commit cb360b4
1 file changed
+47 -10
Showing 1 changed file+47-10
▼scripts/security/audit.mjs
+47-10
| @@ -15,8 +15,9 @@ | ||
15 | 15 | * 1 — fail (disallowed high/critical vulnerability present) |
16 | 16 | * 2 — script error |
17 | 17 | */ |
18 | -import { existsSync, readFileSync, statSync, writeFileSync, unlinkSync } from "node:fs"; | |
19 | -import { resolve } from "node:path"; | |
18 | +import { existsSync, readFileSync, statSync, copyFileSync, mkdtempSync, rmSync, copyFileSync as copyFileSyncEsm } from "node:fs"; | |
19 | +import { resolve, join } from "node:path"; | |
20 | +import { tmpdir } from "node:os"; | |
20 | 21 | import { spawnSync } from "node:child_process"; |
21 | 22 | |
22 | 23 | const ROOT = resolve(process.cwd()); |
| @@ -30,17 +31,53 @@ const log = (level, msg) => console.log(`[${new Date().toISOString()}] [${level} | ||
30 | 31 | function regenerateLockfile() { |
31 | 32 | log("info", "Regenerating package-lock.json from package.json (--package-lock-only)…"); |
32 | 33 | // Some npm versions crash with `Cannot read properties of null (reading 'matches')` |
33 | - // when the existing lockfile is stale or contains shapes they don't expect. | |
34 | - // Try the simple invocation first; fall back to deleting and regenerating | |
35 | - // from scratch; finally fall back to auditing whatever lockfile exists. | |
34 | + // when the existing node_modules is bun-hoisted (paths contain `+`) or when | |
35 | + // hoisted packages reference the yarn/bun `workspace:` protocol in nested | |
36 | + // `node_modules/<pkg>/package.json` (e.g. crossws in docs-site/node_modules). | |
37 | + // We try three strategies in order: | |
38 | + // 1. Plain `npm install --package-lock-only` in the project root. | |
39 | + // 2. Same with `--ignore-scripts --legacy-peer-deps` to skip workspaces. | |
40 | + // 3. Run `npm install --package-lock-only` from a clean temp directory | |
41 | + // containing only package.json, so npm never sees bun's hoisted | |
42 | + // `node_modules/.bun/*` paths or the docs-site nested packages. | |
43 | + // If all three fail AND no lockfile exists on disk, exit 2; otherwise fall | |
44 | + // back to auditing the existing lockfile (may be stale but better than 0). | |
45 | + const root = ROOT; | |
36 | 46 | const attempts = [ |
37 | - ["npm", ["install", "--package-lock-only", "--no-audit", "--no-fund", `--registry=${AUDIT_REGISTRY}`]], | |
38 | - ["npm", ["install", "--package-lock-only", "--no-audit", "--no-fund", "--ignore-scripts", "--legacy-peer-deps", `--registry=${AUDIT_REGISTRY}`]], | |
47 | + { | |
48 | + label: "root (default)", | |
49 | + run: () => spawnSync("npm", ["install", "--package-lock-only", "--no-audit", "--no-fund", `--registry=${AUDIT_REGISTRY}`], { cwd: root, stdio: "inherit" }), | |
50 | + }, | |
51 | + { | |
52 | + label: "root (--ignore-scripts --legacy-peer-deps)", | |
53 | + run: () => spawnSync("npm", ["install", "--package-lock-only", "--no-audit", "--no-fund", "--ignore-scripts", "--legacy-peer-deps", `--registry=${AUDIT_REGISTRY}`], { cwd: root, stdio: "inherit" }), | |
54 | + }, | |
55 | + { | |
56 | + label: "clean temp directory (no hoisted node_modules)", | |
57 | + run: () => { | |
58 | + const tmp = mkdtempSync(join(tmpdir(), "och-audit-")); | |
59 | + copyFileSync(join(root, "package.json"), join(tmp, "package.json")); | |
60 | + try { | |
61 | + return spawnSync("npm", ["install", "--package-lock-only", "--no-audit", "--no-fund", "--ignore-scripts", "--legacy-peer-deps", `--registry=${AUDIT_REGISTRY}`], { cwd: tmp, stdio: "inherit" }); | |
62 | + } finally { | |
63 | + try { | |
64 | + const generated = join(tmp, "package-lock.json"); | |
65 | + if (existsSync(generated)) copyFileSync(generated, LOCKFILE); | |
66 | + } catch { /* best-effort */ } | |
67 | + rmSync(tmp, { recursive: true, force: true }); | |
68 | + } | |
69 | + }, | |
70 | + }, | |
39 | 71 | ]; |
40 | 72 | for (let i = 0; i < attempts.length; i++) { |
41 | - const [cmd, args] = attempts[i]; | |
42 | - const r = spawnSync(cmd, args, { stdio: "inherit" }); | |
43 | - if (r.status === 0) return; | |
73 | + const a = attempts[i]; | |
74 | + log("info", `Lockfile regen attempt ${i + 1}: ${a.label}`); | |
75 | + const r = a.run(); | |
76 | + if (r.status === 0) { | |
77 | + // The clean-tmp attempt copies the lockfile before returning, so a | |
78 | + // subsequent `existsSync(LOCKFILE)` check at the caller will pass. | |
79 | + return; | |
80 | + } | |
44 | 81 | log("warn", `Lockfile regen attempt ${i + 1} failed (exit ${r.status})`); |
45 | 82 | } |
46 | 83 | if (existsSync(LOCKFILE)) { |
47 | 84 | |