fad-checker 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +100 -32
- package/fad-checker-report/cve-report.doc +643 -105
- package/fad-checker-report/cve-report.html +653 -108
- package/fad-checker.js +265 -55
- package/lib/codecs/binary/scan.js +71 -0
- package/lib/codecs/binary/sniff.js +37 -0
- package/lib/codecs/binary.codec.js +54 -0
- package/lib/codecs/composer.codec.js +7 -3
- package/lib/codecs/go/registry.js +19 -11
- package/lib/codecs/go.codec.js +7 -3
- package/lib/codecs/index.js +73 -4
- package/lib/codecs/maven/jar-scan.js +179 -28
- package/lib/codecs/maven.codec.js +4 -2
- package/lib/codecs/npm/collect.js +3 -1
- package/lib/codecs/npm/parse.js +29 -1
- package/lib/codecs/npm/registry.js +29 -18
- package/lib/codecs/npm.codec.js +10 -4
- package/lib/codecs/nuget.codec.js +7 -3
- package/lib/codecs/pypi/registry.js +16 -10
- package/lib/codecs/pypi.codec.js +7 -3
- package/lib/codecs/recipes.js +9 -1
- package/lib/codecs/ruby/registry.js +16 -10
- package/lib/codecs/ruby.codec.js +7 -3
- package/lib/config.js +34 -23
- package/lib/core.js +22 -3
- package/lib/cve-match.js +3 -2
- package/lib/cve-report.js +59 -6
- package/lib/dep-record.js +12 -9
- package/lib/hash-id.js +78 -0
- package/lib/json-export.js +8 -1
- package/lib/license-policy.js +3 -1
- package/lib/options-env.js +113 -0
- package/lib/outdated.js +3 -0
- package/lib/parallel-walk.js +50 -0
- package/lib/path-filter.js +58 -0
- package/lib/registries.js +112 -0
- package/lib/retire.js +66 -0
- package/lib/scan-completeness.js +7 -1
- package/lib/unmanaged.js +82 -0
- package/package.json +1 -1
package/fad-checker.js
CHANGED
|
@@ -20,7 +20,10 @@ const ui = require("./lib/ui");
|
|
|
20
20
|
|
|
21
21
|
const core = require("./lib/core");
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// require() (not fs.readFileSync) so bun --compile statically bundles package.json
|
|
24
|
+
// into the binary — otherwise the compiled exe tries to read it off disk at runtime
|
|
25
|
+
// (from $bunfs/root) and crashes with ENOENT. Keeps the bun builds fully standalone.
|
|
26
|
+
const pkg = require("./package.json");
|
|
24
27
|
|
|
25
28
|
// -------- bash/zsh completion shortcut (must run before required-options parse) --------
|
|
26
29
|
if (process.argv.includes("--completion")) {
|
|
@@ -58,8 +61,9 @@ if (process.argv.includes("--show-config")) {
|
|
|
58
61
|
const cfg = config.load();
|
|
59
62
|
const masked = { ...cfg };
|
|
60
63
|
if (masked.nvd_api_key) masked.nvd_api_key = masked.nvd_api_key.slice(0, 8) + "…" + masked.nvd_api_key.slice(-4);
|
|
61
|
-
if (
|
|
62
|
-
masked.
|
|
64
|
+
if (masked.registries && typeof masked.registries === "object") {
|
|
65
|
+
masked.registries = Object.fromEntries(Object.entries(masked.registries).map(([eco, list]) =>
|
|
66
|
+
[eco, (list || []).map(r => ({ ...r, auth: r.auth ? "***" : undefined, token: r.token ? "***" : undefined }))]));
|
|
63
67
|
}
|
|
64
68
|
console.log(JSON.stringify(masked, null, 2));
|
|
65
69
|
console.log(chalk.gray("Config file: " + config.CONFIG_PATH));
|
|
@@ -69,44 +73,58 @@ if (process.argv.includes("--show-config")) {
|
|
|
69
73
|
// -------- --add-repo / --remove-repo / --list-repos (run before program.parse) --------
|
|
70
74
|
if (process.argv.includes("--add-repo") || process.argv.includes("--remove-repo") || process.argv.includes("--list-repos")) {
|
|
71
75
|
const config = require("./lib/config");
|
|
76
|
+
const { SUPPORTED } = require("./lib/registries");
|
|
77
|
+
const ecoErr = eco => {
|
|
78
|
+
if (!SUPPORTED.includes(eco)) {
|
|
79
|
+
console.error(chalk.red(`❌ unknown ecosystem "${eco}". Supported: ${SUPPORTED.join(", ")}`));
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
72
83
|
if (process.argv.includes("--list-repos")) {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
84
|
+
const map = config.getRegistryMap();
|
|
85
|
+
const ecos = Object.keys(map).filter(e => (map[e] || []).length);
|
|
86
|
+
if (!ecos.length) {
|
|
87
|
+
console.log(chalk.gray("No custom registries configured (public registries are always the fallback)."));
|
|
76
88
|
} else {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
89
|
+
for (const eco of ecos) {
|
|
90
|
+
console.log(chalk.bold(`${eco} (tried in order, then public):`));
|
|
91
|
+
for (const r of map[eco]) {
|
|
92
|
+
const authMark = (r.auth || r.token) ? chalk.yellow(" [auth]") : "";
|
|
93
|
+
console.log(` • ${chalk.cyan(r.name)} → ${r.url}${authMark}`);
|
|
94
|
+
}
|
|
81
95
|
}
|
|
82
96
|
}
|
|
83
97
|
process.exit(0);
|
|
84
98
|
}
|
|
85
99
|
if (process.argv.includes("--add-repo")) {
|
|
86
100
|
const idx = process.argv.indexOf("--add-repo");
|
|
87
|
-
const name = process.argv[idx + 1];
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
console.error(
|
|
91
|
-
console.error("
|
|
92
|
-
console.error(" Optional auth: --add-repo nexus https://nexus.acme.com/repository/maven-public/ --auth user:pass");
|
|
101
|
+
const [eco, name, url] = [process.argv[idx + 1], process.argv[idx + 2], process.argv[idx + 3]];
|
|
102
|
+
if (!eco || !name || !url || [eco, name, url].some(a => a.startsWith("-"))) {
|
|
103
|
+
console.error(chalk.red("❌ --add-repo requires <ecosystem> <name> <url>"));
|
|
104
|
+
console.error(" Example: fad-checker --add-repo npm verdaccio https://npm.acme/ --token TOK");
|
|
105
|
+
console.error(" Maven: fad-checker --add-repo maven nexus https://nexus.acme/maven-public/ --auth user:pass");
|
|
93
106
|
process.exit(1);
|
|
94
107
|
}
|
|
108
|
+
ecoErr(eco);
|
|
95
109
|
const authIdx = process.argv.indexOf("--auth");
|
|
96
|
-
const
|
|
97
|
-
config.
|
|
98
|
-
|
|
110
|
+
const tokIdx = process.argv.indexOf("--token");
|
|
111
|
+
config.addRegistry(eco, name, url, {
|
|
112
|
+
auth: authIdx > -1 ? process.argv[authIdx + 1] : null,
|
|
113
|
+
token: tokIdx > -1 ? process.argv[tokIdx + 1] : null,
|
|
114
|
+
});
|
|
115
|
+
console.log(chalk.green(`✅ Added ${eco} registry "${name}" → ${url}`));
|
|
99
116
|
process.exit(0);
|
|
100
117
|
}
|
|
101
118
|
if (process.argv.includes("--remove-repo")) {
|
|
102
119
|
const idx = process.argv.indexOf("--remove-repo");
|
|
103
|
-
const name = process.argv[idx + 1];
|
|
104
|
-
if (!name || name.startsWith("-")) {
|
|
105
|
-
console.error(chalk.red("❌ --remove-repo requires <name>"));
|
|
120
|
+
const [eco, name] = [process.argv[idx + 1], process.argv[idx + 2]];
|
|
121
|
+
if (!eco || !name || [eco, name].some(a => a.startsWith("-"))) {
|
|
122
|
+
console.error(chalk.red("❌ --remove-repo requires <ecosystem> <name>"));
|
|
106
123
|
process.exit(1);
|
|
107
124
|
}
|
|
108
|
-
|
|
109
|
-
|
|
125
|
+
ecoErr(eco);
|
|
126
|
+
const removed = config.removeRegistry(eco, name);
|
|
127
|
+
console.log(removed ? chalk.green(`✅ Removed ${eco} registry "${name}"`) : chalk.yellow(`⚠️ No ${eco} registry named "${name}"`));
|
|
110
128
|
process.exit(removed ? 0 : 1);
|
|
111
129
|
}
|
|
112
130
|
}
|
|
@@ -166,7 +184,11 @@ program
|
|
|
166
184
|
.option("-t, --target <target>", "output directory (will be rm before written). If omitted, the run is read-only.")
|
|
167
185
|
// Not a requiredOption: --import-anonymized scans a descriptor with no source tree.
|
|
168
186
|
.option("-s, --src <src>", "root directory containing pom.xml files")
|
|
169
|
-
.option("
|
|
187
|
+
.option("--source <src>", "alias of --src (also the JSON config key 'source')")
|
|
188
|
+
.option("--config <file>", "load default options from a JSON config file (else ./.fad-env.json)")
|
|
189
|
+
.option("-e, --exclude <exclude>", "regex of groupId/name to exclude, e.g. '^(client|private)\\.'")
|
|
190
|
+
.option("--exclude-path <glob...>", "ignore sub-paths during the walk (gitignore-style glob, relative to --src). Repeatable. e.g. 'packages/legacy/**' '**/fixtures/**'")
|
|
191
|
+
.option("--no-default-excludes", "don't prune the built-in ignored dirs (node_modules, vendor, target, .git, …) — walk everything")
|
|
170
192
|
.option("-v, --verbose", "verbose")
|
|
171
193
|
// Defaults: report + transitive + allLibs all ON. Use --no-* to disable.
|
|
172
194
|
.option("--no-report", "write NO output files at all — the scan, terminal summary and --fail-on gate still run (gate-only / CI mode)")
|
|
@@ -204,6 +226,7 @@ program
|
|
|
204
226
|
.option("--cve-offline", "use cached CVE index only (no download)")
|
|
205
227
|
.option("--snyk", "run snyk on cleaned POMs and merge into report (requires --target)")
|
|
206
228
|
.option("--no-retire", "skip retire.js vendored-JS scan")
|
|
229
|
+
.option("--no-vendored-js-inventory", "don't list ALL identified vendored JS libs (chapter 1D) — keep only the vulnerable ones (chapter 2)")
|
|
207
230
|
.option("--retire-refresh", "ignore retire cache and re-scan")
|
|
208
231
|
.option("--transitive-depth <n>", "max transitive depth", "6")
|
|
209
232
|
.option("--ecosystem <list>", "codecs to run: auto|all|<comma list> e.g. maven,npm,nuget,composer,pypi,go,ruby (default: auto = detected)", "auto")
|
|
@@ -215,16 +238,35 @@ program
|
|
|
215
238
|
.option("--no-pypi", "skip the PyPI (Python) codec")
|
|
216
239
|
.option("--no-go", "skip the Go codec")
|
|
217
240
|
.option("--no-ruby", "skip the Ruby (Bundler) codec")
|
|
241
|
+
.option("--no-binaries", "skip scanning committed native binaries (.dll/.exe/.so/.dylib)")
|
|
218
242
|
.option("--no-jars", "skip scanning embedded .jar/.war/.ear binaries for Maven coordinates")
|
|
219
243
|
.option("--no-js", "alias: skip JS/npm/yarn manifests even if present (Maven-only)")
|
|
220
|
-
.option("--repo <url...>", "extra
|
|
221
|
-
.option("--add-repo <
|
|
222
|
-
.option("--remove-repo <
|
|
223
|
-
.option("--list-repos", "list configured
|
|
244
|
+
.option("--repo <eco=url...>", "extra registry as <ecosystem>=<url> (e.g. npm=https://npm.acme/) tried before the public one. Repeatable. Supports https://user:pass@host/.")
|
|
245
|
+
.option("--add-repo <eco>", "persist a registry: --add-repo <ecosystem> <name> <url> [--auth user:pass] [--token TOK]")
|
|
246
|
+
.option("--remove-repo <eco>", "remove a persisted registry: --remove-repo <ecosystem> <name>")
|
|
247
|
+
.option("--list-repos", "list configured registries (grouped by ecosystem) and exit")
|
|
248
|
+
.option("--auth <user:pass>", "Basic auth for --add-repo")
|
|
249
|
+
.option("--token <token>", "Bearer token for --add-repo")
|
|
224
250
|
.option("--completion <shell>", "print shell completion script (bash|zsh)");
|
|
225
251
|
program.parse(process.argv);
|
|
226
252
|
|
|
227
253
|
const options = program.opts();
|
|
254
|
+
// Layered config: CLI flags > config file (--config / ./.fad-env.json, JSON) >
|
|
255
|
+
// FAD_CHECKER_ENV (a CLI-flag string) > global ~/.fad-checker/config.json >
|
|
256
|
+
// commander defaults. A file/env value fills an option only if the CLI didn't
|
|
257
|
+
// set it. `registries` are unioned separately (below). Source has src/source aliases.
|
|
258
|
+
const { loadLayers, applyLayers } = require("./lib/options-env");
|
|
259
|
+
let _layers = { fileLayer: {}, envLayer: {}, envRepos: [] };
|
|
260
|
+
try {
|
|
261
|
+
_layers = loadLayers({ cwd: process.cwd(), configPath: options.config, envStr: process.env.FAD_CHECKER_ENV, program });
|
|
262
|
+
} catch (err) {
|
|
263
|
+
console.error(chalk.red(`❌ ${err.message}`));
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
Object.assign(options, applyLayers(program, _layers, require("./lib/config").load()));
|
|
267
|
+
// --source CLI alias → src (applyLayers already maps the file/env JSON 'source' key).
|
|
268
|
+
if (!options.src && options.source) options.src = options.source;
|
|
269
|
+
|
|
228
270
|
const deps2Exclude = options.exclude ? new RegExp(options.exclude) : null;
|
|
229
271
|
const verbose = !!options.verbose;
|
|
230
272
|
|
|
@@ -269,20 +311,87 @@ if (options.src && options.target) {
|
|
|
269
311
|
}
|
|
270
312
|
}
|
|
271
313
|
|
|
272
|
-
|
|
314
|
+
// Maven Central presence cache (~/.fad-checker/maven-exists-cache.json) — keyed by
|
|
315
|
+
// "g:a", value true (on a repo) / false (absent → likely private). Persisted so an
|
|
316
|
+
// online warm-up populates it, --export-cache ships it, and an --offline air-gapped
|
|
317
|
+
// run reads it instead of probing the network. Returns:
|
|
318
|
+
// true → present on a configured repo
|
|
319
|
+
// false → absent (likely private)
|
|
320
|
+
// null → unknown (offline + not cached, or probe error) — caller must NOT guess
|
|
321
|
+
const MAVEN_EXISTS_CACHE_PATH = require("path").join(require("./lib/outdated").CACHE_DIR, "maven-exists-cache.json");
|
|
322
|
+
const MAVEN_EXISTS_MAX_AGE_MS = 7 * 24 * 3600 * 1000; // 7 days
|
|
323
|
+
|
|
324
|
+
async function checkMavenLibExist(groupId, artifactId, repos, cache, opts = {}) {
|
|
273
325
|
const g = core.coord(groupId);
|
|
274
326
|
const a = core.coord(artifactId);
|
|
275
|
-
if (!g || !a) return
|
|
327
|
+
if (!g || !a) return null;
|
|
328
|
+
const key = `${g}:${a}`;
|
|
329
|
+
if (cache && Object.prototype.hasOwnProperty.call(cache.entries, key)) return cache.entries[key];
|
|
330
|
+
if (opts.offline) return null; // air-gapped + not warmed: honestly unknown, never network
|
|
276
331
|
const p = `${g.replace(/\./g, "/")}/${a}/maven-metadata.xml`;
|
|
277
332
|
const { existsInAny } = require("./lib/maven-repo");
|
|
278
333
|
try {
|
|
279
334
|
const hit = await existsInAny(repos, p, { userAgent: "fad-checker-existence" });
|
|
280
|
-
if (
|
|
281
|
-
if (verbose) console.log(chalk.dim(` not on any repo: ${g}:${a}`));
|
|
282
|
-
return
|
|
335
|
+
if (cache) cache.entries[key] = !!hit;
|
|
336
|
+
if (!hit && verbose) console.log(chalk.dim(` not on any repo: ${g}:${a}`));
|
|
337
|
+
return !!hit;
|
|
283
338
|
} catch (err) {
|
|
284
339
|
if (verbose) console.info(chalk.dim(` error querying repos: ${g}:${a} — ${err.message}`));
|
|
285
|
-
return
|
|
340
|
+
return null; // probe failed → unknown, don't poison the cache or mislabel as private
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Build an onProgress callback for the embedded-JAR scan so the user sees what's
|
|
346
|
+
* happening — the scan reads + unzips archives synchronously and can block for a
|
|
347
|
+
* while on big or numerous fat-jars (incl. the silent recursion through a fat-jar's
|
|
348
|
+
* bundled libs). The scanner reports EVERY archive (top-level + nested):
|
|
349
|
+
* - On a TTY: one transient line, rewritten in place, naming the archive being
|
|
350
|
+
* read right now (so a long pause clearly points at the culprit). Cleared at end.
|
|
351
|
+
* - Off a TTY (CI/pipe): a throttled line every ~250 archives so logs show forward
|
|
352
|
+
* motion without being spammed, plus a start and a final summary line.
|
|
353
|
+
* Returns a fresh closure per codec.
|
|
354
|
+
*/
|
|
355
|
+
function makeJarProgress() {
|
|
356
|
+
let total = 0, lastLogged = 0;
|
|
357
|
+
const STEP = 250;
|
|
358
|
+
return (ev) => {
|
|
359
|
+
if (!ev) return;
|
|
360
|
+
if (ev.phase === "start") {
|
|
361
|
+
total = ev.total || 0;
|
|
362
|
+
if (total && !ui.isTTY) ui.info(chalk.dim(`scanning ${total} embedded archive(s) (.jar/.war/.ear)…`));
|
|
363
|
+
} else if (ev.phase === "scan" && total) {
|
|
364
|
+
if (ui.isTTY) {
|
|
365
|
+
const head = total ? `${ev.scanned}/${total}+` : String(ev.scanned);
|
|
366
|
+
process.stdout.write(`\r ${chalk.dim("·")} ${chalk.dim(`reading embedded JARs (${head})`)} ${chalk.dim(ev.path)}\x1b[K`);
|
|
367
|
+
} else if (ev.scanned - lastLogged >= STEP) {
|
|
368
|
+
lastLogged = ev.scanned;
|
|
369
|
+
ui.info(chalk.dim(`… ${ev.scanned} archive(s) read (current: ${ev.path})`));
|
|
370
|
+
}
|
|
371
|
+
} else if (ev.phase === "done") {
|
|
372
|
+
if (total && ui.isTTY) process.stdout.write("\r\x1b[K");
|
|
373
|
+
else if (total && !ui.isTTY) ui.info(chalk.dim(`scanned ${ev.scanned} archive(s) → ${ev.found} embedded coord(s)`));
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Run `fn` (sync or async) while telling the user which phase is in flight, so a
|
|
380
|
+
* long pause is attributable instead of a silent hang. TTY: a transient line that's
|
|
381
|
+
* cleared when done. Non-TTY: a plain "· <label> …" line. Either way, a phase that
|
|
382
|
+
* takes >3s prints "· <label> took Ns" so slow steps self-report even without -v.
|
|
383
|
+
*/
|
|
384
|
+
async function timedPhase(label, fn) {
|
|
385
|
+
if (ui.isTTY) process.stdout.write(` ${chalk.dim("·")} ${chalk.dim(label + " …")}\x1b[K`);
|
|
386
|
+
else ui.info(chalk.dim(label + " …"));
|
|
387
|
+
const t0 = Date.now();
|
|
388
|
+
try {
|
|
389
|
+
return await fn();
|
|
390
|
+
} finally {
|
|
391
|
+
const ms = Date.now() - t0;
|
|
392
|
+
if (ui.isTTY) process.stdout.write("\r\x1b[K");
|
|
393
|
+
if (ms > 3000) ui.info(chalk.dim(`${label} took ${(ms / 1000).toFixed(1)}s`));
|
|
394
|
+
else if (verbose) ui.info(chalk.dim(`${label} done in ${ms}ms`));
|
|
286
395
|
}
|
|
287
396
|
}
|
|
288
397
|
|
|
@@ -292,13 +401,44 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
292
401
|
// Build the Maven repo list once: persisted repos (from ~/.fad-checker/config.json)
|
|
293
402
|
// + ad-hoc --repo URLs + Maven Central as final fallback. Used by transitive
|
|
294
403
|
// resolution, outdated-version check, and existence check.
|
|
295
|
-
const {
|
|
404
|
+
const { getRegistryMap } = require("./lib/config");
|
|
296
405
|
const { buildRepoList } = require("./lib/maven-repo");
|
|
297
|
-
const
|
|
298
|
-
|
|
406
|
+
const { buildRegistryList } = require("./lib/registries");
|
|
407
|
+
// One-off --repo eco=url (from the CLI and the env layer), grouped by ecosystem.
|
|
408
|
+
const cliRepoMap = {};
|
|
409
|
+
for (const spec of [...(options.repo || []), ...(_layers.envRepos || [])]) {
|
|
410
|
+
const m = /^([a-z]+)=(.+)$/i.exec(String(spec));
|
|
411
|
+
if (!m) { console.error(chalk.red(`❌ --repo expects <ecosystem>=<url>, got "${spec}"`)); process.exit(1); }
|
|
412
|
+
(cliRepoMap[m[1]] ||= []).push({ url: m[2] });
|
|
413
|
+
}
|
|
414
|
+
// Union the registry sources: global config + config-file JSON + CLI/env one-offs.
|
|
415
|
+
const fileRegMap = (_layers.fileLayer && _layers.fileLayer.registries) || {};
|
|
416
|
+
const globalRegMap = getRegistryMap();
|
|
417
|
+
const regMap = {};
|
|
418
|
+
for (const eco of new Set([...Object.keys(globalRegMap), ...Object.keys(fileRegMap), ...Object.keys(cliRepoMap)])) {
|
|
419
|
+
regMap[eco] = buildRegistryList(eco, [globalRegMap[eco], fileRegMap[eco], cliRepoMap[eco]]);
|
|
420
|
+
}
|
|
421
|
+
const registriesFor = eco => regMap[eco] || [];
|
|
422
|
+
const mavenRepos = buildRepoList(regMap.maven || [], []); // appends Maven Central last
|
|
423
|
+
|
|
424
|
+
// Walk-pruning: union --exclude-path globs across every config layer (CLI + file
|
|
425
|
+
// + env + global), like registries. `defaultExcludes` (a scalar, false via
|
|
426
|
+
// --no-default-excludes) already flowed through applyLayers.
|
|
427
|
+
const excludePath = [...new Set([
|
|
428
|
+
...(options.excludePath || []),
|
|
429
|
+
...((_layers.fileLayer && _layers.fileLayer.excludePath) || []),
|
|
430
|
+
...((_layers.envLayer && _layers.envLayer.excludePath) || []),
|
|
431
|
+
...(require("./lib/config").get("excludePath") || []),
|
|
432
|
+
].filter(Boolean))];
|
|
433
|
+
const defaultExcludes = options.defaultExcludes !== false;
|
|
434
|
+
const walkOpts = { excludePath, defaultExcludes };
|
|
299
435
|
const runMode = options.importAnonymized ? "import descriptor" : (options.offline ? "offline" : "online");
|
|
300
436
|
if (options.src) ui.kv("source", chalk.white(options.src));
|
|
301
437
|
if (mavenRepos.length > 1) ui.kv("repos", chalk.white(mavenRepos.map(r => r.name).join(chalk.dim(" → "))));
|
|
438
|
+
const otherRegs = Object.keys(regMap).filter(e => e !== "maven" && regMap[e].length);
|
|
439
|
+
if (otherRegs.length) ui.kv("registries", chalk.white(otherRegs.map(e => `${e}:${regMap[e].length}`).join(" ")));
|
|
440
|
+
if (excludePath.length) ui.kv("exclude-path", chalk.white(excludePath.join(chalk.dim(", "))));
|
|
441
|
+
if (!defaultExcludes) ui.kv("default-excludes", chalk.yellow("off (walking node_modules/vendor/.git/…)"));
|
|
302
442
|
ui.kv("mode", chalk.white(runMode));
|
|
303
443
|
|
|
304
444
|
let wrotePom = 0;
|
|
@@ -324,7 +464,7 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
324
464
|
const { warmRetireSignatures } = require("./lib/retire");
|
|
325
465
|
await warmRetireSignatures({ verbose });
|
|
326
466
|
}
|
|
327
|
-
await runReportFlow(resolved, { activeIds, runMaven, runNpm, privateLibIds: [], mavenRepos, collectWarnings: [] });
|
|
467
|
+
await runReportFlow(resolved, { activeIds, runMaven, runNpm, privateLibIds: [], mavenRepos, regMap, collectWarnings: [] });
|
|
328
468
|
return;
|
|
329
469
|
}
|
|
330
470
|
|
|
@@ -332,13 +472,24 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
332
472
|
const { detectCodecs, allCodecs, getCodec } = require("./lib/codecs");
|
|
333
473
|
const { resolveActiveCodecs } = require("./lib/codecs/select");
|
|
334
474
|
const eco = (options.ecosystem || "auto").toLowerCase();
|
|
335
|
-
const detected = (eco === "auto")
|
|
475
|
+
const detected = (eco === "auto")
|
|
476
|
+
? (await timedPhase("detecting ecosystems", () => detectCodecs(options.src, walkOpts))).map(c => c.id)
|
|
477
|
+
: allCodecs().map(c => c.id);
|
|
478
|
+
// The binary scanner is a cross-cutting catch-all (committed native libs in ANY
|
|
479
|
+
// project), and detectCodecs' manifest-glob matcher misses versioned sonames
|
|
480
|
+
// (libz.so.1). Always make it a candidate in auto mode; --no-binaries removes it.
|
|
481
|
+
if (eco === "auto" && !detected.includes("binary")) detected.push("binary");
|
|
336
482
|
const noCodecs = ["maven", "npm", "yarn", "nuget", "composer", "pypi", "go", "ruby"].filter(id => options[id] === false);
|
|
483
|
+
// `--no-binaries` maps to options.binaries (plural) but the codec id is `binary`.
|
|
484
|
+
if (options.binaries === false) noCodecs.push("binary");
|
|
337
485
|
const activeIds = resolveActiveCodecs(eco, detected, { noCodecs, noJs: !options.js });
|
|
338
486
|
const runMaven = activeIds.includes("maven");
|
|
339
487
|
const runNpm = activeIds.includes("npm") || activeIds.includes("yarn");
|
|
340
488
|
|
|
341
489
|
// --- Collect deps from every active codec into one Map (coordKeys never collide) ---
|
|
490
|
+
// Section header first so the embedded-JAR scan can print live progress under it
|
|
491
|
+
// (the scan reads + unzips archives synchronously and would otherwise block silently).
|
|
492
|
+
ui.section("Collection");
|
|
342
493
|
const resolved = new Map();
|
|
343
494
|
let mavenCtx = null;
|
|
344
495
|
const collectWarnings = [];
|
|
@@ -347,7 +498,7 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
347
498
|
const codec = getCodec(id);
|
|
348
499
|
let res;
|
|
349
500
|
try {
|
|
350
|
-
res = await codec.collect(options.src, { ignoreTest: !!options.ignoreTest, deps2Exclude, verbose, scanJars: options.jars !== false, srcRoot: options.src });
|
|
501
|
+
res = await timedPhase(`collecting ${codec.label || id}`, () => codec.collect(options.src, { ignoreTest: !!options.ignoreTest, deps2Exclude, verbose, scanJars: options.jars !== false, srcRoot: options.src, excludePath, defaultExcludes, onJarProgress: makeJarProgress(), onBinaryProgress: null }));
|
|
351
502
|
} catch (err) {
|
|
352
503
|
console.warn(chalk.red(`❌ ${id} collect failed:`), chalk.dim(err.message));
|
|
353
504
|
continue;
|
|
@@ -358,11 +509,12 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
358
509
|
}
|
|
359
510
|
|
|
360
511
|
// --- Collection summary ---
|
|
361
|
-
ui.section("Collection");
|
|
362
512
|
const ecoCount = {};
|
|
363
513
|
let embeddedCount = 0;
|
|
514
|
+
let binaryCount = 0;
|
|
364
515
|
for (const d of resolved.values()) {
|
|
365
516
|
if (d.provenance === "embedded") { embeddedCount++; continue; } // counted separately below
|
|
517
|
+
if (d.provenance === "binary") { binaryCount++; continue; } // committed native libs, no manifest
|
|
366
518
|
ecoCount[d.ecosystem] = (ecoCount[d.ecosystem] || 0) + 1;
|
|
367
519
|
}
|
|
368
520
|
if (runMaven) ui.ok(`${chalk.bold("Maven".padEnd(8))} ${mavenCtx ? mavenCtx.pomFiles.length + " module(s) · " : ""}${ecoCount.maven || 0} direct dep(s)`);
|
|
@@ -372,6 +524,7 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
372
524
|
ui.ok(`${chalk.bold(((getCodec(id)?.label) || id).padEnd(8))} ${n} dep(s)`);
|
|
373
525
|
}
|
|
374
526
|
if (embeddedCount) ui.ok(`${chalk.bold("Embedded".padEnd(8))} ${embeddedCount} coord(s) in .jar/.war/.ear`);
|
|
527
|
+
if (binaryCount) ui.ok(`${chalk.bold("Binary".padEnd(8))} ${binaryCount} native lib(s) (.dll/.exe/.so/.dylib)`);
|
|
375
528
|
if (!ecoCount.maven && !ecoCount.npm && !Object.keys(ecoCount).length) ui.warn("no dependencies found in the source tree");
|
|
376
529
|
if (collectWarnings.length) {
|
|
377
530
|
ui.warn(`${collectWarnings.length} manifest warning(s) — best-effort / no lockfile:`);
|
|
@@ -432,7 +585,18 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
432
585
|
ui.ok("no missing Maven parent POMs");
|
|
433
586
|
}
|
|
434
587
|
|
|
588
|
+
// Private-lib detection asks each configured repo whether a missing coord
|
|
589
|
+
// exists (→ absent = likely private). Results are cached + bundled, so an
|
|
590
|
+
// --offline run reads the online-warmed cache instead of probing the network.
|
|
591
|
+
// A coord that's neither cached nor probeable (offline + cold) stays UNKNOWN —
|
|
592
|
+
// we never fake it as "private", which is what made offline both wrong and slow.
|
|
435
593
|
if (options.allLibs) {
|
|
594
|
+
const { loadJsonCache, saveJsonCache } = require("./lib/outdated");
|
|
595
|
+
const existsCache = loadJsonCache(MAVEN_EXISTS_CACHE_PATH);
|
|
596
|
+
const fresh = existsCache.meta?.fetchedAt && (Date.now() - existsCache.meta.fetchedAt) < MAVEN_EXISTS_MAX_AGE_MS;
|
|
597
|
+
if (!fresh && !options.offline) existsCache.entries = {}; // refresh stale probes when online
|
|
598
|
+
if (!existsCache.entries) existsCache.entries = {};
|
|
599
|
+
|
|
436
600
|
const anyMissingLibs = Object.keys(allPomMetadata.anyMissingById)
|
|
437
601
|
.filter(id => {
|
|
438
602
|
const parts = id.split(":");
|
|
@@ -442,14 +606,20 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
442
606
|
const limit = pLimit(10);
|
|
443
607
|
const results = await Promise.all(anyMissingLibs.map(id => {
|
|
444
608
|
const [g, a] = id.split(":");
|
|
445
|
-
return limit(async () => ({ id, found: await checkMavenLibExist(g, a, mavenRepos) }));
|
|
609
|
+
return limit(async () => ({ id, found: await checkMavenLibExist(g, a, mavenRepos, existsCache, { offline: options.offline }) }));
|
|
446
610
|
}));
|
|
447
|
-
|
|
611
|
+
let unknown = 0;
|
|
612
|
+
for (const r of results) {
|
|
613
|
+
if (r.found === false) privateLibIds.push(r.id);
|
|
614
|
+
else if (r.found === null) unknown++;
|
|
615
|
+
}
|
|
616
|
+
if (!options.offline) { existsCache.meta = { fetchedAt: Date.now() }; saveJsonCache(MAVEN_EXISTS_CACHE_PATH, existsCache); }
|
|
448
617
|
if (privateLibIds.length) {
|
|
449
618
|
ui.warn(`${privateLibIds.length} lib(s) absent from Maven Central (likely private):`);
|
|
450
619
|
for (const id of privateLibIds.slice(0, 10)) ui.info(chalk.magenta(id));
|
|
451
620
|
if (privateLibIds.length > 10) ui.info(chalk.dim(`…and ${privateLibIds.length - 10} more`));
|
|
452
621
|
}
|
|
622
|
+
if (unknown) ui.info(chalk.dim(`${unknown} lib(s) not in the presence cache — run online once (or --export-cache from an online host) to classify them`));
|
|
453
623
|
}
|
|
454
624
|
|
|
455
625
|
if (deps2Exclude) {
|
|
@@ -476,7 +646,7 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
476
646
|
// The scan always runs — it feeds the terminal summary, the file outputs and the
|
|
477
647
|
// CI gate. Which files get written is decided by the --report-* family inside
|
|
478
648
|
// (HTML + .doc by default; --no-report writes nothing).
|
|
479
|
-
await runReportFlow(resolved, { activeIds, runMaven, runNpm, privateLibIds, mavenRepos, collectWarnings });
|
|
649
|
+
await runReportFlow(resolved, { activeIds, runMaven, runNpm, privateLibIds, mavenRepos, regMap, collectWarnings });
|
|
480
650
|
if (!readOnly) {
|
|
481
651
|
ui.section("Next step");
|
|
482
652
|
ui.info(`run Snyk on the cleaned tree:`);
|
|
@@ -485,7 +655,8 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
485
655
|
})();
|
|
486
656
|
|
|
487
657
|
async function runReportFlow(resolved, ecoFlags = {}) {
|
|
488
|
-
const { activeIds = [], runMaven = true, runNpm = false, privateLibIds = [], mavenRepos = [], collectWarnings = [] } = ecoFlags;
|
|
658
|
+
const { activeIds = [], runMaven = true, runNpm = false, privateLibIds = [], mavenRepos = [], regMap = {}, collectWarnings = [] } = ecoFlags;
|
|
659
|
+
const registriesFor = eco => regMap[eco] || [];
|
|
489
660
|
const { expandWithTransitives } = require("./lib/cve-match");
|
|
490
661
|
const { writeReports, computeStats } = require("./lib/cve-report");
|
|
491
662
|
const { getCodec } = require("./lib/codecs");
|
|
@@ -530,9 +701,11 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
530
701
|
const willKev = !!options.kev;
|
|
531
702
|
const willLicenses = !!options.licenses;
|
|
532
703
|
const willRetire = !!options.retire;
|
|
704
|
+
// Identify committed native binaries by checksum (deps.dev + CIRCL) when present.
|
|
705
|
+
const willBinaryId = [...resolved.values()].some(d => d.provenance === "binary");
|
|
533
706
|
// License detection piggybacks on the registry passes (same fetched metadata),
|
|
534
707
|
// so it adds no progress step of its own.
|
|
535
|
-
const totalSteps = [willTransitive, willCve, /*EOL*/ true, willOutdated, /*npm reg*/ true, ...otherRegistryIds.map(() => true), willOsv, willNvd, willEpss, willKev, willRetire].filter(Boolean).length;
|
|
708
|
+
const totalSteps = [willTransitive, willCve, /*EOL*/ true, willOutdated, /*npm reg*/ true, ...otherRegistryIds.map(() => true), willOsv, willNvd, willEpss, willKev, willRetire, willBinaryId].filter(Boolean).length;
|
|
536
709
|
const progress = new ui.Progress(totalSteps);
|
|
537
710
|
|
|
538
711
|
if (willTransitive) {
|
|
@@ -562,6 +735,17 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
562
735
|
}
|
|
563
736
|
}
|
|
564
737
|
|
|
738
|
+
// 1c. Identify committed native binaries by checksum (deps.dev → CIRCL).
|
|
739
|
+
if (willBinaryId) {
|
|
740
|
+
const st = progress.start("Binary identification (deps.dev + CIRCL)");
|
|
741
|
+
try {
|
|
742
|
+
const { enrichUnmanaged } = require("./lib/unmanaged");
|
|
743
|
+
const s = await enrichUnmanaged(resolved, { offline, onProgress: (p, t) => st.tick(p, t) });
|
|
744
|
+
const bits = [`${s.identified}/${s.total} identified`, s.pristine ? `${s.pristine} pristine` : null, s.unknown ? `${s.unknown} unknown` : null, s.malicious ? `${s.malicious} ⚠ malicious` : null].filter(Boolean).join(", ");
|
|
745
|
+
st.done(bits);
|
|
746
|
+
} catch (err) { st.fail(err.message); }
|
|
747
|
+
}
|
|
748
|
+
|
|
565
749
|
// 2. EOL frameworks (endoflife.date) — always a step.
|
|
566
750
|
let eolResults = [];
|
|
567
751
|
{
|
|
@@ -595,7 +779,7 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
595
779
|
const st = progress.start("npm registry");
|
|
596
780
|
try {
|
|
597
781
|
const { checkNpmRegistryDeps } = require("./lib/codecs/npm/registry");
|
|
598
|
-
const npmReg = await checkNpmRegistryDeps(resolved, { verbose, offline, allLibs: options.allLibs, onProgress: (p, t) => st.tick(p, t) });
|
|
782
|
+
const npmReg = await checkNpmRegistryDeps(resolved, { verbose, offline, allLibs: options.allLibs, registries: registriesFor("npm"), onProgress: (p, t) => st.tick(p, t) });
|
|
599
783
|
obsoleteResults = obsoleteResults.concat(npmReg.deprecated);
|
|
600
784
|
outdatedResults = outdatedResults.concat(npmReg.outdated);
|
|
601
785
|
licenseFindings = licenseFindings.concat(npmReg.licensed || []);
|
|
@@ -608,7 +792,7 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
608
792
|
const codec = getCodec(id);
|
|
609
793
|
const st = progress.start(`${codec.label || id} registry`);
|
|
610
794
|
try {
|
|
611
|
-
const reg = await codec.checkRegistry(resolved, { verbose, offline, allLibs: options.allLibs, onProgress: (p, t) => st.tick(p, t) });
|
|
795
|
+
const reg = await codec.checkRegistry(resolved, { verbose, offline, allLibs: options.allLibs, registries: registriesFor(id), onProgress: (p, t) => st.tick(p, t) });
|
|
612
796
|
obsoleteResults = obsoleteResults.concat(reg.deprecated || []);
|
|
613
797
|
outdatedResults = outdatedResults.concat(reg.outdated || []);
|
|
614
798
|
licenseFindings = licenseFindings.concat(reg.licensed || []);
|
|
@@ -702,6 +886,7 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
702
886
|
// vendored .js (which can live in a Maven project's resources too). The
|
|
703
887
|
// scanner is owned by the npm codec but runs whenever --retire is on.
|
|
704
888
|
let retireMatches = [];
|
|
889
|
+
let vendoredJsInventory = [];
|
|
705
890
|
if (willRetire) {
|
|
706
891
|
const st = progress.start("retire.js (vendored JS)");
|
|
707
892
|
const sc = (getCodec("npm").nativeScanners || []).find(s => s.kind === "vendored");
|
|
@@ -711,7 +896,9 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
711
896
|
try {
|
|
712
897
|
const r = await sc.scan(resolved, { src: options.src, verbose, retireRefresh: !!options.retireRefresh, offline });
|
|
713
898
|
retireMatches = r.matches || [];
|
|
714
|
-
|
|
899
|
+
if (options.vendoredJsInventory !== false) vendoredJsInventory = r.meta?.inventory || [];
|
|
900
|
+
const invN = vendoredJsInventory.length;
|
|
901
|
+
st.done(`${retireMatches.length} finding(s)${invN ? ` · ${invN} lib(s) inventoried` : ""}`);
|
|
715
902
|
} catch (err) { st.fail(err.message); }
|
|
716
903
|
}
|
|
717
904
|
}
|
|
@@ -774,6 +961,15 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
774
961
|
const sev = ui.sevColor;
|
|
775
962
|
const depLabel = d => d.ecosystem === "npm" ? `npm:${d.artifactId}` : `${d.groupId}:${d.artifactId}`;
|
|
776
963
|
const coordOf = depLabel; // npm deps show as "npm:name", others as "g:a"
|
|
964
|
+
// Where a finding's dependency was declared — the pom.xml / package.json / jar
|
|
965
|
+
// (embedded jars carry a "app.jar!/BOOT-INF/lib/…" manifestPath). Shown so EOL /
|
|
966
|
+
// obsolete / outdated entries point at the file the reader has to edit.
|
|
967
|
+
const definedInOf = d => {
|
|
968
|
+
const paths = (d?.manifestPaths?.length ? d.manifestPaths : d?.pomPaths?.length ? d.pomPaths : []);
|
|
969
|
+
if (!paths.length) return "";
|
|
970
|
+
const rel = paths.map(p => { try { return path.relative(options.src, p); } catch { return p; } });
|
|
971
|
+
return chalk.dim(` ← ${rel[0]}${rel.length > 1 ? ` (+${rel.length - 1})` : ""}`);
|
|
972
|
+
};
|
|
777
973
|
const fmtStats = s => [
|
|
778
974
|
s.critical ? sev("CRITICAL")(`${s.critical} critical`) : null,
|
|
779
975
|
s.high ? sev("HIGH")(`${s.high} high`) : null,
|
|
@@ -805,16 +1001,30 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
805
1001
|
if (embeddedActive.length > 8) console.log(chalk.dim(` …and ${embeddedActive.length - 8} more (see report ch.1B)`));
|
|
806
1002
|
}
|
|
807
1003
|
|
|
1004
|
+
{
|
|
1005
|
+
const { buildInventory } = require("./lib/unmanaged");
|
|
1006
|
+
const inv = buildInventory(resolved);
|
|
1007
|
+
if (inv.length) {
|
|
1008
|
+
heading("Unmanaged binaries", inv.length);
|
|
1009
|
+
for (const e of inv.slice(0, 10)) {
|
|
1010
|
+
const id = e.identity ? `${e.identity.ecosystem ? e.identity.ecosystem + ":" : ""}${e.identity.name || ""}${e.identity.version ? "@" + e.identity.version : ""}` : chalk.dim("unknown");
|
|
1011
|
+
const flags = [e.knownMalicious ? chalk.bgRed.white(" malicious ") : null, e.nameMismatch ? chalk.yellow("name≠checksum") : null, e.shouldBeManaged ? chalk.cyan("should-be-managed") : null, (e.noOnlineInfo ? chalk.dim("unknown") : null)].filter(Boolean).join(" ");
|
|
1012
|
+
console.log(" " + chalk.white(path.basename(String(e.path))) + " " + chalk.dim(id) + (flags ? " " + flags : ""));
|
|
1013
|
+
}
|
|
1014
|
+
if (inv.length > 10) console.log(chalk.dim(` …and ${inv.length - 10} more (see report ch.1C)`));
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
808
1018
|
heading("EOL frameworks", eolResults.length);
|
|
809
|
-
for (const e of eolResults.slice(0, 8)) console.log(" " + chalk.yellow(e.product.padEnd(18)) + " " + chalk.dim(`${coordOf(e.dep)}:${e.dep.version}`) + " " + chalk.dim(e.eol === true ? "EOL" : String(e.eol)));
|
|
1019
|
+
for (const e of eolResults.slice(0, 8)) console.log(" " + chalk.yellow(e.product.padEnd(18)) + " " + chalk.dim(`${coordOf(e.dep)}:${e.dep.version}`) + " " + chalk.dim(e.eol === true ? "EOL" : String(e.eol)) + definedInOf(e.dep));
|
|
810
1020
|
if (eolResults.length > 8) console.log(chalk.dim(` …and ${eolResults.length - 8} more`));
|
|
811
1021
|
|
|
812
1022
|
heading("Obsolete / deprecated", obsoleteResults.length);
|
|
813
|
-
for (const o of obsoleteResults.slice(0, 8)) console.log(" " + chalk.dim(`${coordOf(o.dep)}:${o.dep.version}`) + " → " + (o.replacement || chalk.dim("n/a")));
|
|
1023
|
+
for (const o of obsoleteResults.slice(0, 8)) console.log(" " + chalk.dim(`${coordOf(o.dep)}:${o.dep.version}`) + " → " + (o.replacement || chalk.dim("n/a")) + definedInOf(o.dep));
|
|
814
1024
|
if (obsoleteResults.length > 8) console.log(chalk.dim(` …and ${obsoleteResults.length - 8} more`));
|
|
815
1025
|
|
|
816
1026
|
heading("Outdated", outdatedResults.length, options.allLibs ? "" : chalk.dim("pass -a/--allLibs to query registries"));
|
|
817
|
-
for (const o of outdatedResults.slice(0, 8)) console.log(" " + chalk.dim(coordOf(o.dep)) + ` ${o.dep.version} → ${chalk.green(o.latest)}`);
|
|
1027
|
+
for (const o of outdatedResults.slice(0, 8)) console.log(" " + chalk.dim(coordOf(o.dep)) + ` ${o.dep.version} → ${chalk.green(o.latest)}` + definedInOf(o.dep));
|
|
818
1028
|
if (outdatedResults.length > 8) console.log(chalk.dim(` …and ${outdatedResults.length - 8} more`));
|
|
819
1029
|
|
|
820
1030
|
if (retireMatches.length) {
|
|
@@ -908,7 +1118,7 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
908
1118
|
if (out.html || out.doc) {
|
|
909
1119
|
await ensureDir(out.html); await ensureDir(out.doc);
|
|
910
1120
|
const { htmlPath, docPath } = await writeReports({
|
|
911
|
-
cveMatches: prodMatches, devCveMatches: devMatches, embeddedMatches, retireMatches,
|
|
1121
|
+
cveMatches: prodMatches, devCveMatches: devMatches, embeddedMatches, retireMatches, vendoredJsInventory,
|
|
912
1122
|
eolResults, obsoleteResults, outdatedResults, licenseResults,
|
|
913
1123
|
resolvedDeps: resolved, projectInfo, warnings: reportWarnings,
|
|
914
1124
|
htmlPath: out.html, docPath: out.doc,
|
|
@@ -939,7 +1149,7 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
939
1149
|
try {
|
|
940
1150
|
const { writeFindings } = require("./lib/json-export");
|
|
941
1151
|
await ensureDir(out.json);
|
|
942
|
-
writeFindings({ cveMatches, retireMatches, eolResults, obsoleteResults, outdatedResults, licenseResults, resolvedDeps: resolved, projectInfo, toolVersion: pkg.version }, out.json);
|
|
1152
|
+
writeFindings({ cveMatches, retireMatches, vendoredJsInventory, eolResults, obsoleteResults, outdatedResults, licenseResults, resolvedDeps: resolved, projectInfo, toolVersion: pkg.version }, out.json);
|
|
943
1153
|
wrote.push(["Findings JSON", out.json]);
|
|
944
1154
|
} catch (err) { ui.warn(`JSON export failed: ${err.message}`); }
|
|
945
1155
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/codecs/binary/scan.js — walk a source tree for committed NATIVE binaries
|
|
3
|
+
* (.dll/.exe/.so/.dylib) and hash each. Dependency archives (.jar/.war/.ear) are
|
|
4
|
+
* owned by the Maven codec's jar-scan.js, not here.
|
|
5
|
+
*
|
|
6
|
+
* Selection requires BOTH an allowlisted extension AND a confirming magic byte
|
|
7
|
+
* (sniff.js), so images/fonts/assets — even with a spoofed extension — are never
|
|
8
|
+
* hashed or reported.
|
|
9
|
+
*
|
|
10
|
+
* @author: N.BRAUN
|
|
11
|
+
* @email: pp9ping@gmail.com
|
|
12
|
+
*/
|
|
13
|
+
const fs = require("fs");
|
|
14
|
+
const path = require("path");
|
|
15
|
+
const crypto = require("crypto");
|
|
16
|
+
const { sniffKind, extKind } = require("./sniff");
|
|
17
|
+
|
|
18
|
+
// Same skip set the other codecs use; never a place vendored binaries we own live.
|
|
19
|
+
const SKIP = new Set([
|
|
20
|
+
".git", ".idea", ".vscode", "node_modules", "dist", "build", "out",
|
|
21
|
+
"target", "vendor", "testdata", ".svn", ".hg", ".gradle", ".cache",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const MAGIC_BYTES = 8; // enough for every signature we sniff
|
|
25
|
+
|
|
26
|
+
function hashFile(fp) {
|
|
27
|
+
const buf = fs.readFileSync(fp);
|
|
28
|
+
return {
|
|
29
|
+
size: buf.length,
|
|
30
|
+
sha1: crypto.createHash("sha1").update(buf).digest("hex"),
|
|
31
|
+
sha256: crypto.createHash("sha256").update(buf).digest("hex"),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readMagic(fp) {
|
|
36
|
+
let fd;
|
|
37
|
+
try {
|
|
38
|
+
fd = fs.openSync(fp, "r");
|
|
39
|
+
const buf = Buffer.alloc(MAGIC_BYTES);
|
|
40
|
+
const n = fs.readSync(fd, buf, 0, MAGIC_BYTES, 0);
|
|
41
|
+
return buf.subarray(0, n);
|
|
42
|
+
} catch { return Buffer.alloc(0); }
|
|
43
|
+
finally { if (fd !== undefined) try { fs.closeSync(fd); } catch { /* ignore */ } }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Walk `dir`, return [{ path, kind, size, sha1, sha256, declaredName }]. */
|
|
47
|
+
function scanBinaries(dir, opts = {}) {
|
|
48
|
+
const { onProgress } = opts;
|
|
49
|
+
const { makeDirFilter } = require("../../path-filter");
|
|
50
|
+
const skipDir = makeDirFilter({ srcRoot: opts.srcRoot || dir, defaultSkip: SKIP, excludePath: opts.excludePath, useDefaults: opts.defaultExcludes !== false });
|
|
51
|
+
const out = [];
|
|
52
|
+
const stack = [dir];
|
|
53
|
+
while (stack.length) {
|
|
54
|
+
const cur = stack.pop();
|
|
55
|
+
let entries; try { entries = fs.readdirSync(cur, { withFileTypes: true }); } catch { continue; }
|
|
56
|
+
for (const e of entries) {
|
|
57
|
+
const fp = path.join(cur, e.name);
|
|
58
|
+
if (e.isDirectory()) { if (!skipDir(fp, e.name)) stack.push(fp); continue; }
|
|
59
|
+
if (!e.isFile()) continue;
|
|
60
|
+
const ext = extKind(e.name);
|
|
61
|
+
if (!ext) continue; // not an allowlisted extension
|
|
62
|
+
if (sniffKind(readMagic(fp)) !== ext) continue; // magic must confirm the extension
|
|
63
|
+
if (onProgress) onProgress(fp);
|
|
64
|
+
const h = hashFile(fp);
|
|
65
|
+
out.push({ path: fp, kind: ext, size: h.size, sha1: h.sha1, sha256: h.sha256, declaredName: e.name });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { scanBinaries };
|