fad-checker 2.0.0 → 2.0.1
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 +15 -0
- package/README.md +58 -10
- package/data/eol-mapping.json +5 -5
- package/fad-checker.js +289 -203
- package/lib/codecs/npm/collect.js +26 -17
- package/lib/codecs/npm/parse.js +114 -10
- package/lib/codecs/npm/registry.js +4 -3
- package/lib/codecs/npm.codec.js +1 -1
- package/lib/codecs/nuget.codec.js +4 -2
- package/lib/codecs/pypi/parse.js +104 -12
- package/lib/codecs/pypi.codec.js +27 -6
- package/lib/core.js +35 -1
- package/lib/cve-match.js +15 -5
- package/lib/deps-descriptor.js +110 -0
- package/lib/nvd.js +4 -3
- package/lib/osv.js +7 -4
- package/lib/outdated.js +7 -2
- package/lib/retire.js +77 -13
- package/lib/transitive.js +3 -3
- package/lib/ui.js +87 -0
- package/package.json +3 -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,7 +164,8 @@ 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.
|
|
@@ -178,6 +180,8 @@ program
|
|
|
178
180
|
.option("--export-cache <file>", "tar.gz/zip the ~/.fad-checker/ caches to <file> (excludes config.json by default)")
|
|
179
181
|
.option("--import-cache <file>", "restore ~/.fad-checker/ from a previously exported archive (existing dir is moved to .bak unless --force)")
|
|
180
182
|
.option("--include-config", "with --export-cache: also bundle config.json (contains the NVD API key)")
|
|
183
|
+
.option("--export-anonymized <file>", "offline: write an anonymized dependency descriptor (public coordinates only, no paths/URLs) for PASSI audits, then exit")
|
|
184
|
+
.option("--import-anonymized <file>", "online: scan an anonymized descriptor (no --src) to warm the caches; pair with --export-cache for offline reporting")
|
|
181
185
|
.option("--force", "with --import-cache: replace ~/.fad-checker/ without backup")
|
|
182
186
|
.option("--report-output <dir>", "report output directory", "./fad-checker-report")
|
|
183
187
|
.option("--ignore-test", "skip test-scoped dependencies in report")
|
|
@@ -208,6 +212,16 @@ const verbose = !!options.verbose;
|
|
|
208
212
|
// Read-only when no target is given. No need for an explicit --test flag.
|
|
209
213
|
const readOnly = !options.target;
|
|
210
214
|
|
|
215
|
+
// --src is required for every mode except --import-anonymized (which scans a
|
|
216
|
+
// descriptor and has no source tree).
|
|
217
|
+
if (!options.src && !options.importAnonymized) {
|
|
218
|
+
console.error(chalk.red("❌ required option '-s, --src <src>' not specified"));
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
if (options.src && options.importAnonymized) {
|
|
222
|
+
console.warn(chalk.yellow("⚠️ --import-anonymized ignores --src (the descriptor is the source of deps)"));
|
|
223
|
+
}
|
|
224
|
+
|
|
211
225
|
if (options.src && options.target) {
|
|
212
226
|
const rel = path.relative(path.resolve(options.src), path.resolve(options.target));
|
|
213
227
|
const isSubdir = !rel || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
@@ -226,16 +240,16 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
226
240
|
try {
|
|
227
241
|
const hit = await existsInAny(repos, p, { userAgent: "fad-checker-existence" });
|
|
228
242
|
if (hit) return true;
|
|
229
|
-
console.log(
|
|
243
|
+
if (verbose) console.log(chalk.dim(` not on any repo: ${g}:${a}`));
|
|
230
244
|
return false;
|
|
231
245
|
} catch (err) {
|
|
232
|
-
console.info(`error querying repos: ${g}:${a} — ${err.message}`);
|
|
246
|
+
if (verbose) console.info(chalk.dim(` error querying repos: ${g}:${a} — ${err.message}`));
|
|
233
247
|
return false;
|
|
234
248
|
}
|
|
235
249
|
}
|
|
236
250
|
|
|
237
251
|
(async function main() {
|
|
238
|
-
|
|
252
|
+
ui.banner();
|
|
239
253
|
|
|
240
254
|
// Build the Maven repo list once: persisted repos (from ~/.fad-checker/config.json)
|
|
241
255
|
// + ad-hoc --repo URLs + Maven Central as final fallback. Used by transitive
|
|
@@ -244,12 +258,38 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
244
258
|
const { buildRepoList } = require("./lib/maven-repo");
|
|
245
259
|
const extraRepos = (options.repo || []).map(url => ({ url }));
|
|
246
260
|
const mavenRepos = buildRepoList(getMavenRepos(), extraRepos);
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
261
|
+
const runMode = options.importAnonymized ? "import descriptor" : (options.offline ? "offline" : "online");
|
|
262
|
+
if (options.src) ui.kv("source", chalk.white(options.src));
|
|
263
|
+
if (mavenRepos.length > 1) ui.kv("repos", chalk.white(mavenRepos.map(r => r.name).join(chalk.dim(" → "))));
|
|
264
|
+
ui.kv("mode", chalk.white(runMode));
|
|
250
265
|
|
|
251
266
|
let wrotePom = 0;
|
|
252
267
|
|
|
268
|
+
// --- PASSI phase 2: import an anonymized descriptor instead of collecting ---
|
|
269
|
+
// Scans the descriptor's public coordinates online to WARM the coordinate-keyed
|
|
270
|
+
// caches (OSV/NVD/CVE/registry/EOL) + retire signatures. Pair with --export-cache.
|
|
271
|
+
if (options.importAnonymized) {
|
|
272
|
+
const { deserializeDeps } = require("./lib/deps-descriptor");
|
|
273
|
+
let descriptor;
|
|
274
|
+
try { descriptor = JSON.parse(fs.readFileSync(options.importAnonymized, "utf8")); }
|
|
275
|
+
catch (e) { console.error(chalk.red(`❌ could not read --import-anonymized file: ${e.message}`)); process.exit(1); }
|
|
276
|
+
let imported;
|
|
277
|
+
try { imported = deserializeDeps(descriptor); }
|
|
278
|
+
catch (e) { console.error(chalk.red(`❌ invalid descriptor: ${e.message}`)); process.exit(1); }
|
|
279
|
+
const { resolved, activeIds, runMaven, runNpm } = imported;
|
|
280
|
+
ui.section("Anonymized descriptor");
|
|
281
|
+
ui.ok(`imported ${chalk.bold(resolved.size)} dep(s) across ${activeIds.join(", ") || "—"}`);
|
|
282
|
+
if (options.offline) ui.warn("--offline: caches won't warm; only useful to re-render from an already-warm cache");
|
|
283
|
+
if (!resolved.size) { ui.warn("descriptor has no dependencies — nothing to scan"); process.exit(0); }
|
|
284
|
+
// Warm retire signatures (online) so --export-cache carries them for offline JS scanning.
|
|
285
|
+
if (runNpm && !options.offline && options.retire !== false) {
|
|
286
|
+
const { warmRetireSignatures } = require("./lib/retire");
|
|
287
|
+
await warmRetireSignatures({ verbose });
|
|
288
|
+
}
|
|
289
|
+
await runReportFlow(resolved, { activeIds, runMaven, runNpm, privateLibIds: [], mavenRepos, collectWarnings: [] });
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
253
293
|
// --- Codec detection + selection ---
|
|
254
294
|
const { detectCodecs, allCodecs, getCodec } = require("./lib/codecs");
|
|
255
295
|
const { resolveActiveCodecs } = require("./lib/codecs/select");
|
|
@@ -279,14 +319,42 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
279
319
|
if (id === "maven") mavenCtx = res._maven;
|
|
280
320
|
}
|
|
281
321
|
|
|
322
|
+
// --- Collection summary ---
|
|
323
|
+
ui.section("Collection");
|
|
324
|
+
const ecoCount = {};
|
|
325
|
+
for (const d of resolved.values()) ecoCount[d.ecosystem] = (ecoCount[d.ecosystem] || 0) + 1;
|
|
326
|
+
if (runMaven) ui.ok(`${chalk.bold("Maven".padEnd(8))} ${mavenCtx ? mavenCtx.pomFiles.length + " module(s) · " : ""}${ecoCount.maven || 0} direct dep(s)`);
|
|
327
|
+
if (runNpm) ui.ok(`${chalk.bold("npm/yarn".padEnd(8))} ${ecoCount.npm || 0} dep(s)`);
|
|
328
|
+
for (const [id, n] of Object.entries(ecoCount)) {
|
|
329
|
+
if (id === "maven" || id === "npm") continue;
|
|
330
|
+
ui.ok(`${chalk.bold(((getCodec(id)?.label) || id).padEnd(8))} ${n} dep(s)`);
|
|
331
|
+
}
|
|
332
|
+
if (!ecoCount.maven && !ecoCount.npm && !Object.keys(ecoCount).length) ui.warn("no dependencies found in the source tree");
|
|
333
|
+
if (collectWarnings.length) {
|
|
334
|
+
ui.warn(`${collectWarnings.length} manifest warning(s) — best-effort / no lockfile:`);
|
|
335
|
+
for (const w of collectWarnings.slice(0, 5)) ui.info(chalk.dim(w.message));
|
|
336
|
+
if (collectWarnings.length > 5) ui.info(chalk.dim(`…and ${collectWarnings.length - 5} more`));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// --- PASSI phase 1: export an anonymized descriptor and exit (no network, no report) ---
|
|
340
|
+
if (options.exportAnonymized) {
|
|
341
|
+
const { serializeDeps } = require("./lib/deps-descriptor");
|
|
342
|
+
const pkgVersion = require("./package.json").version;
|
|
343
|
+
const descriptor = serializeDeps(resolved, { generator: `fad-checker ${pkgVersion}` });
|
|
344
|
+
try { fs.writeFileSync(options.exportAnonymized, JSON.stringify(descriptor, null, 2) + "\n"); }
|
|
345
|
+
catch (e) { console.error(chalk.red(`❌ could not write --export-anonymized file: ${e.message}`)); process.exit(1); }
|
|
346
|
+
const ecoSummary = Object.entries(descriptor.summary.byEcosystem).map(([k, v]) => `${k}:${v}`).join(", ");
|
|
347
|
+
ui.section("Anonymized export");
|
|
348
|
+
ui.ok(`${chalk.bold(descriptor.summary.total)} dep(s) (${ecoSummary || "none"}) → ${chalk.white(options.exportAnonymized)}`);
|
|
349
|
+
ui.info(chalk.dim("public coordinates only — no paths/URLs/host info. Review before transfer."));
|
|
350
|
+
if (!descriptor.summary.total) ui.warn("no dependencies collected — descriptor is empty");
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
282
354
|
if (!readOnly) {
|
|
283
355
|
try { await rimraf(options.target); } catch (_) { /* fresh dir */ }
|
|
284
356
|
}
|
|
285
357
|
|
|
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
358
|
// Maven POM rewrite (cleanup feature). Parse + inheritance already happened
|
|
291
359
|
// inside the maven codec's collect(); we reuse its metadata store here.
|
|
292
360
|
if (runMaven && mavenCtx) {
|
|
@@ -296,80 +364,78 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
296
364
|
try {
|
|
297
365
|
if (await core.rewritePoms(pom, store, propsByPom, rewriteOpts)) wrotePom++;
|
|
298
366
|
} catch (err) {
|
|
299
|
-
console.error(chalk.red(
|
|
367
|
+
console.error(chalk.red(` ✗ rewrite failed for ${pom}:`), err.message);
|
|
300
368
|
}
|
|
301
369
|
}
|
|
302
370
|
}
|
|
303
371
|
|
|
304
|
-
// ----------
|
|
372
|
+
// ---------- Maven POM analysis summary (parents missing / excluded) ----------
|
|
305
373
|
let privateLibIds = [];
|
|
306
374
|
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
|
-
}
|
|
375
|
+
const allPomMetadata = mavenCtx.store; // reuse the codec's parsed metadata
|
|
376
|
+
ui.section("Maven POM analysis");
|
|
341
377
|
|
|
342
|
-
|
|
343
|
-
const excludedLibs = Object.keys(allPomMetadata.excludedById)
|
|
378
|
+
const missingParents = Object.keys(allPomMetadata.missingById)
|
|
344
379
|
.filter(id => {
|
|
345
380
|
const parts = id.split(":");
|
|
346
381
|
if (parts.length === 2) return false;
|
|
347
382
|
return !(allPomMetadata.byId[id] || allPomMetadata.byId[`${parts[0]}:${parts[1]}`]);
|
|
348
383
|
});
|
|
349
|
-
if (
|
|
350
|
-
|
|
351
|
-
|
|
384
|
+
if (missingParents.length) {
|
|
385
|
+
ui.warn(`${missingParents.length} missing parent POM(s) — Snyk will fail if these are private:`);
|
|
386
|
+
for (const id of missingParents.slice(0, 10)) ui.info(chalk.yellow(id));
|
|
387
|
+
if (missingParents.length > 10) ui.info(chalk.dim(`…and ${missingParents.length - 10} more`));
|
|
352
388
|
} else {
|
|
353
|
-
|
|
389
|
+
ui.ok("no missing Maven parent POMs");
|
|
354
390
|
}
|
|
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
391
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
392
|
+
if (options.allLibs) {
|
|
393
|
+
const anyMissingLibs = Object.keys(allPomMetadata.anyMissingById)
|
|
394
|
+
.filter(id => {
|
|
395
|
+
const parts = id.split(":");
|
|
396
|
+
if (parts.length === 3) return false;
|
|
397
|
+
return !(allPomMetadata.byId[id] || allPomMetadata.byId[`${parts[0]}:${parts[1]}`]);
|
|
398
|
+
});
|
|
399
|
+
const limit = pLimit(10);
|
|
400
|
+
const results = await Promise.all(anyMissingLibs.map(id => {
|
|
401
|
+
const [g, a] = id.split(":");
|
|
402
|
+
return limit(async () => ({ id, found: await checkMavenLibExist(g, a, mavenRepos) }));
|
|
403
|
+
}));
|
|
404
|
+
for (const r of results) if (r && r.found === false) privateLibIds.push(r.id);
|
|
405
|
+
if (privateLibIds.length) {
|
|
406
|
+
ui.warn(`${privateLibIds.length} lib(s) absent from Maven Central (likely private):`);
|
|
407
|
+
for (const id of privateLibIds.slice(0, 10)) ui.info(chalk.magenta(id));
|
|
408
|
+
if (privateLibIds.length > 10) ui.info(chalk.dim(`…and ${privateLibIds.length - 10} more`));
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (deps2Exclude) {
|
|
413
|
+
const excludedLibs = Object.keys(allPomMetadata.excludedById)
|
|
414
|
+
.filter(id => {
|
|
415
|
+
const parts = id.split(":");
|
|
416
|
+
if (parts.length === 2) return false;
|
|
417
|
+
return !(allPomMetadata.byId[id] || allPomMetadata.byId[`${parts[0]}:${parts[1]}`]);
|
|
418
|
+
});
|
|
419
|
+
if (excludedLibs.length) {
|
|
420
|
+
ui.warn(`${excludedLibs.length} excluded-and-missing library(ies):`);
|
|
421
|
+
for (const id of excludedLibs.slice(0, 10)) ui.info(chalk.magenta(id));
|
|
422
|
+
if (excludedLibs.length > 10) ui.info(chalk.dim(`…and ${excludedLibs.length - 10} more`));
|
|
423
|
+
} else {
|
|
424
|
+
ui.ok("no excluded-and-missing libraries");
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (!readOnly) ui.ok(`${chalk.bold(wrotePom)} cleaned POM(s) written → ${chalk.white(options.target)}`);
|
|
429
|
+
else ui.info(chalk.dim(`${wrotePom} POM(s) cleanable (read-only — pass -t <dir> to write them)`));
|
|
430
|
+
}
|
|
365
431
|
|
|
366
432
|
// ---------- Report flow (CVE / EOL / Obsolete) ----------
|
|
367
433
|
if (options.report) {
|
|
368
434
|
await runReportFlow(resolved, { activeIds, runMaven, runNpm, privateLibIds, mavenRepos, collectWarnings });
|
|
369
435
|
} else if (!readOnly) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
console.log(chalk.
|
|
436
|
+
ui.section("Next step");
|
|
437
|
+
ui.info(`run Snyk on the cleaned tree:`);
|
|
438
|
+
console.log(" " + chalk.white(`cd ${options.target} && snyk test --json --all-projects | snyk-to-html -o ../snyk-deps-check.html`));
|
|
373
439
|
}
|
|
374
440
|
})();
|
|
375
441
|
|
|
@@ -379,31 +445,13 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
379
445
|
const { writeReports, computeStats } = require("./lib/cve-report");
|
|
380
446
|
const { getCodec } = require("./lib/codecs");
|
|
381
447
|
const outdated = require("./lib/outdated");
|
|
448
|
+
const { getNvdApiKey } = require("./lib/config");
|
|
382
449
|
const offline = !!options.offline;
|
|
383
|
-
if (offline) console.log(chalk.gray(" (--offline: cached data only, no network)"));
|
|
384
450
|
|
|
385
|
-
|
|
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
|
-
|
|
398
|
-
// Warnings surfaced during collection (e.g. npm no-lockfile fallback).
|
|
451
|
+
// Collection counts already shown in the "Collection" section by main();
|
|
452
|
+
// for --import-anonymized they were shown in the "Anonymized descriptor" section.
|
|
399
453
|
const npmWarnings = collectWarnings || [];
|
|
400
454
|
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
455
|
const directCount = resolved.size;
|
|
408
456
|
|
|
409
457
|
// Scan-completeness signals: BOMs and unresolved-version deps mean fad-checker
|
|
@@ -411,20 +459,34 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
411
459
|
if (runMaven) {
|
|
412
460
|
const { detectScanCompletenessWarnings } = require("./lib/scan-completeness");
|
|
413
461
|
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
462
|
}
|
|
426
463
|
|
|
427
|
-
|
|
464
|
+
// ---- Vulnerability database update (global step progress) ----
|
|
465
|
+
ui.section("Vulnerability database update");
|
|
466
|
+
if (offline) ui.info(chalk.dim("--offline: cached data only, no network"));
|
|
467
|
+
|
|
468
|
+
const hasNvdKey = !!getNvdApiKey();
|
|
469
|
+
if (options.nvd && !offline && !hasNvdKey) {
|
|
470
|
+
ui.warn(chalk.yellow("No NVD API key — enrichment throttled to 5 req/30s (slow)."));
|
|
471
|
+
ui.info(chalk.dim("Free & instant key: https://nvd.nist.gov/developers/request-an-api-key"));
|
|
472
|
+
ui.info(chalk.dim("then: fad-checker --set-nvd-key <KEY>"));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Decide which update steps will run (from flags) so the [n/N] counter is accurate.
|
|
476
|
+
const cveScanner = runMaven ? (getCodec("maven").nativeScanners || []).find(s => s.kind === "cve") : null;
|
|
477
|
+
const cveIndexExists = fs.existsSync(require("./lib/cve-download").CVE_INDEX_PATH);
|
|
478
|
+
const otherRegistryIds = activeIds.filter(id => id !== "maven" && id !== "npm" && id !== "yarn" && getCodec(id)?.checkRegistry);
|
|
479
|
+
const willCve = !!cveScanner && (!(options.cveOffline || offline) || cveIndexExists);
|
|
480
|
+
const willTransitive = !!(options.transitive && runMaven);
|
|
481
|
+
const willOsv = !!options.osv;
|
|
482
|
+
const willOutdated = !!options.allLibs;
|
|
483
|
+
const willNvd = !!options.nvd;
|
|
484
|
+
const willRetire = !!options.retire;
|
|
485
|
+
const totalSteps = [willTransitive, willCve, /*EOL*/ true, willOutdated, /*npm reg*/ true, ...otherRegistryIds.map(() => true), willOsv, willNvd, willRetire].filter(Boolean).length;
|
|
486
|
+
const progress = new ui.Progress(totalSteps);
|
|
487
|
+
|
|
488
|
+
if (willTransitive) {
|
|
489
|
+
const st = progress.start("Transitive resolution (Maven Central)");
|
|
428
490
|
await expandWithTransitives(resolved, {
|
|
429
491
|
verbose,
|
|
430
492
|
offline,
|
|
@@ -432,66 +494,70 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
432
494
|
includeTestDeps: !options.ignoreTest,
|
|
433
495
|
repos: mavenRepos,
|
|
434
496
|
});
|
|
435
|
-
|
|
497
|
+
st.done(`+${resolved.size - directCount} transitive (total ${resolved.size})`);
|
|
436
498
|
}
|
|
437
499
|
|
|
438
500
|
// 1. CVE — native scanner contributed by the maven codec (local cvelistV5 index).
|
|
439
501
|
let cveMatches = [];
|
|
440
502
|
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
|
-
}
|
|
503
|
+
if (willCve) {
|
|
504
|
+
const st = progress.start("CVE index (CVEProject)");
|
|
505
|
+
try {
|
|
506
|
+
const r = await cveScanner.scan(resolved, { cveRefresh: !!options.cveRefresh, cveOffline: !!options.cveOffline, offline, verbose });
|
|
507
|
+
cveMatches = r.matches || [];
|
|
508
|
+
cveDataDate = r.meta?.cveDataDate || null;
|
|
509
|
+
st.done(`${cveMatches.length} match(es)${cveDataDate ? ` · ${String(cveDataDate).slice(0, 10)}` : ""}`);
|
|
510
|
+
} catch (err) {
|
|
511
|
+
st.fail(err.message);
|
|
452
512
|
}
|
|
453
513
|
}
|
|
454
514
|
|
|
455
|
-
// 2. EOL frameworks
|
|
515
|
+
// 2. EOL frameworks (endoflife.date) — always a step.
|
|
456
516
|
let eolResults = [];
|
|
457
|
-
|
|
458
|
-
|
|
517
|
+
{
|
|
518
|
+
const st = progress.start("EOL frameworks (endoflife.date)");
|
|
519
|
+
try { eolResults = await outdated.checkEolDeps(resolved, { verbose, offline }); st.done(`${eolResults.length} EOL`); }
|
|
520
|
+
catch (err) { st.fail(err.message); }
|
|
521
|
+
}
|
|
459
522
|
|
|
460
|
-
// 3. Obsolete / deprecated
|
|
523
|
+
// 3. Obsolete / deprecated — local curated list, instant (no network step).
|
|
461
524
|
let obsoleteResults = [];
|
|
462
525
|
try { obsoleteResults = outdated.checkObsoleteDeps(resolved); }
|
|
463
|
-
catch (err) {
|
|
526
|
+
catch (err) { ui.warn(`obsolete check skipped: ${err.message}`); }
|
|
464
527
|
|
|
465
|
-
// 4. Outdated (latest Maven Central)
|
|
528
|
+
// 4. Outdated (latest Maven Central) — gated by --all-libs.
|
|
466
529
|
let outdatedResults = [];
|
|
467
|
-
if (
|
|
468
|
-
|
|
469
|
-
|
|
530
|
+
if (willOutdated) {
|
|
531
|
+
const st = progress.start("Maven Central (outdated)");
|
|
532
|
+
try {
|
|
533
|
+
outdatedResults = await outdated.checkOutdatedDeps(resolved, { verbose, offline, repos: mavenRepos, onProgress: (p, t) => st.tick(p, t) });
|
|
534
|
+
st.done(`${outdatedResults.length} outdated`);
|
|
535
|
+
} catch (err) { st.fail(err.message); }
|
|
470
536
|
}
|
|
471
537
|
|
|
472
|
-
// 4a. npm registry — deprecation (always, authoritative
|
|
473
|
-
//
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
538
|
+
// 4a. npm registry — deprecation (always, authoritative) + outdated (with --all-libs).
|
|
539
|
+
// Covers npm deps and WebJars, so it runs even in Maven-only mode.
|
|
540
|
+
{
|
|
541
|
+
const st = progress.start("npm registry");
|
|
542
|
+
try {
|
|
543
|
+
const { checkNpmRegistryDeps } = require("./lib/codecs/npm/registry");
|
|
544
|
+
const npmReg = await checkNpmRegistryDeps(resolved, { verbose, offline, allLibs: options.allLibs, onProgress: (p, t) => st.tick(p, t) });
|
|
545
|
+
obsoleteResults = obsoleteResults.concat(npmReg.deprecated);
|
|
546
|
+
outdatedResults = outdatedResults.concat(npmReg.outdated);
|
|
547
|
+
st.done(`${npmReg.deprecated.length} deprecated, ${npmReg.outdated.length} outdated`);
|
|
548
|
+
} catch (err) { st.fail(err.message); }
|
|
549
|
+
}
|
|
482
550
|
|
|
483
551
|
// 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;
|
|
552
|
+
for (const id of otherRegistryIds) {
|
|
488
553
|
const codec = getCodec(id);
|
|
489
|
-
|
|
554
|
+
const st = progress.start(`${codec.label || id} registry`);
|
|
490
555
|
try {
|
|
491
|
-
const reg = await codec.checkRegistry(resolved, { verbose, offline, allLibs: options.allLibs });
|
|
556
|
+
const reg = await codec.checkRegistry(resolved, { verbose, offline, allLibs: options.allLibs, onProgress: (p, t) => st.tick(p, t) });
|
|
492
557
|
obsoleteResults = obsoleteResults.concat(reg.deprecated || []);
|
|
493
558
|
outdatedResults = outdatedResults.concat(reg.outdated || []);
|
|
494
|
-
|
|
559
|
+
st.done(`${(reg.deprecated || []).length} deprecated, ${(reg.outdated || []).length} outdated`);
|
|
560
|
+
} catch (err) { st.fail(err.message); }
|
|
495
561
|
}
|
|
496
562
|
|
|
497
563
|
// Cross-section dedup: drop entries from outdated that already appear in EOL/Obsolete
|
|
@@ -503,38 +569,39 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
503
569
|
});
|
|
504
570
|
|
|
505
571
|
// 4b. OSV.dev — Maven-native CVE+GHSA feed (huge recall win over raw CVEProject)
|
|
506
|
-
if (
|
|
572
|
+
if (willOsv) {
|
|
573
|
+
const st = progress.start("OSV.dev");
|
|
507
574
|
try {
|
|
508
575
|
const { queryOsvForDeps } = require("./lib/osv");
|
|
509
|
-
const osvMatches = await queryOsvForDeps(resolved, { verbose, offline });
|
|
576
|
+
const osvMatches = await queryOsvForDeps(resolved, { verbose, offline, onProgress: (p, t) => st.tick(p, t) });
|
|
510
577
|
const before = cveMatches.length;
|
|
511
578
|
cveMatches = mergeBySource(cveMatches, osvMatches);
|
|
512
|
-
|
|
579
|
+
st.done(`${osvMatches.length} vulns · +${cveMatches.length - before} after merge`);
|
|
513
580
|
} catch (err) {
|
|
514
|
-
|
|
581
|
+
st.fail(err.message);
|
|
515
582
|
}
|
|
516
583
|
}
|
|
517
584
|
|
|
518
|
-
// 4c. NVD enrichment — canonical description + full CVSS for matched CVEs
|
|
519
|
-
if (
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
585
|
+
// 4c. NVD enrichment — canonical description + full CVSS for matched CVEs.
|
|
586
|
+
if (willNvd) {
|
|
587
|
+
const st = progress.start("NVD enrichment");
|
|
588
|
+
if (!cveMatches.length) {
|
|
589
|
+
st.skip("no CVE to enrich");
|
|
590
|
+
} else {
|
|
591
|
+
try {
|
|
592
|
+
const { enrichMatches } = require("./lib/nvd");
|
|
593
|
+
await enrichMatches(cveMatches, { verbose, offline, onProgress: (p, t) => st.tick(p, t) });
|
|
594
|
+
// 4d. CPE refinement — use NVD's CPE configurations to upgrade match
|
|
595
|
+
// confidence and flag likely false positives (version outside CPE range).
|
|
596
|
+
let filtered = 0;
|
|
597
|
+
try {
|
|
598
|
+
const { refineMatchesWithCpe } = require("./lib/cpe");
|
|
599
|
+
refineMatchesWithCpe(cveMatches);
|
|
600
|
+
filtered = cveMatches.filter(m => m.cpeFiltered).length;
|
|
601
|
+
} catch (err) { ui.warn(`CPE refinement skipped: ${err.message}`); }
|
|
602
|
+
const uniqueCves = new Set(cveMatches.map(m => m.cve?.id)).size;
|
|
603
|
+
st.done(`${uniqueCves} CVE${filtered ? ` · ${filtered} false-positive(s) filtered` : ""}${hasNvdKey ? "" : " · no key (slow)"}`);
|
|
604
|
+
} catch (err) { st.fail(err.message); }
|
|
538
605
|
}
|
|
539
606
|
}
|
|
540
607
|
|
|
@@ -545,16 +612,17 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
545
612
|
// vendored .js (which can live in a Maven project's resources too). The
|
|
546
613
|
// scanner is owned by the npm codec but runs whenever --retire is on.
|
|
547
614
|
let retireMatches = [];
|
|
548
|
-
if (
|
|
615
|
+
if (willRetire) {
|
|
616
|
+
const st = progress.start("retire.js (vendored JS)");
|
|
549
617
|
const sc = (getCodec("npm").nativeScanners || []).find(s => s.kind === "vendored");
|
|
550
|
-
if (sc) {
|
|
618
|
+
if (!sc) { st.skip("scanner unavailable"); }
|
|
619
|
+
else if (!options.src) { st.skip("no source tree (descriptor import)"); }
|
|
620
|
+
else {
|
|
551
621
|
try {
|
|
552
622
|
const r = await sc.scan(resolved, { src: options.src, verbose, retireRefresh: !!options.retireRefresh, offline });
|
|
553
623
|
retireMatches = r.matches || [];
|
|
554
|
-
|
|
555
|
-
} catch (err) {
|
|
556
|
-
console.warn(chalk.yellow("⚠️ retire.js skipped:"), err.message);
|
|
557
|
-
}
|
|
624
|
+
st.done(`${retireMatches.length} finding(s)`);
|
|
625
|
+
} catch (err) { st.fail(err.message); }
|
|
558
626
|
}
|
|
559
627
|
}
|
|
560
628
|
|
|
@@ -562,16 +630,16 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
562
630
|
let snykMatches = [];
|
|
563
631
|
if (options.snyk) {
|
|
564
632
|
if (!options.target) {
|
|
565
|
-
|
|
633
|
+
ui.warn("--snyk requires --target (snyk runs on cleaned POMs)");
|
|
566
634
|
} else {
|
|
567
635
|
const snyk = require("./lib/snyk");
|
|
568
636
|
try {
|
|
569
637
|
const raw = await snyk.runSnykTest(options.target, { verbose });
|
|
570
638
|
snykMatches = snyk.parseSnykResults(raw);
|
|
571
639
|
cveMatches = snyk.mergeWithFadResults(cveMatches, snykMatches);
|
|
572
|
-
|
|
640
|
+
ui.ok(`Snyk: ${snykMatches.length} findings merged`);
|
|
573
641
|
} catch (err) {
|
|
574
|
-
|
|
642
|
+
ui.warn(`Snyk run failed: ${err.message}`);
|
|
575
643
|
}
|
|
576
644
|
}
|
|
577
645
|
}
|
|
@@ -589,50 +657,65 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
589
657
|
|
|
590
658
|
const stats = computeStats(prodActive);
|
|
591
659
|
const devStats = computeStats(devActive);
|
|
592
|
-
|
|
593
|
-
console.log(` critical=${stats.critical} high=${stats.high} medium=${stats.medium} low=${stats.low} unknown=${stats.unknown}`);
|
|
660
|
+
const sev = ui.sevColor;
|
|
594
661
|
const depLabel = d => d.ecosystem === "npm" ? `npm:${d.artifactId}` : `${d.groupId}:${d.artifactId}`;
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
662
|
+
const coordOf = depLabel; // npm deps show as "npm:name", others as "g:a"
|
|
663
|
+
const fmtStats = s => [
|
|
664
|
+
s.critical ? sev("CRITICAL")(`${s.critical} critical`) : null,
|
|
665
|
+
s.high ? sev("HIGH")(`${s.high} high`) : null,
|
|
666
|
+
s.medium ? sev("MEDIUM")(`${s.medium} medium`) : null,
|
|
667
|
+
s.low ? sev("LOW")(`${s.low} low`) : null,
|
|
668
|
+
s.unknown ? chalk.gray(`${s.unknown} unknown`) : null,
|
|
669
|
+
].filter(Boolean).join(" ") || chalk.gray("none");
|
|
670
|
+
const heading = (label, n, extra = "") => console.log("\n " + chalk.bold(label) + chalk.dim(` (${n})`) + (extra ? " " + extra : ""));
|
|
671
|
+
|
|
672
|
+
ui.section("Results");
|
|
673
|
+
|
|
674
|
+
heading("CVE · production", prodActive.length, fmtStats(stats));
|
|
675
|
+
for (const m of prodActive.slice(0, 12)) {
|
|
676
|
+
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}`));
|
|
598
677
|
}
|
|
599
|
-
if (prodActive.length >
|
|
600
|
-
if (cpeFilteredCount) console.log(chalk.
|
|
678
|
+
if (prodActive.length > 12) console.log(chalk.dim(` …and ${prodActive.length - 12} more (see report)`));
|
|
679
|
+
if (cpeFilteredCount) console.log(chalk.dim(` ${cpeFilteredCount} likely false positive(s) → report appendix`));
|
|
601
680
|
|
|
602
|
-
|
|
603
|
-
console.log(chalk.bold.cyan(`\n 2. CVE in dev dependencies (${devActive.length})`));
|
|
604
|
-
console.log(` critical=${devStats.critical} high=${devStats.high} medium=${devStats.medium} low=${devStats.low} unknown=${devStats.unknown}`);
|
|
605
|
-
}
|
|
606
|
-
if (retireMatches.length) {
|
|
607
|
-
console.log(chalk.bold.cyan(`\n 3. Vendored JS (retire.js): ${retireMatches.length}`));
|
|
608
|
-
for (const m of retireMatches.slice(0, 10)) {
|
|
609
|
-
console.log(` ${chalk.red((m.cve.severity || "?").padEnd(8))} ${m.cve.id} ${m.dep.artifactId}@${m.dep.version} ← ${m.dep.vendoredFile}`);
|
|
610
|
-
}
|
|
611
|
-
if (retireMatches.length > 10) console.log(` ... and ${retireMatches.length - 10} more (see report)`);
|
|
612
|
-
}
|
|
681
|
+
heading("CVE · dev", devActive.length, devActive.length ? fmtStats(devStats) : "");
|
|
613
682
|
|
|
614
|
-
|
|
615
|
-
const
|
|
683
|
+
heading("EOL frameworks", eolResults.length);
|
|
684
|
+
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)));
|
|
685
|
+
if (eolResults.length > 8) console.log(chalk.dim(` …and ${eolResults.length - 8} more`));
|
|
616
686
|
|
|
617
|
-
|
|
618
|
-
for (const
|
|
619
|
-
if (
|
|
687
|
+
heading("Obsolete / deprecated", obsoleteResults.length);
|
|
688
|
+
for (const o of obsoleteResults.slice(0, 8)) console.log(" " + chalk.dim(`${coordOf(o.dep)}:${o.dep.version}`) + " → " + (o.replacement || chalk.dim("n/a")));
|
|
689
|
+
if (obsoleteResults.length > 8) console.log(chalk.dim(` …and ${obsoleteResults.length - 8} more`));
|
|
620
690
|
|
|
621
|
-
|
|
622
|
-
for (const o of
|
|
623
|
-
if (
|
|
691
|
+
heading("Outdated", outdatedResults.length, options.allLibs ? "" : chalk.dim("pass -a/--allLibs to query registries"));
|
|
692
|
+
for (const o of outdatedResults.slice(0, 8)) console.log(" " + chalk.dim(coordOf(o.dep)) + ` ${o.dep.version} → ${chalk.green(o.latest)}`);
|
|
693
|
+
if (outdatedResults.length > 8) console.log(chalk.dim(` …and ${outdatedResults.length - 8} more`));
|
|
624
694
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
695
|
+
if (retireMatches.length) {
|
|
696
|
+
heading("Vendored JS (retire.js)", retireMatches.length);
|
|
697
|
+
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}`));
|
|
698
|
+
if (retireMatches.length > 8) console.log(chalk.dim(` …and ${retireMatches.length - 8} more`));
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (scanWarnings.length) {
|
|
702
|
+
console.log();
|
|
703
|
+
ui.warn(`${scanWarnings.length} scan-completeness note(s) — a real Maven/Snyk run may surface more:`);
|
|
704
|
+
for (const w of scanWarnings) {
|
|
705
|
+
ui.info(chalk.dim(`[${w.type}] ${w.message}`));
|
|
706
|
+
for (const it of (w.items || []).slice(0, 4)) console.log(" " + chalk.dim(`· ${it}`));
|
|
707
|
+
if ((w.items || []).length > 4) console.log(" " + chalk.dim(`· …and ${w.items.length - 4} more`));
|
|
708
|
+
}
|
|
709
|
+
}
|
|
630
710
|
|
|
631
711
|
const reportDir = options.reportOutput || "./fad-checker-report";
|
|
632
712
|
await fs.promises.mkdir(reportDir, { recursive: true });
|
|
713
|
+
// --import-anonymized has no source tree; keep the report path-free (consistent
|
|
714
|
+
// with the anonymized descriptor it was fed).
|
|
715
|
+
const srcResolved = options.src ? path.resolve(options.src) : null;
|
|
633
716
|
const projectInfo = {
|
|
634
|
-
name: path.basename(
|
|
635
|
-
src: path
|
|
717
|
+
name: srcResolved ? path.basename(srcResolved) : "anonymized-descriptor",
|
|
718
|
+
src: srcResolved || "(anonymized descriptor — source path withheld)",
|
|
636
719
|
generatedAt: new Date().toISOString(),
|
|
637
720
|
toolVersion: pkg.version,
|
|
638
721
|
cveDataDate,
|
|
@@ -664,7 +747,10 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
664
747
|
}] : []),
|
|
665
748
|
],
|
|
666
749
|
});
|
|
667
|
-
|
|
750
|
+
ui.section("Report");
|
|
751
|
+
ui.ok(chalk.white(htmlPath));
|
|
752
|
+
ui.ok(chalk.white(docPath));
|
|
753
|
+
console.log();
|
|
668
754
|
}
|
|
669
755
|
|
|
670
756
|
/**
|