fad-checker 2.0.0 → 2.1.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 +65 -0
- package/README.md +147 -25
- package/completions/fad-checker.bash +1 -1
- package/completions/fad-checker.zsh +14 -1
- package/data/eol-mapping.json +5 -5
- package/data/license-policy.json +97 -0
- package/fad-checker-report/cve-report.doc +630 -0
- package/fad-checker-report/cve-report.html +748 -0
- package/fad-checker.js +556 -245
- package/lib/cache-archive.js +3 -0
- package/lib/codecs/codec.interface.js +3 -0
- package/lib/codecs/composer/parse.js +3 -0
- package/lib/codecs/composer/registry.js +13 -5
- package/lib/codecs/composer.codec.js +3 -0
- package/lib/codecs/go/parse.js +65 -0
- package/lib/codecs/go/registry.js +74 -0
- package/lib/codecs/go.codec.js +76 -0
- package/lib/codecs/index.js +7 -2
- package/lib/codecs/maven/jar-scan.js +199 -0
- package/lib/codecs/maven.codec.js +17 -1
- package/lib/codecs/npm/collect.js +37 -17
- package/lib/codecs/npm/parse.js +117 -10
- package/lib/codecs/npm/registry.js +14 -5
- package/lib/codecs/npm.codec.js +4 -1
- package/lib/codecs/nuget/parse.js +3 -0
- package/lib/codecs/nuget/registry.js +11 -4
- package/lib/codecs/nuget.codec.js +7 -2
- package/lib/codecs/pypi/parse.js +111 -14
- package/lib/codecs/pypi/registry.js +24 -5
- package/lib/codecs/pypi.codec.js +30 -6
- package/lib/codecs/recipes.js +28 -1
- package/lib/codecs/ruby/parse.js +42 -0
- package/lib/codecs/ruby/registry.js +76 -0
- package/lib/codecs/ruby.codec.js +66 -0
- package/lib/codecs/select.js +3 -0
- package/lib/codecs/yarn.codec.js +3 -0
- package/lib/config.js +3 -0
- package/lib/core.js +38 -1
- package/lib/cpe.js +30 -5
- package/lib/csaf-export.js +159 -0
- package/lib/cve-download.js +3 -0
- package/lib/cve-match.js +27 -6
- package/lib/cve-report.js +157 -28
- package/lib/dep-record.js +15 -2
- package/lib/deps-descriptor.js +113 -0
- package/lib/epss.js +115 -0
- package/lib/gate.js +45 -0
- package/lib/json-export.js +110 -0
- package/lib/kev.js +88 -0
- package/lib/license-policy.js +110 -0
- package/lib/maven-license.js +52 -0
- package/lib/maven-repo.js +3 -0
- package/lib/maven-version.js +8 -3
- package/lib/nvd.js +17 -6
- package/lib/osv.js +75 -23
- package/lib/outdated.js +10 -2
- package/lib/priority.js +90 -0
- package/lib/purl.js +77 -0
- package/lib/retire.js +80 -13
- package/lib/sarif-export.js +134 -0
- package/lib/sbom-export.js +153 -0
- package/lib/scan-completeness.js +3 -0
- package/lib/snyk.js +3 -0
- package/lib/suppress.js +113 -0
- package/lib/transitive.js +6 -3
- package/lib/ui.js +90 -0
- package/package.json +49 -2
- package/CLAUDE.md +0 -147
- package/docs/ARCHITECTURE.md +0 -182
- package/docs/USAGE.md +0 -187
- package/docs/superpowers/plans/2026-05-29-codec-composer.md +0 -556
- package/docs/superpowers/plans/2026-05-29-codec-foundation.md +0 -851
- package/docs/superpowers/plans/2026-05-29-codec-nuget.md +0 -432
- package/docs/superpowers/plans/2026-05-29-codec-pypi.md +0 -450
- package/docs/superpowers/specs/2026-05-29-codecs-multi-ecosystem-design.md +0 -251
- package/test/cli-ecosystem.test.js +0 -30
- package/test/codec-edge-cases.test.js +0 -131
- package/test/codec-integration.test.js +0 -119
- package/test/codecs.test.js +0 -92
- package/test/composer.test.js +0 -71
- package/test/core.test.js +0 -153
- package/test/cpe.test.js +0 -166
- package/test/cve-download.test.js +0 -39
- package/test/cve-match.test.js +0 -217
- package/test/cve-report.test.js +0 -180
- package/test/dep-record.test.js +0 -31
- package/test/fixtures/complex-enterprise/api/pom.xml +0 -32
- package/test/fixtures/complex-enterprise/build/pom.xml +0 -46
- package/test/fixtures/complex-enterprise/dao/pom.xml +0 -37
- package/test/fixtures/complex-enterprise/pom.xml +0 -66
- package/test/fixtures/complex-enterprise/web/pom.xml +0 -77
- package/test/fixtures/csharp-config/packages.config +0 -4
- package/test/fixtures/csharp-csproj/Directory.Packages.props +0 -5
- package/test/fixtures/csharp-csproj/app.csproj +0 -7
- package/test/fixtures/csharp-lock/packages.lock.json +0 -6
- package/test/fixtures/cve-samples/cve-non-java.json +0 -19
- package/test/fixtures/cve-samples/cve-product-only.json +0 -31
- package/test/fixtures/cve-samples/cve-with-packagename.json +0 -37
- package/test/fixtures/cve-samples/nvd-log4shell.json +0 -40
- package/test/fixtures/cve-samples/nvd-npm-lodash.json +0 -22
- package/test/fixtures/monorepo-mixed/libs/common-bom/pom.xml +0 -26
- package/test/fixtures/monorepo-mixed/packages/cli/package.json +0 -14
- package/test/fixtures/monorepo-mixed/packages/cli/yarn.lock +0 -41
- package/test/fixtures/monorepo-mixed/packages/no-lock/package.json +0 -10
- package/test/fixtures/monorepo-mixed/packages/web-app/package-lock.json +0 -71
- package/test/fixtures/monorepo-mixed/packages/web-app/package.json +0 -17
- package/test/fixtures/monorepo-mixed/pom.xml +0 -29
- package/test/fixtures/monorepo-mixed/services/api/pom.xml +0 -27
- package/test/fixtures/monorepo-mixed/services/worker/pom.xml +0 -28
- package/test/fixtures/php-app/composer.json +0 -5
- package/test/fixtures/php-app/composer.lock +0 -10
- package/test/fixtures/polyglot/cs/packages.lock.json +0 -1
- package/test/fixtures/polyglot/js/package-lock.json +0 -1
- package/test/fixtures/polyglot/js/package.json +0 -1
- package/test/fixtures/polyglot/mvn/pom.xml +0 -7
- package/test/fixtures/polyglot/php/composer.lock +0 -1
- package/test/fixtures/polyglot/py/poetry.lock +0 -3
- package/test/fixtures/private-lib-detection/core/pom.xml +0 -26
- package/test/fixtures/private-lib-detection/plugin/pom.xml +0 -23
- package/test/fixtures/private-lib-detection/pom.xml +0 -35
- package/test/fixtures/python-pipenv/Pipfile.lock +0 -1
- package/test/fixtures/python-poetry/poetry.lock +0 -7
- package/test/fixtures/python-reqs/requirements.txt +0 -5
- package/test/fixtures/python-uv/uv.lock +0 -3
- package/test/fixtures/simple/app/pom.xml +0 -28
- package/test/fixtures/simple/lib/pom.xml +0 -18
- package/test/fixtures/simple/pom.xml +0 -24
- package/test/maven-repo.test.js +0 -111
- package/test/maven-version.test.js +0 -57
- package/test/monorepo.test.js +0 -132
- package/test/npm-registry.test.js +0 -64
- package/test/npm.test.js +0 -150
- package/test/nuget.test.js +0 -66
- package/test/osv.test.js +0 -62
- package/test/outdated.test.js +0 -101
- package/test/pypi.test.js +0 -72
- package/test/snyk.test.js +0 -64
- package/test/transitive.test.js +0 -305
- package/test/webjar.test.js +0 -33
package/fad-checker.js
CHANGED
|
@@ -16,6 +16,7 @@ const { rimraf } = require("rimraf");
|
|
|
16
16
|
const chalk = require("chalk");
|
|
17
17
|
const pLimit = require("p-limit");
|
|
18
18
|
const { program } = require("commander");
|
|
19
|
+
const ui = require("./lib/ui");
|
|
19
20
|
|
|
20
21
|
const core = require("./lib/core");
|
|
21
22
|
|
|
@@ -163,21 +164,39 @@ program
|
|
|
163
164
|
.showHelpAfterError()
|
|
164
165
|
.usage(USAGE)
|
|
165
166
|
.option("-t, --target <target>", "output directory (will be rm before written). If omitted, the run is read-only.")
|
|
166
|
-
|
|
167
|
+
// Not a requiredOption: --import-anonymized scans a descriptor with no source tree.
|
|
168
|
+
.option("-s, --src <src>", "root directory containing pom.xml files")
|
|
167
169
|
.option("-e, --exclude <exclude>", "regex of groupId to exclude, e.g. '^(client|private)\\.'")
|
|
168
170
|
.option("-v, --verbose", "verbose")
|
|
169
171
|
// Defaults: report + transitive + allLibs all ON. Use --no-* to disable.
|
|
170
|
-
.option("--no-report", "
|
|
172
|
+
.option("--no-report", "write NO output files at all — the scan, terminal summary and --fail-on gate still run (gate-only / CI mode)")
|
|
171
173
|
.option("--no-transitive", "skip transitive dependency resolution")
|
|
172
174
|
.option("--no-all-libs", "skip Maven Central queries (outdated check + missing-on-central check)")
|
|
173
175
|
.option("--no-osv", "skip OSV.dev (Google/GitHub aggregated Maven CVE feed)")
|
|
174
176
|
.option("--no-nvd", "skip NIST NVD enrichment of matched CVEs")
|
|
175
|
-
.option("--
|
|
177
|
+
.option("--no-epss", "skip EPSS (FIRST.org exploit-prediction) enrichment")
|
|
178
|
+
.option("--no-kev", "skip CISA KEV (known-exploited) enrichment")
|
|
179
|
+
// Output family: each --report-<type> takes an OPTIONAL path (omit → default name
|
|
180
|
+
// under --report-output). With NO --report-* flag at all, HTML + .doc are written
|
|
181
|
+
// by default. --no-report writes nothing (scan + gate only).
|
|
182
|
+
.option("--report-html [file]", "write the self-contained HTML report (default: <report-output>/cve-report.html)")
|
|
183
|
+
.option("--report-doc [file]", "write the Word-compatible .doc report (default: <report-output>/cve-report.doc)")
|
|
184
|
+
.option("--report-sbom [file]", "write a CycloneDX 1.6 SBOM, vulnerabilities inline (default: <report-output>/sbom.cdx.json)")
|
|
185
|
+
.option("--report-csaf [file]", "write a CSAF 2.0 VEX document (default: <report-output>/csaf-vex.json)")
|
|
186
|
+
.option("--report-json [file]", "write a flat machine-readable findings JSON (default: <report-output>/findings.json)")
|
|
187
|
+
.option("--report-sarif [file]", "write a SARIF 2.1.0 log for GitHub/GitLab code scanning (default: <report-output>/fad.sarif)")
|
|
188
|
+
.option("--fail-on <level>", "exit non-zero if a production finding meets <level>: low|medium|high|critical|kev|none", "none")
|
|
189
|
+
.option("--ignore <file>", "suppress findings listed in <file> (CVE ids / coords / globs, one per line)")
|
|
190
|
+
.option("--vex <file>", "ingest a CSAF VEX: suppress CVEs marked not_affected/fixed")
|
|
191
|
+
.option("--no-licenses", "skip license detection + copyleft policy check")
|
|
192
|
+
.option("--offline", "no network: use cached CVE/OSV/NVD/EPSS/KEV/POM data only")
|
|
176
193
|
.option("--set-nvd-key <key>", "save NVD API key to ~/.fad-checker/config.json (10× faster NVD enrichment)")
|
|
177
194
|
.option("--show-config", "print the persisted ~/.fad-checker/config.json")
|
|
178
195
|
.option("--export-cache <file>", "tar.gz/zip the ~/.fad-checker/ caches to <file> (excludes config.json by default)")
|
|
179
196
|
.option("--import-cache <file>", "restore ~/.fad-checker/ from a previously exported archive (existing dir is moved to .bak unless --force)")
|
|
180
197
|
.option("--include-config", "with --export-cache: also bundle config.json (contains the NVD API key)")
|
|
198
|
+
.option("--export-anonymized <file>", "offline: write an anonymized dependency descriptor (public coordinates only, no paths/URLs) for PASSI audits, then exit")
|
|
199
|
+
.option("--import-anonymized <file>", "online: scan an anonymized descriptor (no --src) to warm the caches; pair with --export-cache for offline reporting")
|
|
181
200
|
.option("--force", "with --import-cache: replace ~/.fad-checker/ without backup")
|
|
182
201
|
.option("--report-output <dir>", "report output directory", "./fad-checker-report")
|
|
183
202
|
.option("--ignore-test", "skip test-scoped dependencies in report")
|
|
@@ -187,13 +206,16 @@ program
|
|
|
187
206
|
.option("--no-retire", "skip retire.js vendored-JS scan")
|
|
188
207
|
.option("--retire-refresh", "ignore retire cache and re-scan")
|
|
189
208
|
.option("--transitive-depth <n>", "max transitive depth", "6")
|
|
190
|
-
.option("--ecosystem <list>", "codecs to run: auto|all|<comma list> e.g. maven,npm,nuget,composer,pypi (default: auto = detected)", "auto")
|
|
209
|
+
.option("--ecosystem <list>", "codecs to run: auto|all|<comma list> e.g. maven,npm,nuget,composer,pypi,go,ruby (default: auto = detected)", "auto")
|
|
191
210
|
.option("--no-maven", "skip the Maven codec")
|
|
192
211
|
.option("--no-npm", "skip the npm codec")
|
|
193
212
|
.option("--no-yarn", "skip the Yarn codec")
|
|
194
213
|
.option("--no-nuget", "skip the NuGet (C#/.NET) codec")
|
|
195
214
|
.option("--no-composer", "skip the Composer (PHP) codec")
|
|
196
215
|
.option("--no-pypi", "skip the PyPI (Python) codec")
|
|
216
|
+
.option("--no-go", "skip the Go codec")
|
|
217
|
+
.option("--no-ruby", "skip the Ruby (Bundler) codec")
|
|
218
|
+
.option("--no-jars", "skip scanning embedded .jar/.war/.ear binaries for Maven coordinates")
|
|
197
219
|
.option("--no-js", "alias: skip JS/npm/yarn manifests even if present (Maven-only)")
|
|
198
220
|
.option("--repo <url...>", "extra Maven repository URL(s) to try before Maven Central. Supports https://user:pass@host/path/. Repeatable.")
|
|
199
221
|
.option("--add-repo <name>", "persist a Maven repo: --add-repo <name> <url> [--auth user:pass]")
|
|
@@ -205,14 +227,44 @@ program.parse(process.argv);
|
|
|
205
227
|
const options = program.opts();
|
|
206
228
|
const deps2Exclude = options.exclude ? new RegExp(options.exclude) : null;
|
|
207
229
|
const verbose = !!options.verbose;
|
|
230
|
+
|
|
231
|
+
// Validate --fail-on early: an unrecognised value (typo like "hgih", wrong case)
|
|
232
|
+
// must HARD-FAIL, never silently disable the CI gate.
|
|
233
|
+
if (options.failOn) {
|
|
234
|
+
const FAIL_ON_LEVELS = ["none", "low", "medium", "high", "critical", "kev"];
|
|
235
|
+
const lvl = String(options.failOn).toLowerCase();
|
|
236
|
+
if (!FAIL_ON_LEVELS.includes(lvl)) {
|
|
237
|
+
console.error(chalk.red(`❌ invalid --fail-on "${options.failOn}" — expected one of: ${FAIL_ON_LEVELS.join(", ")}`));
|
|
238
|
+
process.exit(2);
|
|
239
|
+
}
|
|
240
|
+
options.failOn = lvl;
|
|
241
|
+
}
|
|
208
242
|
// Read-only when no target is given. No need for an explicit --test flag.
|
|
209
243
|
const readOnly = !options.target;
|
|
210
244
|
|
|
245
|
+
// --src is required for every mode except --import-anonymized (which scans a
|
|
246
|
+
// descriptor and has no source tree).
|
|
247
|
+
if (!options.src && !options.importAnonymized) {
|
|
248
|
+
console.error(chalk.red("❌ required option '-s, --src <src>' not specified"));
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
if (options.src && options.importAnonymized) {
|
|
252
|
+
console.warn(chalk.yellow("⚠️ --import-anonymized ignores --src (the descriptor is the source of deps)"));
|
|
253
|
+
}
|
|
254
|
+
|
|
211
255
|
if (options.src && options.target) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
256
|
+
// --target is rimraf'd before being rewritten, so it must NOT overlap --src in
|
|
257
|
+
// EITHER direction: not the same dir, not a subdir of --src, and — the
|
|
258
|
+
// catastrophic case — not a PARENT of --src (which would delete the source tree
|
|
259
|
+
// and everything beside it).
|
|
260
|
+
const srcAbs = path.resolve(options.src);
|
|
261
|
+
const tgtAbs = path.resolve(options.target);
|
|
262
|
+
const relFromSrc = path.relative(srcAbs, tgtAbs); // target as seen from src
|
|
263
|
+
const relToSrc = path.relative(tgtAbs, srcAbs); // src as seen from target
|
|
264
|
+
const targetInsideSrc = !relFromSrc || (!relFromSrc.startsWith("..") && !path.isAbsolute(relFromSrc));
|
|
265
|
+
const srcInsideTarget = !relToSrc || (!relToSrc.startsWith("..") && !path.isAbsolute(relToSrc));
|
|
266
|
+
if (targetInsideSrc || srcInsideTarget) {
|
|
267
|
+
console.error(chalk.red("❌ --target must not overlap --src (it cannot be the same as, a subdirectory of, or a parent of --src) — it is deleted before being rewritten"));
|
|
216
268
|
process.exit(1);
|
|
217
269
|
}
|
|
218
270
|
}
|
|
@@ -226,16 +278,16 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
226
278
|
try {
|
|
227
279
|
const hit = await existsInAny(repos, p, { userAgent: "fad-checker-existence" });
|
|
228
280
|
if (hit) return true;
|
|
229
|
-
console.log(
|
|
281
|
+
if (verbose) console.log(chalk.dim(` not on any repo: ${g}:${a}`));
|
|
230
282
|
return false;
|
|
231
283
|
} catch (err) {
|
|
232
|
-
console.info(`error querying repos: ${g}:${a} — ${err.message}`);
|
|
284
|
+
if (verbose) console.info(chalk.dim(` error querying repos: ${g}:${a} — ${err.message}`));
|
|
233
285
|
return false;
|
|
234
286
|
}
|
|
235
287
|
}
|
|
236
288
|
|
|
237
289
|
(async function main() {
|
|
238
|
-
|
|
290
|
+
ui.banner();
|
|
239
291
|
|
|
240
292
|
// Build the Maven repo list once: persisted repos (from ~/.fad-checker/config.json)
|
|
241
293
|
// + ad-hoc --repo URLs + Maven Central as final fallback. Used by transitive
|
|
@@ -244,18 +296,44 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
244
296
|
const { buildRepoList } = require("./lib/maven-repo");
|
|
245
297
|
const extraRepos = (options.repo || []).map(url => ({ url }));
|
|
246
298
|
const mavenRepos = buildRepoList(getMavenRepos(), extraRepos);
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
299
|
+
const runMode = options.importAnonymized ? "import descriptor" : (options.offline ? "offline" : "online");
|
|
300
|
+
if (options.src) ui.kv("source", chalk.white(options.src));
|
|
301
|
+
if (mavenRepos.length > 1) ui.kv("repos", chalk.white(mavenRepos.map(r => r.name).join(chalk.dim(" → "))));
|
|
302
|
+
ui.kv("mode", chalk.white(runMode));
|
|
250
303
|
|
|
251
304
|
let wrotePom = 0;
|
|
252
305
|
|
|
306
|
+
// --- PASSI phase 2: import an anonymized descriptor instead of collecting ---
|
|
307
|
+
// Scans the descriptor's public coordinates online to WARM the coordinate-keyed
|
|
308
|
+
// caches (OSV/NVD/CVE/registry/EOL) + retire signatures. Pair with --export-cache.
|
|
309
|
+
if (options.importAnonymized) {
|
|
310
|
+
const { deserializeDeps } = require("./lib/deps-descriptor");
|
|
311
|
+
let descriptor;
|
|
312
|
+
try { descriptor = JSON.parse(fs.readFileSync(options.importAnonymized, "utf8")); }
|
|
313
|
+
catch (e) { console.error(chalk.red(`❌ could not read --import-anonymized file: ${e.message}`)); process.exit(1); }
|
|
314
|
+
let imported;
|
|
315
|
+
try { imported = deserializeDeps(descriptor); }
|
|
316
|
+
catch (e) { console.error(chalk.red(`❌ invalid descriptor: ${e.message}`)); process.exit(1); }
|
|
317
|
+
const { resolved, activeIds, runMaven, runNpm } = imported;
|
|
318
|
+
ui.section("Anonymized descriptor");
|
|
319
|
+
ui.ok(`imported ${chalk.bold(resolved.size)} dep(s) across ${activeIds.join(", ") || "—"}`);
|
|
320
|
+
if (options.offline) ui.warn("--offline: caches won't warm; only useful to re-render from an already-warm cache");
|
|
321
|
+
if (!resolved.size) { ui.warn("descriptor has no dependencies — nothing to scan"); process.exit(0); }
|
|
322
|
+
// Warm retire signatures (online) so --export-cache carries them for offline JS scanning.
|
|
323
|
+
if (runNpm && !options.offline && options.retire !== false) {
|
|
324
|
+
const { warmRetireSignatures } = require("./lib/retire");
|
|
325
|
+
await warmRetireSignatures({ verbose });
|
|
326
|
+
}
|
|
327
|
+
await runReportFlow(resolved, { activeIds, runMaven, runNpm, privateLibIds: [], mavenRepos, collectWarnings: [] });
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
253
331
|
// --- Codec detection + selection ---
|
|
254
332
|
const { detectCodecs, allCodecs, getCodec } = require("./lib/codecs");
|
|
255
333
|
const { resolveActiveCodecs } = require("./lib/codecs/select");
|
|
256
334
|
const eco = (options.ecosystem || "auto").toLowerCase();
|
|
257
335
|
const detected = (eco === "auto") ? detectCodecs(options.src).map(c => c.id) : allCodecs().map(c => c.id);
|
|
258
|
-
const noCodecs = ["maven", "npm", "yarn", "nuget", "composer", "pypi"].filter(id => options[id] === false);
|
|
336
|
+
const noCodecs = ["maven", "npm", "yarn", "nuget", "composer", "pypi", "go", "ruby"].filter(id => options[id] === false);
|
|
259
337
|
const activeIds = resolveActiveCodecs(eco, detected, { noCodecs, noJs: !options.js });
|
|
260
338
|
const runMaven = activeIds.includes("maven");
|
|
261
339
|
const runNpm = activeIds.includes("npm") || activeIds.includes("yarn");
|
|
@@ -269,7 +347,7 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
269
347
|
const codec = getCodec(id);
|
|
270
348
|
let res;
|
|
271
349
|
try {
|
|
272
|
-
res = await codec.collect(options.src, { ignoreTest: !!options.ignoreTest, deps2Exclude, verbose });
|
|
350
|
+
res = await codec.collect(options.src, { ignoreTest: !!options.ignoreTest, deps2Exclude, verbose, scanJars: options.jars !== false, srcRoot: options.src });
|
|
273
351
|
} catch (err) {
|
|
274
352
|
console.warn(chalk.red(`❌ ${id} collect failed:`), chalk.dim(err.message));
|
|
275
353
|
continue;
|
|
@@ -279,14 +357,47 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
279
357
|
if (id === "maven") mavenCtx = res._maven;
|
|
280
358
|
}
|
|
281
359
|
|
|
360
|
+
// --- Collection summary ---
|
|
361
|
+
ui.section("Collection");
|
|
362
|
+
const ecoCount = {};
|
|
363
|
+
let embeddedCount = 0;
|
|
364
|
+
for (const d of resolved.values()) {
|
|
365
|
+
if (d.provenance === "embedded") { embeddedCount++; continue; } // counted separately below
|
|
366
|
+
ecoCount[d.ecosystem] = (ecoCount[d.ecosystem] || 0) + 1;
|
|
367
|
+
}
|
|
368
|
+
if (runMaven) ui.ok(`${chalk.bold("Maven".padEnd(8))} ${mavenCtx ? mavenCtx.pomFiles.length + " module(s) · " : ""}${ecoCount.maven || 0} direct dep(s)`);
|
|
369
|
+
if (runNpm) ui.ok(`${chalk.bold("npm/yarn".padEnd(8))} ${ecoCount.npm || 0} dep(s)`);
|
|
370
|
+
for (const [id, n] of Object.entries(ecoCount)) {
|
|
371
|
+
if (id === "maven" || id === "npm") continue;
|
|
372
|
+
ui.ok(`${chalk.bold(((getCodec(id)?.label) || id).padEnd(8))} ${n} dep(s)`);
|
|
373
|
+
}
|
|
374
|
+
if (embeddedCount) ui.ok(`${chalk.bold("Embedded".padEnd(8))} ${embeddedCount} coord(s) in .jar/.war/.ear`);
|
|
375
|
+
if (!ecoCount.maven && !ecoCount.npm && !Object.keys(ecoCount).length) ui.warn("no dependencies found in the source tree");
|
|
376
|
+
if (collectWarnings.length) {
|
|
377
|
+
ui.warn(`${collectWarnings.length} manifest warning(s) — best-effort / no lockfile:`);
|
|
378
|
+
for (const w of collectWarnings.slice(0, 5)) ui.info(chalk.dim(w.message));
|
|
379
|
+
if (collectWarnings.length > 5) ui.info(chalk.dim(`…and ${collectWarnings.length - 5} more`));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// --- PASSI phase 1: export an anonymized descriptor and exit (no network, no report) ---
|
|
383
|
+
if (options.exportAnonymized) {
|
|
384
|
+
const { serializeDeps } = require("./lib/deps-descriptor");
|
|
385
|
+
const pkgVersion = require("./package.json").version;
|
|
386
|
+
const descriptor = serializeDeps(resolved, { generator: `fad-checker ${pkgVersion}` });
|
|
387
|
+
try { fs.writeFileSync(options.exportAnonymized, JSON.stringify(descriptor, null, 2) + "\n"); }
|
|
388
|
+
catch (e) { console.error(chalk.red(`❌ could not write --export-anonymized file: ${e.message}`)); process.exit(1); }
|
|
389
|
+
const ecoSummary = Object.entries(descriptor.summary.byEcosystem).map(([k, v]) => `${k}:${v}`).join(", ");
|
|
390
|
+
ui.section("Anonymized export");
|
|
391
|
+
ui.ok(`${chalk.bold(descriptor.summary.total)} dep(s) (${ecoSummary || "none"}) → ${chalk.white(options.exportAnonymized)}`);
|
|
392
|
+
ui.info(chalk.dim("public coordinates only — no paths/URLs/host info. Review before transfer."));
|
|
393
|
+
if (!descriptor.summary.total) ui.warn("no dependencies collected — descriptor is empty");
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
282
397
|
if (!readOnly) {
|
|
283
398
|
try { await rimraf(options.target); } catch (_) { /* fresh dir */ }
|
|
284
399
|
}
|
|
285
400
|
|
|
286
|
-
if (runMaven && mavenCtx) console.log(chalk.blue(`🔍 Found ${mavenCtx.pomFiles.length} pom.xml files`));
|
|
287
|
-
if (runNpm) console.log(chalk.blue(`🔍 JS manifests detected — npm/yarn pipeline enabled`));
|
|
288
|
-
console.log();
|
|
289
|
-
|
|
290
401
|
// Maven POM rewrite (cleanup feature). Parse + inheritance already happened
|
|
291
402
|
// inside the maven codec's collect(); we reuse its metadata store here.
|
|
292
403
|
if (runMaven && mavenCtx) {
|
|
@@ -296,80 +407,80 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
296
407
|
try {
|
|
297
408
|
if (await core.rewritePoms(pom, store, propsByPom, rewriteOpts)) wrotePom++;
|
|
298
409
|
} catch (err) {
|
|
299
|
-
console.error(chalk.red(
|
|
410
|
+
console.error(chalk.red(` ✗ rewrite failed for ${pom}:`), err.message);
|
|
300
411
|
}
|
|
301
412
|
}
|
|
302
413
|
}
|
|
303
414
|
|
|
304
|
-
// ----------
|
|
415
|
+
// ---------- Maven POM analysis summary (parents missing / excluded) ----------
|
|
305
416
|
let privateLibIds = [];
|
|
306
417
|
if (runMaven && mavenCtx) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
console.log(chalk.cyanBright("📦 Résumé des POM analysés :"));
|
|
310
|
-
console.log(chalk.cyanBright("─────────────────────────────────────────────\n"));
|
|
311
|
-
|
|
312
|
-
const missingParents = Object.keys(allPomMetadata.missingById)
|
|
313
|
-
.filter(id => {
|
|
314
|
-
const parts = id.split(":");
|
|
315
|
-
if (parts.length === 2) return false;
|
|
316
|
-
return !(allPomMetadata.byId[id] || allPomMetadata.byId[`${parts[0]}:${parts[1]}`]);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
if (missingParents.length) {
|
|
320
|
-
console.log(chalk.yellowBright("⚠️ Parents libs Maven manquants ( si ces lib sont privées snyk plantera ) :"));
|
|
321
|
-
console.log(missingParents.map(id => chalk.yellow(" • ") + id).join("\n"));
|
|
322
|
-
} else {
|
|
323
|
-
console.log(chalk.greenBright("✅ Aucun parent Maven manquant."));
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (options.allLibs) {
|
|
327
|
-
const anyMissingLibs = Object.keys(allPomMetadata.anyMissingById)
|
|
328
|
-
.filter(id => {
|
|
329
|
-
const parts = id.split(":");
|
|
330
|
-
if (parts.length === 3) return false;
|
|
331
|
-
return !(allPomMetadata.byId[id] || allPomMetadata.byId[`${parts[0]}:${parts[1]}`]);
|
|
332
|
-
});
|
|
333
|
-
const limit = pLimit(10);
|
|
334
|
-
console.log(chalk.magentaBright("\n🚫 Libs absentes de Maven Central :"));
|
|
335
|
-
const results = await Promise.all(anyMissingLibs.map(id => {
|
|
336
|
-
const [g, a] = id.split(":");
|
|
337
|
-
return limit(async () => ({ id, found: await checkMavenLibExist(g, a, mavenRepos) }));
|
|
338
|
-
}));
|
|
339
|
-
for (const r of results) if (r && r.found === false) privateLibIds.push(r.id);
|
|
340
|
-
}
|
|
418
|
+
const allPomMetadata = mavenCtx.store; // reuse the codec's parsed metadata
|
|
419
|
+
ui.section("Maven POM analysis");
|
|
341
420
|
|
|
342
|
-
|
|
343
|
-
const excludedLibs = Object.keys(allPomMetadata.excludedById)
|
|
421
|
+
const missingParents = Object.keys(allPomMetadata.missingById)
|
|
344
422
|
.filter(id => {
|
|
345
423
|
const parts = id.split(":");
|
|
346
424
|
if (parts.length === 2) return false;
|
|
347
425
|
return !(allPomMetadata.byId[id] || allPomMetadata.byId[`${parts[0]}:${parts[1]}`]);
|
|
348
426
|
});
|
|
349
|
-
if (
|
|
350
|
-
|
|
351
|
-
|
|
427
|
+
if (missingParents.length) {
|
|
428
|
+
ui.warn(`${missingParents.length} missing parent POM(s) — Snyk will fail if these are private:`);
|
|
429
|
+
for (const id of missingParents.slice(0, 10)) ui.info(chalk.yellow(id));
|
|
430
|
+
if (missingParents.length > 10) ui.info(chalk.dim(`…and ${missingParents.length - 10} more`));
|
|
352
431
|
} else {
|
|
353
|
-
|
|
432
|
+
ui.ok("no missing Maven parent POMs");
|
|
354
433
|
}
|
|
355
|
-
} else {
|
|
356
|
-
console.log(chalk.magentaBright("\n🚫 Bibliothèques exclues ( privées ) et manquantes : "));
|
|
357
|
-
console.log(chalk.magenta(" • ") + "Pas d'exclusions ( on considère donc que toutes les deps hors parents sont publiques )");
|
|
358
|
-
}
|
|
359
434
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
435
|
+
if (options.allLibs) {
|
|
436
|
+
const anyMissingLibs = Object.keys(allPomMetadata.anyMissingById)
|
|
437
|
+
.filter(id => {
|
|
438
|
+
const parts = id.split(":");
|
|
439
|
+
if (parts.length === 3) return false;
|
|
440
|
+
return !(allPomMetadata.byId[id] || allPomMetadata.byId[`${parts[0]}:${parts[1]}`]);
|
|
441
|
+
});
|
|
442
|
+
const limit = pLimit(10);
|
|
443
|
+
const results = await Promise.all(anyMissingLibs.map(id => {
|
|
444
|
+
const [g, a] = id.split(":");
|
|
445
|
+
return limit(async () => ({ id, found: await checkMavenLibExist(g, a, mavenRepos) }));
|
|
446
|
+
}));
|
|
447
|
+
for (const r of results) if (r && r.found === false) privateLibIds.push(r.id);
|
|
448
|
+
if (privateLibIds.length) {
|
|
449
|
+
ui.warn(`${privateLibIds.length} lib(s) absent from Maven Central (likely private):`);
|
|
450
|
+
for (const id of privateLibIds.slice(0, 10)) ui.info(chalk.magenta(id));
|
|
451
|
+
if (privateLibIds.length > 10) ui.info(chalk.dim(`…and ${privateLibIds.length - 10} more`));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (deps2Exclude) {
|
|
456
|
+
const excludedLibs = Object.keys(allPomMetadata.excludedById)
|
|
457
|
+
.filter(id => {
|
|
458
|
+
const parts = id.split(":");
|
|
459
|
+
if (parts.length === 2) return false;
|
|
460
|
+
return !(allPomMetadata.byId[id] || allPomMetadata.byId[`${parts[0]}:${parts[1]}`]);
|
|
461
|
+
});
|
|
462
|
+
if (excludedLibs.length) {
|
|
463
|
+
ui.warn(`${excludedLibs.length} excluded-and-missing library(ies):`);
|
|
464
|
+
for (const id of excludedLibs.slice(0, 10)) ui.info(chalk.magenta(id));
|
|
465
|
+
if (excludedLibs.length > 10) ui.info(chalk.dim(`…and ${excludedLibs.length - 10} more`));
|
|
466
|
+
} else {
|
|
467
|
+
ui.ok("no excluded-and-missing libraries");
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (!readOnly) ui.ok(`${chalk.bold(wrotePom)} cleaned POM(s) written → ${chalk.white(options.target)}`);
|
|
472
|
+
else ui.info(chalk.dim(`${wrotePom} POM(s) cleanable (read-only — pass -t <dir> to write them)`));
|
|
473
|
+
}
|
|
365
474
|
|
|
366
|
-
// ----------
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
475
|
+
// ---------- Scan flow (CVE / EOL / Obsolete) ----------
|
|
476
|
+
// The scan always runs — it feeds the terminal summary, the file outputs and the
|
|
477
|
+
// CI gate. Which files get written is decided by the --report-* family inside
|
|
478
|
+
// (HTML + .doc by default; --no-report writes nothing).
|
|
479
|
+
await runReportFlow(resolved, { activeIds, runMaven, runNpm, privateLibIds, mavenRepos, collectWarnings });
|
|
480
|
+
if (!readOnly) {
|
|
481
|
+
ui.section("Next step");
|
|
482
|
+
ui.info(`run Snyk on the cleaned tree:`);
|
|
483
|
+
console.log(" " + chalk.white(`cd ${options.target} && snyk test --json --all-projects | snyk-to-html -o ../snyk-deps-check.html`));
|
|
373
484
|
}
|
|
374
485
|
})();
|
|
375
486
|
|
|
@@ -379,31 +490,13 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
379
490
|
const { writeReports, computeStats } = require("./lib/cve-report");
|
|
380
491
|
const { getCodec } = require("./lib/codecs");
|
|
381
492
|
const outdated = require("./lib/outdated");
|
|
493
|
+
const { getNvdApiKey } = require("./lib/config");
|
|
382
494
|
const offline = !!options.offline;
|
|
383
|
-
if (offline) console.log(chalk.gray(" (--offline: cached data only, no network)"));
|
|
384
|
-
|
|
385
|
-
console.log(chalk.bold.cyan("\n📋 Rapport CVE / EOL / Obsolète\n") + chalk.gray("─────────────────────────────"));
|
|
386
|
-
|
|
387
|
-
// Deps were already collected per-codec by main(). Just report the counts.
|
|
388
|
-
const byEcoCount = {};
|
|
389
|
-
for (const d of resolved.values()) byEcoCount[d.ecosystem] = (byEcoCount[d.ecosystem] || 0) + 1;
|
|
390
|
-
if (runMaven) console.log(chalk.blue(`📚 ${byEcoCount.maven || 0} dépendances Maven directes (incl. parent POMs)`));
|
|
391
|
-
if (runNpm) console.log(chalk.blue(`📦 ${byEcoCount.npm || 0} dépendances npm/yarn`));
|
|
392
|
-
for (const [ecoId, n] of Object.entries(byEcoCount)) {
|
|
393
|
-
if (ecoId === "maven" || ecoId === "npm") continue;
|
|
394
|
-
const label = (require("./lib/codecs").getCodec(ecoId)?.label) || ecoId;
|
|
395
|
-
console.log(chalk.blue(`📦 ${n} dépendances ${label}`));
|
|
396
|
-
}
|
|
397
495
|
|
|
398
|
-
//
|
|
496
|
+
// Collection counts already shown in the "Collection" section by main();
|
|
497
|
+
// for --import-anonymized they were shown in the "Anonymized descriptor" section.
|
|
399
498
|
const npmWarnings = collectWarnings || [];
|
|
400
499
|
let scanWarnings = [];
|
|
401
|
-
if (npmWarnings.length) {
|
|
402
|
-
console.log(chalk.yellow(`⚠️ ${npmWarnings.length} manifest warning(s) :`));
|
|
403
|
-
for (const w of npmWarnings) {
|
|
404
|
-
console.log(chalk.yellow(` • ${w.manifestPath} — ${w.message}`));
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
500
|
const directCount = resolved.size;
|
|
408
501
|
|
|
409
502
|
// Scan-completeness signals: BOMs and unresolved-version deps mean fad-checker
|
|
@@ -411,20 +504,39 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
411
504
|
if (runMaven) {
|
|
412
505
|
const { detectScanCompletenessWarnings } = require("./lib/scan-completeness");
|
|
413
506
|
scanWarnings = detectScanCompletenessWarnings(resolved, { ranSnyk: !!options.snyk, ranTransitive: !!options.transitive });
|
|
414
|
-
if (scanWarnings.length) {
|
|
415
|
-
console.log(chalk.yellow(`\n⚠️ ${scanWarnings.length} scan-completeness alert(s) — a real Maven/Snyk run may surface more findings:`));
|
|
416
|
-
for (const w of scanWarnings) {
|
|
417
|
-
console.log(chalk.yellow(` • [${w.type}] ${w.message}`));
|
|
418
|
-
if (w.items?.length) {
|
|
419
|
-
const shown = w.items.slice(0, 6);
|
|
420
|
-
for (const it of shown) console.log(chalk.gray(` · ${it}`));
|
|
421
|
-
if (w.items.length > shown.length) console.log(chalk.gray(` · …and ${w.items.length - shown.length} more`));
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
507
|
}
|
|
426
508
|
|
|
427
|
-
|
|
509
|
+
// ---- Vulnerability database update (global step progress) ----
|
|
510
|
+
ui.section("Vulnerability database update");
|
|
511
|
+
if (offline) ui.info(chalk.dim("--offline: cached data only, no network"));
|
|
512
|
+
|
|
513
|
+
const hasNvdKey = !!getNvdApiKey();
|
|
514
|
+
if (options.nvd && !offline && !hasNvdKey) {
|
|
515
|
+
ui.warn(chalk.yellow("No NVD API key — enrichment throttled to 5 req/30s (slow)."));
|
|
516
|
+
ui.info(chalk.dim("Free & instant key: https://nvd.nist.gov/developers/request-an-api-key"));
|
|
517
|
+
ui.info(chalk.dim("then: fad-checker --set-nvd-key <KEY>"));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Decide which update steps will run (from flags) so the [n/N] counter is accurate.
|
|
521
|
+
const cveScanner = runMaven ? (getCodec("maven").nativeScanners || []).find(s => s.kind === "cve") : null;
|
|
522
|
+
const cveIndexExists = fs.existsSync(require("./lib/cve-download").CVE_INDEX_PATH);
|
|
523
|
+
const otherRegistryIds = activeIds.filter(id => id !== "maven" && id !== "npm" && id !== "yarn" && getCodec(id)?.checkRegistry);
|
|
524
|
+
const willCve = !!cveScanner && (!(options.cveOffline || offline) || cveIndexExists);
|
|
525
|
+
const willTransitive = !!(options.transitive && runMaven);
|
|
526
|
+
const willOsv = !!options.osv;
|
|
527
|
+
const willOutdated = !!options.allLibs;
|
|
528
|
+
const willNvd = !!options.nvd;
|
|
529
|
+
const willEpss = !!options.epss;
|
|
530
|
+
const willKev = !!options.kev;
|
|
531
|
+
const willLicenses = !!options.licenses;
|
|
532
|
+
const willRetire = !!options.retire;
|
|
533
|
+
// License detection piggybacks on the registry passes (same fetched metadata),
|
|
534
|
+
// 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;
|
|
536
|
+
const progress = new ui.Progress(totalSteps);
|
|
537
|
+
|
|
538
|
+
if (willTransitive) {
|
|
539
|
+
const st = progress.start("Transitive resolution (Maven Central)");
|
|
428
540
|
await expandWithTransitives(resolved, {
|
|
429
541
|
verbose,
|
|
430
542
|
offline,
|
|
@@ -432,66 +544,76 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
432
544
|
includeTestDeps: !options.ignoreTest,
|
|
433
545
|
repos: mavenRepos,
|
|
434
546
|
});
|
|
435
|
-
|
|
547
|
+
st.done(`+${resolved.size - directCount} transitive (total ${resolved.size})`);
|
|
436
548
|
}
|
|
437
549
|
|
|
438
550
|
// 1. CVE — native scanner contributed by the maven codec (local cvelistV5 index).
|
|
439
551
|
let cveMatches = [];
|
|
440
552
|
let cveDataDate = null;
|
|
441
|
-
if (
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
console.warn(chalk.yellow("⚠️ CVE scan skipped:"), err.message);
|
|
451
|
-
}
|
|
553
|
+
if (willCve) {
|
|
554
|
+
const st = progress.start("CVE index (CVEProject)");
|
|
555
|
+
try {
|
|
556
|
+
const r = await cveScanner.scan(resolved, { cveRefresh: !!options.cveRefresh, cveOffline: !!options.cveOffline, offline, verbose });
|
|
557
|
+
cveMatches = r.matches || [];
|
|
558
|
+
cveDataDate = r.meta?.cveDataDate || null;
|
|
559
|
+
st.done(`${cveMatches.length} match(es)${cveDataDate ? ` · ${String(cveDataDate).slice(0, 10)}` : ""}`);
|
|
560
|
+
} catch (err) {
|
|
561
|
+
st.fail(err.message);
|
|
452
562
|
}
|
|
453
563
|
}
|
|
454
564
|
|
|
455
|
-
// 2. EOL frameworks
|
|
565
|
+
// 2. EOL frameworks (endoflife.date) — always a step.
|
|
456
566
|
let eolResults = [];
|
|
457
|
-
|
|
458
|
-
|
|
567
|
+
{
|
|
568
|
+
const st = progress.start("EOL frameworks (endoflife.date)");
|
|
569
|
+
try { eolResults = await outdated.checkEolDeps(resolved, { verbose, offline }); st.done(`${eolResults.length} EOL`); }
|
|
570
|
+
catch (err) { st.fail(err.message); }
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// License findings accumulate from each registry pass (same fetched metadata)
|
|
574
|
+
// plus Maven's cached POMs — assessed against the copyleft policy below.
|
|
575
|
+
let licenseFindings = [];
|
|
459
576
|
|
|
460
|
-
// 3. Obsolete / deprecated
|
|
577
|
+
// 3. Obsolete / deprecated — local curated list, instant (no network step).
|
|
461
578
|
let obsoleteResults = [];
|
|
462
579
|
try { obsoleteResults = outdated.checkObsoleteDeps(resolved); }
|
|
463
|
-
catch (err) {
|
|
580
|
+
catch (err) { ui.warn(`obsolete check skipped: ${err.message}`); }
|
|
464
581
|
|
|
465
|
-
// 4. Outdated (latest Maven Central)
|
|
582
|
+
// 4. Outdated (latest Maven Central) — gated by --all-libs.
|
|
466
583
|
let outdatedResults = [];
|
|
467
|
-
if (
|
|
468
|
-
|
|
469
|
-
|
|
584
|
+
if (willOutdated) {
|
|
585
|
+
const st = progress.start("Maven Central (outdated)");
|
|
586
|
+
try {
|
|
587
|
+
outdatedResults = await outdated.checkOutdatedDeps(resolved, { verbose, offline, repos: mavenRepos, onProgress: (p, t) => st.tick(p, t) });
|
|
588
|
+
st.done(`${outdatedResults.length} outdated`);
|
|
589
|
+
} catch (err) { st.fail(err.message); }
|
|
470
590
|
}
|
|
471
591
|
|
|
472
|
-
// 4a. npm registry — deprecation (always, authoritative
|
|
473
|
-
//
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
592
|
+
// 4a. npm registry — deprecation (always, authoritative) + outdated (with --all-libs).
|
|
593
|
+
// Covers npm deps and WebJars, so it runs even in Maven-only mode.
|
|
594
|
+
{
|
|
595
|
+
const st = progress.start("npm registry");
|
|
596
|
+
try {
|
|
597
|
+
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) });
|
|
599
|
+
obsoleteResults = obsoleteResults.concat(npmReg.deprecated);
|
|
600
|
+
outdatedResults = outdatedResults.concat(npmReg.outdated);
|
|
601
|
+
licenseFindings = licenseFindings.concat(npmReg.licensed || []);
|
|
602
|
+
st.done(`${npmReg.deprecated.length} deprecated, ${npmReg.outdated.length} outdated`);
|
|
603
|
+
} catch (err) { st.fail(err.message); }
|
|
604
|
+
}
|
|
482
605
|
|
|
483
606
|
// 4b. Per-codec registry for ecosystems beyond maven/npm (composer/pypi/nuget).
|
|
484
|
-
|
|
485
|
-
// drives each remaining active codec's own registry (Packagist abandoned, etc.).
|
|
486
|
-
for (const id of activeIds) {
|
|
487
|
-
if (id === "maven" || id === "npm" || id === "yarn") continue;
|
|
607
|
+
for (const id of otherRegistryIds) {
|
|
488
608
|
const codec = getCodec(id);
|
|
489
|
-
|
|
609
|
+
const st = progress.start(`${codec.label || id} registry`);
|
|
490
610
|
try {
|
|
491
|
-
const reg = await codec.checkRegistry(resolved, { verbose, offline, allLibs: options.allLibs });
|
|
611
|
+
const reg = await codec.checkRegistry(resolved, { verbose, offline, allLibs: options.allLibs, onProgress: (p, t) => st.tick(p, t) });
|
|
492
612
|
obsoleteResults = obsoleteResults.concat(reg.deprecated || []);
|
|
493
613
|
outdatedResults = outdatedResults.concat(reg.outdated || []);
|
|
494
|
-
|
|
614
|
+
licenseFindings = licenseFindings.concat(reg.licensed || []);
|
|
615
|
+
st.done(`${(reg.deprecated || []).length} deprecated, ${(reg.outdated || []).length} outdated`);
|
|
616
|
+
} catch (err) { st.fail(err.message); }
|
|
495
617
|
}
|
|
496
618
|
|
|
497
619
|
// Cross-section dedup: drop entries from outdated that already appear in EOL/Obsolete
|
|
@@ -503,41 +625,76 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
503
625
|
});
|
|
504
626
|
|
|
505
627
|
// 4b. OSV.dev — Maven-native CVE+GHSA feed (huge recall win over raw CVEProject)
|
|
506
|
-
if (
|
|
628
|
+
if (willOsv) {
|
|
629
|
+
const st = progress.start("OSV.dev");
|
|
507
630
|
try {
|
|
508
631
|
const { queryOsvForDeps } = require("./lib/osv");
|
|
509
|
-
const osvMatches = await queryOsvForDeps(resolved, { verbose, offline });
|
|
632
|
+
const osvMatches = await queryOsvForDeps(resolved, { verbose, offline, onProgress: (p, t) => st.tick(p, t) });
|
|
510
633
|
const before = cveMatches.length;
|
|
511
634
|
cveMatches = mergeBySource(cveMatches, osvMatches);
|
|
512
|
-
|
|
635
|
+
st.done(`${osvMatches.length} vulns · +${cveMatches.length - before} after merge`);
|
|
513
636
|
} catch (err) {
|
|
514
|
-
|
|
637
|
+
st.fail(err.message);
|
|
515
638
|
}
|
|
516
639
|
}
|
|
517
640
|
|
|
518
|
-
// 4c. NVD enrichment — canonical description + full CVSS for matched CVEs
|
|
519
|
-
if (
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
}
|
|
524
|
-
|
|
641
|
+
// 4c. NVD enrichment — canonical description + full CVSS for matched CVEs.
|
|
642
|
+
if (willNvd) {
|
|
643
|
+
const st = progress.start("NVD enrichment");
|
|
644
|
+
if (!cveMatches.length) {
|
|
645
|
+
st.skip("no CVE to enrich");
|
|
646
|
+
} else {
|
|
647
|
+
try {
|
|
648
|
+
const { enrichMatches } = require("./lib/nvd");
|
|
649
|
+
await enrichMatches(cveMatches, { verbose, offline, onProgress: (p, t) => st.tick(p, t) });
|
|
650
|
+
// 4d. CPE refinement — use NVD's CPE configurations to upgrade match
|
|
651
|
+
// confidence and flag likely false positives (version outside CPE range).
|
|
652
|
+
let filtered = 0;
|
|
653
|
+
try {
|
|
654
|
+
const { refineMatchesWithCpe } = require("./lib/cpe");
|
|
655
|
+
refineMatchesWithCpe(cveMatches);
|
|
656
|
+
filtered = cveMatches.filter(m => m.cpeFiltered).length;
|
|
657
|
+
} catch (err) { ui.warn(`CPE refinement skipped: ${err.message}`); }
|
|
658
|
+
const uniqueCves = new Set(cveMatches.map(m => m.cve?.id)).size;
|
|
659
|
+
st.done(`${uniqueCves} CVE${filtered ? ` · ${filtered} false-positive(s) filtered` : ""}${hasNvdKey ? "" : " · no key (slow)"}`);
|
|
660
|
+
} catch (err) { st.fail(err.message); }
|
|
525
661
|
}
|
|
662
|
+
}
|
|
526
663
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
664
|
+
// 4e. EPSS — exploit-prediction percentile for each matched CVE (FIRST.org).
|
|
665
|
+
if (willEpss) {
|
|
666
|
+
const st = progress.start("EPSS (FIRST.org)");
|
|
667
|
+
if (!cveMatches.length) { st.skip("no CVE"); }
|
|
668
|
+
else {
|
|
669
|
+
try {
|
|
670
|
+
const { enrichEpss } = require("./lib/epss");
|
|
671
|
+
await enrichEpss(cveMatches, { verbose, offline, onProgress: (p, t) => st.tick(p, t) });
|
|
672
|
+
const scored = cveMatches.filter(m => m.cve?.epssPercentile != null).length;
|
|
673
|
+
st.done(`${scored} scored`);
|
|
674
|
+
} catch (err) { st.fail(err.message); }
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// 4f. CISA KEV — flag CVEs known to be exploited in the wild.
|
|
679
|
+
if (willKev) {
|
|
680
|
+
const st = progress.start("CISA KEV");
|
|
681
|
+
if (!cveMatches.length) { st.skip("no CVE"); }
|
|
682
|
+
else {
|
|
683
|
+
try {
|
|
684
|
+
const { enrichKev } = require("./lib/kev");
|
|
685
|
+
await enrichKev(cveMatches, { verbose, offline });
|
|
686
|
+
const kevd = cveMatches.filter(m => m.cve?.kev).length;
|
|
687
|
+
st.done(`${kevd} known-exploited`);
|
|
688
|
+
} catch (err) { st.fail(err.message); }
|
|
538
689
|
}
|
|
539
690
|
}
|
|
540
691
|
|
|
692
|
+
// 4g. Composite priority (KEV > EPSS-weighted CVSS). Always — cheap, pure.
|
|
693
|
+
try {
|
|
694
|
+
const { attachPriority } = require("./lib/priority");
|
|
695
|
+
attachPriority(cveMatches);
|
|
696
|
+
} catch (err) { ui.warn(`priority scoring skipped: ${err.message}`); }
|
|
697
|
+
|
|
541
698
|
// 5. retire.js — native "vendored" scanner contributed by the npm codec. Scans
|
|
542
699
|
// vendored JS files (jquery copies, bootstrap, pdf.js, …) that live in the
|
|
543
700
|
// source tree without any lockfile to back them.
|
|
@@ -545,16 +702,17 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
545
702
|
// vendored .js (which can live in a Maven project's resources too). The
|
|
546
703
|
// scanner is owned by the npm codec but runs whenever --retire is on.
|
|
547
704
|
let retireMatches = [];
|
|
548
|
-
if (
|
|
705
|
+
if (willRetire) {
|
|
706
|
+
const st = progress.start("retire.js (vendored JS)");
|
|
549
707
|
const sc = (getCodec("npm").nativeScanners || []).find(s => s.kind === "vendored");
|
|
550
|
-
if (sc) {
|
|
708
|
+
if (!sc) { st.skip("scanner unavailable"); }
|
|
709
|
+
else if (!options.src) { st.skip("no source tree (descriptor import)"); }
|
|
710
|
+
else {
|
|
551
711
|
try {
|
|
552
712
|
const r = await sc.scan(resolved, { src: options.src, verbose, retireRefresh: !!options.retireRefresh, offline });
|
|
553
713
|
retireMatches = r.matches || [];
|
|
554
|
-
|
|
555
|
-
} catch (err) {
|
|
556
|
-
console.warn(chalk.yellow("⚠️ retire.js skipped:"), err.message);
|
|
557
|
-
}
|
|
714
|
+
st.done(`${retireMatches.length} finding(s)`);
|
|
715
|
+
} catch (err) { st.fail(err.message); }
|
|
558
716
|
}
|
|
559
717
|
}
|
|
560
718
|
|
|
@@ -562,16 +720,16 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
562
720
|
let snykMatches = [];
|
|
563
721
|
if (options.snyk) {
|
|
564
722
|
if (!options.target) {
|
|
565
|
-
|
|
723
|
+
ui.warn("--snyk requires --target (snyk runs on cleaned POMs)");
|
|
566
724
|
} else {
|
|
567
725
|
const snyk = require("./lib/snyk");
|
|
568
726
|
try {
|
|
569
727
|
const raw = await snyk.runSnykTest(options.target, { verbose });
|
|
570
728
|
snykMatches = snyk.parseSnykResults(raw);
|
|
571
729
|
cveMatches = snyk.mergeWithFadResults(cveMatches, snykMatches);
|
|
572
|
-
|
|
730
|
+
ui.ok(`Snyk: ${snykMatches.length} findings merged`);
|
|
573
731
|
} catch (err) {
|
|
574
|
-
|
|
732
|
+
ui.warn(`Snyk run failed: ${err.message}`);
|
|
575
733
|
}
|
|
576
734
|
}
|
|
577
735
|
}
|
|
@@ -581,90 +739,241 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
581
739
|
// full per-bucket list (including cpeFiltered) so the HTML report can render
|
|
582
740
|
// its "Likely false positives" appendix — only the CLI headline excludes
|
|
583
741
|
// cpeFiltered to avoid alarming on triaged-out matches.
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
742
|
+
// Triage — suppress accepted-risk / false-positive findings (--ignore / --vex).
|
|
743
|
+
// Marked in place; kept in the machine exports (flagged) but dropped from the
|
|
744
|
+
// human report's active chapters and from CI gating.
|
|
745
|
+
let suppressedCount = 0;
|
|
746
|
+
if (options.ignore || options.vex) {
|
|
747
|
+
try {
|
|
748
|
+
const { parseIgnoreFile, parseVex, applySuppressions } = require("./lib/suppress");
|
|
749
|
+
const rules = [];
|
|
750
|
+
if (options.ignore) rules.push(...parseIgnoreFile(fs.readFileSync(options.ignore, "utf8")));
|
|
751
|
+
if (options.vex) rules.push(...parseVex(JSON.parse(fs.readFileSync(options.vex, "utf8"))));
|
|
752
|
+
suppressedCount = applySuppressions(cveMatches, rules);
|
|
753
|
+
const via = [options.ignore && "--ignore", options.vex && "--vex"].filter(Boolean).join(" + ");
|
|
754
|
+
if (suppressedCount) ui.info(chalk.dim(`triage: ${suppressedCount} finding(s) suppressed by ${via}`));
|
|
755
|
+
} catch (err) { ui.warn(`suppression skipped: ${err.message}`); }
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const { sortByPriority } = require("./lib/priority");
|
|
759
|
+
const isEmbedded = m => m.dep?.provenance === "embedded";
|
|
760
|
+
// Embedded-binary findings get their own chapter, so keep them out of the
|
|
761
|
+
// declared prod/dev sets (a coord that's both declared AND embedded yields two
|
|
762
|
+
// distinct records — one in each — which is the intended, audit-useful split).
|
|
763
|
+
const prodMatches = cveMatches.filter(m => !m.dep?.isDev && !m.suppressed && !isEmbedded(m));
|
|
764
|
+
const devMatches = cveMatches.filter(m => m.dep?.isDev && !m.suppressed && !isEmbedded(m));
|
|
765
|
+
const embeddedMatches = cveMatches.filter(m => isEmbedded(m) && !m.suppressed);
|
|
766
|
+
const prodActive = sortByPriority(prodMatches.filter(m => !m.cpeFiltered));
|
|
767
|
+
const devActive = sortByPriority(devMatches.filter(m => !m.cpeFiltered));
|
|
768
|
+
const embeddedActive = sortByPriority(embeddedMatches.filter(m => !m.cpeFiltered));
|
|
769
|
+
const kevCount = prodActive.filter(m => m.cve?.kev).length;
|
|
588
770
|
const cpeFilteredCount = (prodMatches.length - prodActive.length) + (devMatches.length - devActive.length);
|
|
589
771
|
|
|
590
772
|
const stats = computeStats(prodActive);
|
|
591
773
|
const devStats = computeStats(devActive);
|
|
592
|
-
|
|
593
|
-
console.log(` critical=${stats.critical} high=${stats.high} medium=${stats.medium} low=${stats.low} unknown=${stats.unknown}`);
|
|
774
|
+
const sev = ui.sevColor;
|
|
594
775
|
const depLabel = d => d.ecosystem === "npm" ? `npm:${d.artifactId}` : `${d.groupId}:${d.artifactId}`;
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
776
|
+
const coordOf = depLabel; // npm deps show as "npm:name", others as "g:a"
|
|
777
|
+
const fmtStats = s => [
|
|
778
|
+
s.critical ? sev("CRITICAL")(`${s.critical} critical`) : null,
|
|
779
|
+
s.high ? sev("HIGH")(`${s.high} high`) : null,
|
|
780
|
+
s.medium ? sev("MEDIUM")(`${s.medium} medium`) : null,
|
|
781
|
+
s.low ? sev("LOW")(`${s.low} low`) : null,
|
|
782
|
+
s.unknown ? chalk.gray(`${s.unknown} unknown`) : null,
|
|
783
|
+
].filter(Boolean).join(" ") || chalk.gray("none");
|
|
784
|
+
const heading = (label, n, extra = "") => console.log("\n " + chalk.bold(label) + chalk.dim(` (${n})`) + (extra ? " " + extra : ""));
|
|
785
|
+
|
|
786
|
+
ui.section("Results");
|
|
787
|
+
|
|
788
|
+
heading("CVE · production", prodActive.length, fmtStats(stats) + (kevCount ? " " + chalk.bgRed.white(` ${kevCount} KEV `) : ""));
|
|
789
|
+
for (const m of prodActive.slice(0, 12)) {
|
|
790
|
+
const epss = m.cve?.epssPercentile != null ? chalk.dim(` epss ${Math.round(m.cve.epssPercentile * 100)}%`) : "";
|
|
791
|
+
const kev = m.cve?.kev ? " " + chalk.bgRed.white(" KEV ") : "";
|
|
792
|
+
console.log(" " + sev(m.cve.severity)((m.cve.severity || "UNKNOWN").padEnd(8)) + " " + chalk.white(m.cve.id) + " " + chalk.dim(`${depLabel(m.dep)}:${m.dep.version}`) + epss + kev);
|
|
598
793
|
}
|
|
599
|
-
if (prodActive.length >
|
|
600
|
-
if (cpeFilteredCount) console.log(chalk.
|
|
794
|
+
if (prodActive.length > 12) console.log(chalk.dim(` …and ${prodActive.length - 12} more (see report)`));
|
|
795
|
+
if (cpeFilteredCount) console.log(chalk.dim(` ${cpeFilteredCount} likely false positive(s) → report appendix`));
|
|
601
796
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
console.log(` ${chalk.red((m.cve.severity || "?").padEnd(8))} ${m.cve.id} ${m.dep.artifactId}@${m.dep.version} ← ${m.dep.vendoredFile}`);
|
|
797
|
+
heading("CVE · dev", devActive.length, devActive.length ? fmtStats(devStats) : "");
|
|
798
|
+
|
|
799
|
+
if (embeddedActive.length) {
|
|
800
|
+
heading("CVE · embedded binaries", embeddedActive.length, fmtStats(computeStats(embeddedActive)));
|
|
801
|
+
for (const m of embeddedActive.slice(0, 8)) {
|
|
802
|
+
const top = (m.dep.manifestPaths?.[0] || "").split("!/")[0];
|
|
803
|
+
console.log(" " + sev(m.cve.severity)((m.cve.severity || "UNKNOWN").padEnd(8)) + " " + chalk.white(m.cve.id) + " " + chalk.dim(`${depLabel(m.dep)}:${m.dep.version}`) + chalk.dim(` ⊂ ${top}`));
|
|
610
804
|
}
|
|
611
|
-
if (
|
|
805
|
+
if (embeddedActive.length > 8) console.log(chalk.dim(` …and ${embeddedActive.length - 8} more (see report ch.1B)`));
|
|
612
806
|
}
|
|
613
807
|
|
|
614
|
-
|
|
615
|
-
const
|
|
808
|
+
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)));
|
|
810
|
+
if (eolResults.length > 8) console.log(chalk.dim(` …and ${eolResults.length - 8} more`));
|
|
811
|
+
|
|
812
|
+
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")));
|
|
814
|
+
if (obsoleteResults.length > 8) console.log(chalk.dim(` …and ${obsoleteResults.length - 8} more`));
|
|
815
|
+
|
|
816
|
+
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)}`);
|
|
818
|
+
if (outdatedResults.length > 8) console.log(chalk.dim(` …and ${outdatedResults.length - 8} more`));
|
|
616
819
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
820
|
+
if (retireMatches.length) {
|
|
821
|
+
heading("Vendored JS (retire.js)", retireMatches.length);
|
|
822
|
+
for (const m of retireMatches.slice(0, 8)) console.log(" " + sev(m.cve.severity)((m.cve.severity || "?").padEnd(8)) + " " + chalk.white(m.cve.id) + " " + chalk.dim(`${m.dep.artifactId}@${m.dep.version}`));
|
|
823
|
+
if (retireMatches.length > 8) console.log(chalk.dim(` …and ${retireMatches.length - 8} more`));
|
|
824
|
+
}
|
|
620
825
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
826
|
+
if (scanWarnings.length) {
|
|
827
|
+
console.log();
|
|
828
|
+
ui.warn(`${scanWarnings.length} scan-completeness note(s) — a real Maven/Snyk run may surface more:`);
|
|
829
|
+
for (const w of scanWarnings) {
|
|
830
|
+
ui.info(chalk.dim(`[${w.type}] ${w.message}`));
|
|
831
|
+
for (const it of (w.items || []).slice(0, 4)) console.log(" " + chalk.dim(`· ${it}`));
|
|
832
|
+
if ((w.items || []).length > 4) console.log(" " + chalk.dim(`· …and ${w.items.length - 4} more`));
|
|
833
|
+
}
|
|
834
|
+
}
|
|
624
835
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
if (
|
|
629
|
-
|
|
836
|
+
// License assessment — Maven licenses come (network-free) from cached POMs;
|
|
837
|
+
// the registry passes already filled licenseFindings for the other ecosystems.
|
|
838
|
+
let licenseResults = null;
|
|
839
|
+
if (willLicenses) {
|
|
840
|
+
try {
|
|
841
|
+
if (runMaven) {
|
|
842
|
+
const { collectMavenLicenses } = require("./lib/maven-license");
|
|
843
|
+
licenseFindings = licenseFindings.concat(collectMavenLicenses(resolved));
|
|
844
|
+
}
|
|
845
|
+
const { assessLicenses } = require("./lib/license-policy");
|
|
846
|
+
licenseResults = assessLicenses(licenseFindings);
|
|
847
|
+
const flaggedN = licenseResults.flagged.length;
|
|
848
|
+
heading("Licenses", licenseResults.assessed.length, flaggedN ? chalk.yellow(`${flaggedN} to review`) : "");
|
|
849
|
+
for (const e of licenseResults.flagged.slice(0, 8)) {
|
|
850
|
+
console.log(" " + chalk.yellow((e.category).padEnd(16)) + " " + chalk.dim(`${coordOf(e.dep)}`) + " " + chalk.dim((e.ids.concat(e.raw)).join(", ") || "—"));
|
|
851
|
+
}
|
|
852
|
+
if (licenseResults.flagged.length > 8) console.log(chalk.dim(` …and ${licenseResults.flagged.length - 8} more`));
|
|
853
|
+
} catch (err) { ui.warn(`license assessment skipped: ${err.message}`); }
|
|
854
|
+
}
|
|
630
855
|
|
|
631
856
|
const reportDir = options.reportOutput || "./fad-checker-report";
|
|
632
|
-
|
|
857
|
+
// --import-anonymized has no source tree; keep the report path-free (consistent
|
|
858
|
+
// with the anonymized descriptor it was fed).
|
|
859
|
+
const srcResolved = options.src ? path.resolve(options.src) : null;
|
|
633
860
|
const projectInfo = {
|
|
634
|
-
name: path.basename(
|
|
635
|
-
src: path
|
|
861
|
+
name: srcResolved ? path.basename(srcResolved) : "anonymized-descriptor",
|
|
862
|
+
src: srcResolved || "(anonymized descriptor — source path withheld)",
|
|
636
863
|
generatedAt: new Date().toISOString(),
|
|
637
864
|
toolVersion: pkg.version,
|
|
638
865
|
cveDataDate,
|
|
639
866
|
};
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
}
|
|
665
|
-
],
|
|
666
|
-
|
|
667
|
-
|
|
867
|
+
|
|
868
|
+
// --- Output target resolution -------------------------------------------------
|
|
869
|
+
// One --report-<type> flag per output, each taking an OPTIONAL path: a string is
|
|
870
|
+
// an explicit path, `true` means "use the default name under --report-output",
|
|
871
|
+
// undefined means "not requested". If NO --report-* flag is given at all, fall
|
|
872
|
+
// back to the historical default set (HTML + .doc). --no-report suppresses ALL
|
|
873
|
+
// file outputs (the scan, terminal summary and --fail-on gate still ran).
|
|
874
|
+
const DEFAULT_NAMES = { html: "cve-report.html", doc: "cve-report.doc", sbom: "sbom.cdx.json", csaf: "csaf-vex.json", json: "findings.json", sarif: "fad.sarif" };
|
|
875
|
+
const sel = { html: options.reportHtml, doc: options.reportDoc, sbom: options.reportSbom, csaf: options.reportCsaf, json: options.reportJson, sarif: options.reportSarif };
|
|
876
|
+
const anySpecified = Object.values(sel).some(v => v !== undefined);
|
|
877
|
+
const resolveOut = key => {
|
|
878
|
+
const v = sel[key];
|
|
879
|
+
if (v === undefined) return (!anySpecified && (key === "html" || key === "doc")) ? path.join(reportDir, DEFAULT_NAMES[key]) : null;
|
|
880
|
+
return (v === true) ? path.join(reportDir, DEFAULT_NAMES[key]) : v;
|
|
881
|
+
};
|
|
882
|
+
const out = options.report === false
|
|
883
|
+
? { html: null, doc: null, sbom: null, csaf: null, json: null, sarif: null }
|
|
884
|
+
: { html: resolveOut("html"), doc: resolveOut("doc"), sbom: resolveOut("sbom"), csaf: resolveOut("csaf"), json: resolveOut("json"), sarif: resolveOut("sarif") };
|
|
885
|
+
const ensureDir = async p => { if (p) await fs.promises.mkdir(path.dirname(path.resolve(p)), { recursive: true }); };
|
|
886
|
+
|
|
887
|
+
const reportWarnings = [
|
|
888
|
+
...(suppressedCount ? [{
|
|
889
|
+
type: "suppressed",
|
|
890
|
+
count: suppressedCount,
|
|
891
|
+
message: `${suppressedCount} finding(s) suppressed via triage (--ignore/--vex) — excluded from the chapters above and from CI gating, but retained (flagged) in the JSON/SBOM/CSAF exports.`,
|
|
892
|
+
}] : []),
|
|
893
|
+
...npmWarnings,
|
|
894
|
+
...scanWarnings,
|
|
895
|
+
...(privateLibIds.length ? [{
|
|
896
|
+
type: "private-libs",
|
|
897
|
+
count: privateLibIds.length,
|
|
898
|
+
items: privateLibIds.map(id => {
|
|
899
|
+
const dep = resolved.get(id);
|
|
900
|
+
const paths = (dep?.pomPaths || []).map(p => path.relative(options.src, p));
|
|
901
|
+
return { id, manifestPaths: paths };
|
|
902
|
+
}),
|
|
903
|
+
message: `${privateLibIds.length} Maven coord(s) not found on Maven Central — they are private/internal libraries. Their CVEs (if any) cannot be detected by fad-checker; if you have an internal CVE feed, audit them separately.`,
|
|
904
|
+
}] : []),
|
|
905
|
+
];
|
|
906
|
+
|
|
907
|
+
const wrote = [];
|
|
908
|
+
if (out.html || out.doc) {
|
|
909
|
+
await ensureDir(out.html); await ensureDir(out.doc);
|
|
910
|
+
const { htmlPath, docPath } = await writeReports({
|
|
911
|
+
cveMatches: prodMatches, devCveMatches: devMatches, embeddedMatches, retireMatches,
|
|
912
|
+
eolResults, obsoleteResults, outdatedResults, licenseResults,
|
|
913
|
+
resolvedDeps: resolved, projectInfo, warnings: reportWarnings,
|
|
914
|
+
htmlPath: out.html, docPath: out.doc,
|
|
915
|
+
});
|
|
916
|
+
if (htmlPath) wrote.push(["HTML report", htmlPath]);
|
|
917
|
+
if (docPath) wrote.push(["Word .doc", docPath]);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Machine-readable exports. Use the full match set (prod + dev + cpe-filtered) so
|
|
921
|
+
// the artifacts are complete; cpeFiltered is marked as a property/flag, not dropped.
|
|
922
|
+
if (out.sbom) {
|
|
923
|
+
try {
|
|
924
|
+
const { writeCycloneDx } = require("./lib/sbom-export");
|
|
925
|
+
await ensureDir(out.sbom);
|
|
926
|
+
writeCycloneDx(resolved, cveMatches, out.sbom, { projectInfo, toolVersion: pkg.version, timestamp: projectInfo.generatedAt, licenseResults });
|
|
927
|
+
wrote.push(["CycloneDX SBOM", out.sbom]);
|
|
928
|
+
} catch (err) { ui.warn(`SBOM export failed: ${err.message}`); }
|
|
929
|
+
}
|
|
930
|
+
if (out.csaf) {
|
|
931
|
+
try {
|
|
932
|
+
const { writeCsaf } = require("./lib/csaf-export");
|
|
933
|
+
await ensureDir(out.csaf);
|
|
934
|
+
writeCsaf(resolved, cveMatches, out.csaf, { projectInfo, toolVersion: pkg.version, timestamp: projectInfo.generatedAt });
|
|
935
|
+
wrote.push(["CSAF 2.0 VEX", out.csaf]);
|
|
936
|
+
} catch (err) { ui.warn(`CSAF export failed: ${err.message}`); }
|
|
937
|
+
}
|
|
938
|
+
if (out.json) {
|
|
939
|
+
try {
|
|
940
|
+
const { writeFindings } = require("./lib/json-export");
|
|
941
|
+
await ensureDir(out.json);
|
|
942
|
+
writeFindings({ cveMatches, retireMatches, eolResults, obsoleteResults, outdatedResults, licenseResults, resolvedDeps: resolved, projectInfo, toolVersion: pkg.version }, out.json);
|
|
943
|
+
wrote.push(["Findings JSON", out.json]);
|
|
944
|
+
} catch (err) { ui.warn(`JSON export failed: ${err.message}`); }
|
|
945
|
+
}
|
|
946
|
+
if (out.sarif) {
|
|
947
|
+
try {
|
|
948
|
+
const { writeSarif } = require("./lib/sarif-export");
|
|
949
|
+
await ensureDir(out.sarif);
|
|
950
|
+
writeSarif(cveMatches.filter(m => !m.suppressed), out.sarif, { projectInfo, toolVersion: pkg.version });
|
|
951
|
+
wrote.push(["SARIF", out.sarif]);
|
|
952
|
+
} catch (err) { ui.warn(`SARIF export failed: ${err.message}`); }
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (wrote.length) {
|
|
956
|
+
ui.section("Output");
|
|
957
|
+
for (const [label, p] of wrote) ui.ok(`${label} → ${chalk.white(p)}`);
|
|
958
|
+
} else if (options.report === false) {
|
|
959
|
+
ui.info(chalk.dim("--no-report: no files written (scan + gate only)"));
|
|
960
|
+
}
|
|
961
|
+
console.log();
|
|
962
|
+
|
|
963
|
+
// CI gating — set a non-zero exit code (after all reports/exports are written)
|
|
964
|
+
// when a production finding meets the --fail-on threshold.
|
|
965
|
+
if (options.failOn && options.failOn !== "none") {
|
|
966
|
+
const { evaluateGate } = require("./lib/gate");
|
|
967
|
+
// Embedded-binary findings are real production risk → gate on them too.
|
|
968
|
+
const gate = evaluateGate([...prodActive, ...embeddedActive], options.failOn);
|
|
969
|
+
if (gate.failed) {
|
|
970
|
+
ui.section("Gate");
|
|
971
|
+
console.log(chalk.red(`✗ --fail-on ${options.failOn}: ${gate.reason}`));
|
|
972
|
+
process.exitCode = 1;
|
|
973
|
+
} else if (verbose) {
|
|
974
|
+
ui.info(chalk.dim(`--fail-on ${options.failOn}: no blocking finding`));
|
|
975
|
+
}
|
|
976
|
+
}
|
|
668
977
|
}
|
|
669
978
|
|
|
670
979
|
/**
|
|
@@ -674,7 +983,9 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
674
983
|
*/
|
|
675
984
|
function mergeBySource(existing, additions) {
|
|
676
985
|
const byKey = new Map();
|
|
677
|
-
|
|
986
|
+
// coordKey keeps embedded-binary findings distinct from a same-g:a:v declared dep
|
|
987
|
+
// (see cve-match dedup). Falls back to g:a for any match lacking a coordKey.
|
|
988
|
+
const k = m => `${m.dep.coordKey || (m.dep.groupId + ":" + m.dep.artifactId)}:${m.dep.version}|${m.cve.id}`;
|
|
678
989
|
for (const m of existing || []) byKey.set(k(m), { ...m, source: m.source || "fad" });
|
|
679
990
|
for (const m of additions || []) {
|
|
680
991
|
const key = k(m);
|