fad-checker 2.1.2 → 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 +93 -20
- package/fad-checker.js +164 -45
- 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 +9 -4
- package/lib/codecs/maven/jar-scan.js +8 -3
- package/lib/codecs/maven.codec.js +4 -2
- package/lib/codecs/npm/parse.js +5 -2
- package/lib/codecs/npm/registry.js +29 -18
- package/lib/codecs/npm.codec.js +7 -5
- 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 +9 -5
- package/lib/cve-match.js +3 -2
- package/lib/cve-report.js +56 -5
- package/lib/dep-record.js +12 -9
- package/lib/hash-id.js +78 -0
- package/lib/json-export.js +8 -1
- package/lib/options-env.js +113 -0
- package/lib/parallel-walk.js +5 -2
- 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
|
@@ -61,8 +61,9 @@ if (process.argv.includes("--show-config")) {
|
|
|
61
61
|
const cfg = config.load();
|
|
62
62
|
const masked = { ...cfg };
|
|
63
63
|
if (masked.nvd_api_key) masked.nvd_api_key = masked.nvd_api_key.slice(0, 8) + "…" + masked.nvd_api_key.slice(-4);
|
|
64
|
-
if (
|
|
65
|
-
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 }))]));
|
|
66
67
|
}
|
|
67
68
|
console.log(JSON.stringify(masked, null, 2));
|
|
68
69
|
console.log(chalk.gray("Config file: " + config.CONFIG_PATH));
|
|
@@ -72,44 +73,58 @@ if (process.argv.includes("--show-config")) {
|
|
|
72
73
|
// -------- --add-repo / --remove-repo / --list-repos (run before program.parse) --------
|
|
73
74
|
if (process.argv.includes("--add-repo") || process.argv.includes("--remove-repo") || process.argv.includes("--list-repos")) {
|
|
74
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
|
+
};
|
|
75
83
|
if (process.argv.includes("--list-repos")) {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
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)."));
|
|
79
88
|
} else {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
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
|
+
}
|
|
84
95
|
}
|
|
85
96
|
}
|
|
86
97
|
process.exit(0);
|
|
87
98
|
}
|
|
88
99
|
if (process.argv.includes("--add-repo")) {
|
|
89
100
|
const idx = process.argv.indexOf("--add-repo");
|
|
90
|
-
const name = process.argv[idx + 1];
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
console.error(
|
|
94
|
-
console.error("
|
|
95
|
-
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");
|
|
96
106
|
process.exit(1);
|
|
97
107
|
}
|
|
108
|
+
ecoErr(eco);
|
|
98
109
|
const authIdx = process.argv.indexOf("--auth");
|
|
99
|
-
const
|
|
100
|
-
config.
|
|
101
|
-
|
|
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}`));
|
|
102
116
|
process.exit(0);
|
|
103
117
|
}
|
|
104
118
|
if (process.argv.includes("--remove-repo")) {
|
|
105
119
|
const idx = process.argv.indexOf("--remove-repo");
|
|
106
|
-
const name = process.argv[idx + 1];
|
|
107
|
-
if (!name || name.startsWith("-")) {
|
|
108
|
-
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>"));
|
|
109
123
|
process.exit(1);
|
|
110
124
|
}
|
|
111
|
-
|
|
112
|
-
|
|
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}"`));
|
|
113
128
|
process.exit(removed ? 0 : 1);
|
|
114
129
|
}
|
|
115
130
|
}
|
|
@@ -169,7 +184,11 @@ program
|
|
|
169
184
|
.option("-t, --target <target>", "output directory (will be rm before written). If omitted, the run is read-only.")
|
|
170
185
|
// Not a requiredOption: --import-anonymized scans a descriptor with no source tree.
|
|
171
186
|
.option("-s, --src <src>", "root directory containing pom.xml files")
|
|
172
|
-
.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")
|
|
173
192
|
.option("-v, --verbose", "verbose")
|
|
174
193
|
// Defaults: report + transitive + allLibs all ON. Use --no-* to disable.
|
|
175
194
|
.option("--no-report", "write NO output files at all — the scan, terminal summary and --fail-on gate still run (gate-only / CI mode)")
|
|
@@ -207,6 +226,7 @@ program
|
|
|
207
226
|
.option("--cve-offline", "use cached CVE index only (no download)")
|
|
208
227
|
.option("--snyk", "run snyk on cleaned POMs and merge into report (requires --target)")
|
|
209
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)")
|
|
210
230
|
.option("--retire-refresh", "ignore retire cache and re-scan")
|
|
211
231
|
.option("--transitive-depth <n>", "max transitive depth", "6")
|
|
212
232
|
.option("--ecosystem <list>", "codecs to run: auto|all|<comma list> e.g. maven,npm,nuget,composer,pypi,go,ruby (default: auto = detected)", "auto")
|
|
@@ -218,16 +238,35 @@ program
|
|
|
218
238
|
.option("--no-pypi", "skip the PyPI (Python) codec")
|
|
219
239
|
.option("--no-go", "skip the Go codec")
|
|
220
240
|
.option("--no-ruby", "skip the Ruby (Bundler) codec")
|
|
241
|
+
.option("--no-binaries", "skip scanning committed native binaries (.dll/.exe/.so/.dylib)")
|
|
221
242
|
.option("--no-jars", "skip scanning embedded .jar/.war/.ear binaries for Maven coordinates")
|
|
222
243
|
.option("--no-js", "alias: skip JS/npm/yarn manifests even if present (Maven-only)")
|
|
223
|
-
.option("--repo <url...>", "extra
|
|
224
|
-
.option("--add-repo <
|
|
225
|
-
.option("--remove-repo <
|
|
226
|
-
.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")
|
|
227
250
|
.option("--completion <shell>", "print shell completion script (bash|zsh)");
|
|
228
251
|
program.parse(process.argv);
|
|
229
252
|
|
|
230
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
|
+
|
|
231
270
|
const deps2Exclude = options.exclude ? new RegExp(options.exclude) : null;
|
|
232
271
|
const verbose = !!options.verbose;
|
|
233
272
|
|
|
@@ -362,13 +401,44 @@ async function timedPhase(label, fn) {
|
|
|
362
401
|
// Build the Maven repo list once: persisted repos (from ~/.fad-checker/config.json)
|
|
363
402
|
// + ad-hoc --repo URLs + Maven Central as final fallback. Used by transitive
|
|
364
403
|
// resolution, outdated-version check, and existence check.
|
|
365
|
-
const {
|
|
404
|
+
const { getRegistryMap } = require("./lib/config");
|
|
366
405
|
const { buildRepoList } = require("./lib/maven-repo");
|
|
367
|
-
const
|
|
368
|
-
|
|
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 };
|
|
369
435
|
const runMode = options.importAnonymized ? "import descriptor" : (options.offline ? "offline" : "online");
|
|
370
436
|
if (options.src) ui.kv("source", chalk.white(options.src));
|
|
371
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/…)"));
|
|
372
442
|
ui.kv("mode", chalk.white(runMode));
|
|
373
443
|
|
|
374
444
|
let wrotePom = 0;
|
|
@@ -394,7 +464,7 @@ async function timedPhase(label, fn) {
|
|
|
394
464
|
const { warmRetireSignatures } = require("./lib/retire");
|
|
395
465
|
await warmRetireSignatures({ verbose });
|
|
396
466
|
}
|
|
397
|
-
await runReportFlow(resolved, { activeIds, runMaven, runNpm, privateLibIds: [], mavenRepos, collectWarnings: [] });
|
|
467
|
+
await runReportFlow(resolved, { activeIds, runMaven, runNpm, privateLibIds: [], mavenRepos, regMap, collectWarnings: [] });
|
|
398
468
|
return;
|
|
399
469
|
}
|
|
400
470
|
|
|
@@ -403,9 +473,15 @@ async function timedPhase(label, fn) {
|
|
|
403
473
|
const { resolveActiveCodecs } = require("./lib/codecs/select");
|
|
404
474
|
const eco = (options.ecosystem || "auto").toLowerCase();
|
|
405
475
|
const detected = (eco === "auto")
|
|
406
|
-
? (await timedPhase("detecting ecosystems", () => detectCodecs(options.src))).map(c => c.id)
|
|
476
|
+
? (await timedPhase("detecting ecosystems", () => detectCodecs(options.src, walkOpts))).map(c => c.id)
|
|
407
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");
|
|
408
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");
|
|
409
485
|
const activeIds = resolveActiveCodecs(eco, detected, { noCodecs, noJs: !options.js });
|
|
410
486
|
const runMaven = activeIds.includes("maven");
|
|
411
487
|
const runNpm = activeIds.includes("npm") || activeIds.includes("yarn");
|
|
@@ -422,7 +498,7 @@ async function timedPhase(label, fn) {
|
|
|
422
498
|
const codec = getCodec(id);
|
|
423
499
|
let res;
|
|
424
500
|
try {
|
|
425
|
-
res = await timedPhase(`collecting ${codec.label || id}`, () => codec.collect(options.src, { ignoreTest: !!options.ignoreTest, deps2Exclude, verbose, scanJars: options.jars !== false, srcRoot: options.src, onJarProgress: makeJarProgress() }));
|
|
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 }));
|
|
426
502
|
} catch (err) {
|
|
427
503
|
console.warn(chalk.red(`❌ ${id} collect failed:`), chalk.dim(err.message));
|
|
428
504
|
continue;
|
|
@@ -435,8 +511,10 @@ async function timedPhase(label, fn) {
|
|
|
435
511
|
// --- Collection summary ---
|
|
436
512
|
const ecoCount = {};
|
|
437
513
|
let embeddedCount = 0;
|
|
514
|
+
let binaryCount = 0;
|
|
438
515
|
for (const d of resolved.values()) {
|
|
439
516
|
if (d.provenance === "embedded") { embeddedCount++; continue; } // counted separately below
|
|
517
|
+
if (d.provenance === "binary") { binaryCount++; continue; } // committed native libs, no manifest
|
|
440
518
|
ecoCount[d.ecosystem] = (ecoCount[d.ecosystem] || 0) + 1;
|
|
441
519
|
}
|
|
442
520
|
if (runMaven) ui.ok(`${chalk.bold("Maven".padEnd(8))} ${mavenCtx ? mavenCtx.pomFiles.length + " module(s) · " : ""}${ecoCount.maven || 0} direct dep(s)`);
|
|
@@ -446,6 +524,7 @@ async function timedPhase(label, fn) {
|
|
|
446
524
|
ui.ok(`${chalk.bold(((getCodec(id)?.label) || id).padEnd(8))} ${n} dep(s)`);
|
|
447
525
|
}
|
|
448
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)`);
|
|
449
528
|
if (!ecoCount.maven && !ecoCount.npm && !Object.keys(ecoCount).length) ui.warn("no dependencies found in the source tree");
|
|
450
529
|
if (collectWarnings.length) {
|
|
451
530
|
ui.warn(`${collectWarnings.length} manifest warning(s) — best-effort / no lockfile:`);
|
|
@@ -567,7 +646,7 @@ async function timedPhase(label, fn) {
|
|
|
567
646
|
// The scan always runs — it feeds the terminal summary, the file outputs and the
|
|
568
647
|
// CI gate. Which files get written is decided by the --report-* family inside
|
|
569
648
|
// (HTML + .doc by default; --no-report writes nothing).
|
|
570
|
-
await runReportFlow(resolved, { activeIds, runMaven, runNpm, privateLibIds, mavenRepos, collectWarnings });
|
|
649
|
+
await runReportFlow(resolved, { activeIds, runMaven, runNpm, privateLibIds, mavenRepos, regMap, collectWarnings });
|
|
571
650
|
if (!readOnly) {
|
|
572
651
|
ui.section("Next step");
|
|
573
652
|
ui.info(`run Snyk on the cleaned tree:`);
|
|
@@ -576,7 +655,8 @@ async function timedPhase(label, fn) {
|
|
|
576
655
|
})();
|
|
577
656
|
|
|
578
657
|
async function runReportFlow(resolved, ecoFlags = {}) {
|
|
579
|
-
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] || [];
|
|
580
660
|
const { expandWithTransitives } = require("./lib/cve-match");
|
|
581
661
|
const { writeReports, computeStats } = require("./lib/cve-report");
|
|
582
662
|
const { getCodec } = require("./lib/codecs");
|
|
@@ -621,9 +701,11 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
621
701
|
const willKev = !!options.kev;
|
|
622
702
|
const willLicenses = !!options.licenses;
|
|
623
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");
|
|
624
706
|
// License detection piggybacks on the registry passes (same fetched metadata),
|
|
625
707
|
// so it adds no progress step of its own.
|
|
626
|
-
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;
|
|
627
709
|
const progress = new ui.Progress(totalSteps);
|
|
628
710
|
|
|
629
711
|
if (willTransitive) {
|
|
@@ -653,6 +735,17 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
653
735
|
}
|
|
654
736
|
}
|
|
655
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
|
+
|
|
656
749
|
// 2. EOL frameworks (endoflife.date) — always a step.
|
|
657
750
|
let eolResults = [];
|
|
658
751
|
{
|
|
@@ -686,7 +779,7 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
686
779
|
const st = progress.start("npm registry");
|
|
687
780
|
try {
|
|
688
781
|
const { checkNpmRegistryDeps } = require("./lib/codecs/npm/registry");
|
|
689
|
-
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) });
|
|
690
783
|
obsoleteResults = obsoleteResults.concat(npmReg.deprecated);
|
|
691
784
|
outdatedResults = outdatedResults.concat(npmReg.outdated);
|
|
692
785
|
licenseFindings = licenseFindings.concat(npmReg.licensed || []);
|
|
@@ -699,7 +792,7 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
699
792
|
const codec = getCodec(id);
|
|
700
793
|
const st = progress.start(`${codec.label || id} registry`);
|
|
701
794
|
try {
|
|
702
|
-
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) });
|
|
703
796
|
obsoleteResults = obsoleteResults.concat(reg.deprecated || []);
|
|
704
797
|
outdatedResults = outdatedResults.concat(reg.outdated || []);
|
|
705
798
|
licenseFindings = licenseFindings.concat(reg.licensed || []);
|
|
@@ -793,6 +886,7 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
793
886
|
// vendored .js (which can live in a Maven project's resources too). The
|
|
794
887
|
// scanner is owned by the npm codec but runs whenever --retire is on.
|
|
795
888
|
let retireMatches = [];
|
|
889
|
+
let vendoredJsInventory = [];
|
|
796
890
|
if (willRetire) {
|
|
797
891
|
const st = progress.start("retire.js (vendored JS)");
|
|
798
892
|
const sc = (getCodec("npm").nativeScanners || []).find(s => s.kind === "vendored");
|
|
@@ -802,7 +896,9 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
802
896
|
try {
|
|
803
897
|
const r = await sc.scan(resolved, { src: options.src, verbose, retireRefresh: !!options.retireRefresh, offline });
|
|
804
898
|
retireMatches = r.matches || [];
|
|
805
|
-
|
|
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` : ""}`);
|
|
806
902
|
} catch (err) { st.fail(err.message); }
|
|
807
903
|
}
|
|
808
904
|
}
|
|
@@ -865,6 +961,15 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
865
961
|
const sev = ui.sevColor;
|
|
866
962
|
const depLabel = d => d.ecosystem === "npm" ? `npm:${d.artifactId}` : `${d.groupId}:${d.artifactId}`;
|
|
867
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
|
+
};
|
|
868
973
|
const fmtStats = s => [
|
|
869
974
|
s.critical ? sev("CRITICAL")(`${s.critical} critical`) : null,
|
|
870
975
|
s.high ? sev("HIGH")(`${s.high} high`) : null,
|
|
@@ -896,16 +1001,30 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
896
1001
|
if (embeddedActive.length > 8) console.log(chalk.dim(` …and ${embeddedActive.length - 8} more (see report ch.1B)`));
|
|
897
1002
|
}
|
|
898
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
|
+
|
|
899
1018
|
heading("EOL frameworks", eolResults.length);
|
|
900
|
-
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));
|
|
901
1020
|
if (eolResults.length > 8) console.log(chalk.dim(` …and ${eolResults.length - 8} more`));
|
|
902
1021
|
|
|
903
1022
|
heading("Obsolete / deprecated", obsoleteResults.length);
|
|
904
|
-
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));
|
|
905
1024
|
if (obsoleteResults.length > 8) console.log(chalk.dim(` …and ${obsoleteResults.length - 8} more`));
|
|
906
1025
|
|
|
907
1026
|
heading("Outdated", outdatedResults.length, options.allLibs ? "" : chalk.dim("pass -a/--allLibs to query registries"));
|
|
908
|
-
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));
|
|
909
1028
|
if (outdatedResults.length > 8) console.log(chalk.dim(` …and ${outdatedResults.length - 8} more`));
|
|
910
1029
|
|
|
911
1030
|
if (retireMatches.length) {
|
|
@@ -999,7 +1118,7 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
999
1118
|
if (out.html || out.doc) {
|
|
1000
1119
|
await ensureDir(out.html); await ensureDir(out.doc);
|
|
1001
1120
|
const { htmlPath, docPath } = await writeReports({
|
|
1002
|
-
cveMatches: prodMatches, devCveMatches: devMatches, embeddedMatches, retireMatches,
|
|
1121
|
+
cveMatches: prodMatches, devCveMatches: devMatches, embeddedMatches, retireMatches, vendoredJsInventory,
|
|
1003
1122
|
eolResults, obsoleteResults, outdatedResults, licenseResults,
|
|
1004
1123
|
resolvedDeps: resolved, projectInfo, warnings: reportWarnings,
|
|
1005
1124
|
htmlPath: out.html, docPath: out.doc,
|
|
@@ -1030,7 +1149,7 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
1030
1149
|
try {
|
|
1031
1150
|
const { writeFindings } = require("./lib/json-export");
|
|
1032
1151
|
await ensureDir(out.json);
|
|
1033
|
-
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);
|
|
1034
1153
|
wrote.push(["Findings JSON", out.json]);
|
|
1035
1154
|
} catch (err) { ui.warn(`JSON export failed: ${err.message}`); }
|
|
1036
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 };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/codecs/binary/sniff.js — file-type confirmation for the binary codec.
|
|
3
|
+
*
|
|
4
|
+
* Two gates: extKind() (extension allowlist) AND sniffKind() (magic bytes). A
|
|
5
|
+
* candidate is accepted only when both agree, so an image renamed `.so` (PNG
|
|
6
|
+
* magic) is rejected. We never trust the extension alone.
|
|
7
|
+
*
|
|
8
|
+
* @author: N.BRAUN
|
|
9
|
+
* @email: pp9ping@gmail.com
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Magic-byte signatures → family. Mach-O has four (32/64-bit + fat, both endian).
|
|
13
|
+
function sniffKind(buf) {
|
|
14
|
+
if (!buf || buf.length < 4) return null;
|
|
15
|
+
const b0 = buf[0], b1 = buf[1], b2 = buf[2], b3 = buf[3];
|
|
16
|
+
if (b0 === 0x4d && b1 === 0x5a) return "pe"; // MZ
|
|
17
|
+
if (b0 === 0x7f && b1 === 0x45 && b2 === 0x4c && b3 === 0x46) return "elf"; // \x7FELF
|
|
18
|
+
if (b0 === 0x50 && b1 === 0x4b && b2 === 0x03 && b3 === 0x04) return "zip"; // PK\x03\x04
|
|
19
|
+
const be = ((b0 << 24) | (b1 << 16) | (b2 << 8) | b3) >>> 0;
|
|
20
|
+
const le = ((b3 << 24) | (b2 << 16) | (b1 << 8) | b0) >>> 0;
|
|
21
|
+
const machO = new Set([0xfeedface, 0xfeedfacf, 0xcafebabe, 0xcafebabf]);
|
|
22
|
+
if (machO.has(be) || machO.has(le)) return "macho";
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const EXT_PE = /\.(dll|exe)$/i;
|
|
27
|
+
const EXT_MACHO = /\.dylib$/i;
|
|
28
|
+
const EXT_ELF = /\.so(\.\d+)*$/i; // .so, .so.1, .so.1.2.3
|
|
29
|
+
|
|
30
|
+
function extKind(name) {
|
|
31
|
+
if (EXT_PE.test(name)) return "pe";
|
|
32
|
+
if (EXT_MACHO.test(name)) return "macho";
|
|
33
|
+
if (EXT_ELF.test(name)) return "elf";
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { sniffKind, extKind };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/codecs/binary.codec.js — codec for committed NATIVE binaries
|
|
3
|
+
* (.dll/.exe/.so/.dylib) that no package manager governs.
|
|
4
|
+
*
|
|
5
|
+
* Plan 1 scope: discover + hash only. The records carry no resolved coordinate
|
|
6
|
+
* (just a filename); Plan 2's hash-id service fills `identity`, Plan 3 builds the
|
|
7
|
+
* unmanaged inventory + report. Until then the records are `provenance:"binary"`
|
|
8
|
+
* and the CVE/OSV/EOL/outdated stages skip them (no coordinate to query).
|
|
9
|
+
*
|
|
10
|
+
* @author: N.BRAUN
|
|
11
|
+
* @email: pp9ping@gmail.com
|
|
12
|
+
*/
|
|
13
|
+
const { makeDepRecord } = require("../dep-record");
|
|
14
|
+
const { scanBinaries } = require("./binary/scan");
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
id: "binary",
|
|
18
|
+
label: "Binaries",
|
|
19
|
+
osvEcosystem: null, // no OSV ecosystem until identified
|
|
20
|
+
manifestNames: ["*.dll", "*.exe", "*.so", "*.dylib"], // lets detectCodecs include us in auto mode
|
|
21
|
+
|
|
22
|
+
detect(dir) {
|
|
23
|
+
try { return scanBinaries(dir, { onProgress: null }).length > 0; }
|
|
24
|
+
catch { return false; }
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
async collect(dir, opts = {}) {
|
|
28
|
+
const out = new Map();
|
|
29
|
+
const warnings = [];
|
|
30
|
+
let records;
|
|
31
|
+
try { records = scanBinaries(dir, { onProgress: opts.onBinaryProgress, srcRoot: opts.srcRoot || dir, excludePath: opts.excludePath, defaultExcludes: opts.defaultExcludes }); }
|
|
32
|
+
catch (e) { return { deps: out, warnings: [{ type: "scan-error", message: `binary scan failed: ${e.message}` }] }; }
|
|
33
|
+
for (const r of records) {
|
|
34
|
+
out.set(`binary:${r.path}`, makeDepRecord({
|
|
35
|
+
ecosystem: "binary",
|
|
36
|
+
name: r.declaredName,
|
|
37
|
+
version: null,
|
|
38
|
+
manifestPath: r.path,
|
|
39
|
+
provenance: "binary",
|
|
40
|
+
hashes: { sha1: r.sha1, sha256: r.sha256 },
|
|
41
|
+
declaredName: r.declaredName,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
return { deps: out, warnings };
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
coordKey(d) { return `binary:${d.manifestPaths?.[0] || d.name}`; },
|
|
48
|
+
formatCoord(d) { return d.declaredName || d.name; },
|
|
49
|
+
osvPackageName() { return null; },
|
|
50
|
+
async checkRegistry() { return { deprecated: [], outdated: [], licensed: [] }; },
|
|
51
|
+
resolveEolProduct() { return null; },
|
|
52
|
+
recipe: require("./recipes").binary,
|
|
53
|
+
nativeScanners: [],
|
|
54
|
+
};
|
|
@@ -15,7 +15,7 @@ const { parseComposerLock, parseComposerJson, isConcrete } = require("./composer
|
|
|
15
15
|
|
|
16
16
|
const SKIP = new Set(["vendor", ".git", ".idea", ".vscode", "node_modules", "dist", "build", "out", "target"]);
|
|
17
17
|
|
|
18
|
-
function findComposerManifests(dir) {
|
|
18
|
+
function findComposerManifests(dir, skipDir = (child, name) => SKIP.has(name)) {
|
|
19
19
|
const groups = [];
|
|
20
20
|
const stack = [dir];
|
|
21
21
|
while (stack.length) {
|
|
@@ -29,11 +29,15 @@ function findComposerManifests(dir) {
|
|
|
29
29
|
composerLock: names.has("composer.lock") ? path.join(cur, "composer.lock") : null,
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
|
-
for (const e of entries) if (e.isDirectory() && !
|
|
32
|
+
for (const e of entries) if (e.isDirectory() && !skipDir(path.join(cur, e.name), e.name)) stack.push(path.join(cur, e.name));
|
|
33
33
|
}
|
|
34
34
|
return groups;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function dirFilter(dir, opts) {
|
|
38
|
+
return require("../path-filter").makeDirFilter({ srcRoot: opts.srcRoot || dir, defaultSkip: SKIP, excludePath: opts.excludePath, useDefaults: opts.defaultExcludes !== false });
|
|
39
|
+
}
|
|
40
|
+
|
|
37
41
|
module.exports = {
|
|
38
42
|
id: "composer",
|
|
39
43
|
label: "Composer",
|
|
@@ -46,7 +50,7 @@ module.exports = {
|
|
|
46
50
|
const { ignoreTest, deps2Exclude } = opts;
|
|
47
51
|
const out = new Map();
|
|
48
52
|
const warnings = [];
|
|
49
|
-
for (const g of findComposerManifests(dir)) {
|
|
53
|
+
for (const g of findComposerManifests(dir, dirFilter(dir, opts))) {
|
|
50
54
|
if (g.composerLock) {
|
|
51
55
|
let parsed;
|
|
52
56
|
try { parsed = parseComposerLock(g.composerLock); }
|