File size: 6,501 Bytes
1a6d0d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
#!/usr/bin/env node

/**
 * Native/WASM module supply-chain gate.
 *
 * Verifies the trust invariants documented in .github/SECURITY.md for the
 * project's native and WebAssembly dependencies:
 *
 *  1. Every tracked module is pinned to an exact version in package.json
 *     (no caret/tilde range a later publish could slide into).
 *  2. package-lock.json resolves it to that exact version with an
 *     integrity hash.
 *  3. Every package in the lockfile that ships an install script has an
 *     explicit entry in package.json#allowScripts β€” no unreviewed script,
 *     whether allowed or denied.
 *  4. `npm audit signatures` reports no invalid or missing registry
 *     signature for a tracked module.
 *
 * Fails loudly (non-zero exit, one line per problem) on a missing or
 * mismatched entry.
 */

const { spawnSync } = require("node:child_process");
const fs = require("node:fs");
const path = require("node:path");

/**
 * Direct dependencies whose payload is native code or WebAssembly rather
 * than auditable JavaScript. Kept in sync with the inventory table in
 * .github/SECURITY.md.
 */
const NATIVE_MODULES = [
  "@huggingface/tokenizers",
  "@moonshine-ai/moonshine-wasm",
  "@wllama/wllama",
  "hash-wasm",
  "onnxruntime-node",
];

// A single exact version, optionally with a prerelease tag. Anything with a
// range operator is rejected: native/WASM deps must be reviewed per version.
const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;

// The two key shapes `npm approve-scripts` / `npm deny-scripts` write:
// a bare name (covers every version) or an exact `name@version` pin.
function policyCovers(allowScripts, name, version) {
  return (
    Object.hasOwn(allowScripts, name) ||
    Object.hasOwn(allowScripts, `${name}@${version}`)
  );
}

function checkPins(pkg, problems) {
  for (const name of NATIVE_MODULES) {
    const spec = pkg.dependencies?.[name];
    if (spec === undefined) {
      problems.push(`${name}: not a direct dependency of this project`);
    } else if (!EXACT_VERSION.test(spec)) {
      problems.push(
        `${name}: "${spec}" is not an exact pin; native/WASM deps must declare a single version`,
      );
    }
  }
}

function checkLock(lock, pkg, problems) {
  for (const name of NATIVE_MODULES) {
    const spec = pkg.dependencies?.[name];
    if (typeof spec !== "string" || !EXACT_VERSION.test(spec)) {
      continue; // already reported by checkPins
    }
    const entry = lock.packages?.[`node_modules/${name}`];
    if (!entry) {
      problems.push(`${name}@${spec}: missing from package-lock.json`);
      continue;
    }
    if (entry.version !== spec) {
      problems.push(
        `${name}: package.json pins ${spec} but the lockfile resolves ${entry.version}`,
      );
    }
    if (!entry.integrity) {
      problems.push(
        `${name}@${entry.version}: lockfile entry has no integrity hash`,
      );
    }
  }
}

function checkInstallScriptPolicy(lock, allowScripts, problems) {
  for (const [key, entry] of Object.entries(lock.packages ?? {})) {
    if (key === "" || !entry.hasInstallScript) continue;
    const name = key.split("node_modules/").pop();
    if (!policyCovers(allowScripts, name, entry.version)) {
      problems.push(
        `${name}@${entry.version}: ships an install script with no entry in package.json#allowScripts ` +
          `(review it, then \`npm approve-scripts ${name}\` or \`npm deny-scripts ${name}\`)`,
      );
    }
  }
}

function checkRegistrySignatures(rootDir, problems) {
  const res = spawnSync("npm", ["audit", "signatures", "--json"], {
    cwd: rootDir,
    encoding: "utf8",
    maxBuffer: 64 * 1024 * 1024,
  });
  if (res.error) {
    problems.push(
      `npm audit signatures could not be run: ${res.error.message}`,
    );
    return;
  }
  let report;
  try {
    report = JSON.parse(res.stdout);
  } catch {
    problems.push(
      `npm audit signatures produced no parseable report (exit ${res.status}); ` +
        "the registry may be unreachable β€” treat as unverified",
    );
    return;
  }
  for (const [kind, items] of [
    ["invalid", report.invalid ?? []],
    ["missing", report.missing ?? []],
  ]) {
    for (const item of items) {
      if (NATIVE_MODULES.includes(item.name)) {
        problems.push(
          `${item.name}@${item.version}: ${kind} registry signature`,
        );
      } else {
        console.log(
          `note: ${item.name}@${item.version} has a ${kind} registry signature ` +
            "(outside this gate's native/WASM scope; see `npm audit signatures`)",
        );
      }
    }
  }
}

/**
 * Runs every check against a project root and returns the process exit code.
 *
 * @param {string} rootDir Project root containing package.json,
 *   package-lock.json and an installed node_modules tree.
 * @returns {number} 0 when all invariants hold, 1 otherwise.
 */
function run(rootDir) {
  const pkg = JSON.parse(
    fs.readFileSync(path.join(rootDir, "package.json"), "utf8"),
  );
  const lock = JSON.parse(
    fs.readFileSync(path.join(rootDir, "package-lock.json"), "utf8"),
  );
  const problems = [];

  checkPins(pkg, problems);
  checkLock(lock, pkg, problems);
  checkInstallScriptPolicy(lock, pkg.allowScripts ?? {}, problems);
  if (fs.existsSync(path.join(rootDir, "node_modules"))) {
    checkRegistrySignatures(rootDir, problems);
  } else {
    problems.push(
      "node_modules is missing; run `npm ci` before the integrity check",
    );
  }

  if (problems.length > 0) {
    for (const problem of problems) {
      console.log(`❌ ${problem}`);
    }
    console.log(
      `\n${problems.length} native-module integrity problem(s) found (of ${NATIVE_MODULES.length} tracked modules)`,
    );
    return 1;
  }

  console.log(
    `βœ… All ${NATIVE_MODULES.length} tracked native/WASM modules are exactly pinned, locked with integrity hashes, covered by the install-script policy, and free of invalid or missing registry signatures`,
  );
  return 0;
}

if (require.main === module) {
  const rootDirArg = process.argv[2];
  const usage = "Usage: node scripts/native-module-integrity.cjs [rootDir]";

  if (rootDirArg?.startsWith("-")) {
    console.error(`❌ Unknown option: ${rootDirArg}\n   ${usage}`);
    process.exit(1);
  }

  if (rootDirArg && !fs.existsSync(rootDirArg)) {
    console.error(`❌ Directory not found: ${rootDirArg}`);
    process.exit(1);
  }

  process.exit(run(rootDirArg || path.resolve(__dirname, "..")));
}

module.exports = { run, NATIVE_MODULES };