fad-checker 1.0.6 → 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 +62 -0
- package/README.md +77 -15
- package/completions/fad-checker.bash +4 -1
- package/completions/fad-checker.zsh +9 -0
- package/data/eol-mapping.json +17 -0
- package/fad-checker.js +353 -241
- package/lib/codecs/codec.interface.js +27 -0
- package/lib/codecs/composer/parse.js +59 -0
- package/lib/codecs/composer/registry.js +92 -0
- package/lib/codecs/composer.codec.js +93 -0
- package/lib/codecs/index.js +37 -0
- package/lib/codecs/maven.codec.js +71 -0
- package/lib/{npm → codecs/npm}/collect.js +58 -35
- package/lib/{npm → codecs/npm}/parse.js +114 -10
- package/lib/{npm → codecs/npm}/registry.js +4 -3
- package/lib/codecs/npm.codec.js +52 -0
- package/lib/codecs/nuget/parse.js +75 -0
- package/lib/codecs/nuget/registry.js +88 -0
- package/lib/codecs/nuget.codec.js +94 -0
- package/lib/codecs/pypi/parse.js +163 -0
- package/lib/codecs/pypi/registry.js +89 -0
- package/lib/codecs/pypi.codec.js +102 -0
- package/lib/codecs/recipes.js +108 -0
- package/lib/codecs/select.js +27 -0
- package/lib/codecs/yarn.codec.js +19 -0
- package/lib/core.js +35 -1
- package/lib/cve-match.js +18 -11
- package/lib/cve-report.js +34 -70
- package/lib/dep-record.js +60 -0
- package/lib/deps-descriptor.js +110 -0
- package/lib/nvd.js +4 -3
- package/lib/osv.js +29 -18
- package/lib/outdated.js +20 -4
- package/lib/retire.js +77 -13
- package/lib/transitive.js +3 -3
- package/lib/ui.js +87 -0
- package/package.json +4 -2
- package/CLAUDE.md +0 -129
- package/docs/ARCHITECTURE.md +0 -154
- package/docs/USAGE.md +0 -179
- 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 -171
- 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/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 -9
- 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/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/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 -146
- package/test/outdated.test.js +0 -101
- 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")
|
|
@@ -187,8 +191,14 @@ program
|
|
|
187
191
|
.option("--no-retire", "skip retire.js vendored-JS scan")
|
|
188
192
|
.option("--retire-refresh", "ignore retire cache and re-scan")
|
|
189
193
|
.option("--transitive-depth <n>", "max transitive depth", "6")
|
|
190
|
-
.option("--ecosystem <
|
|
191
|
-
.option("--no-
|
|
194
|
+
.option("--ecosystem <list>", "codecs to run: auto|all|<comma list> e.g. maven,npm,nuget,composer,pypi (default: auto = detected)", "auto")
|
|
195
|
+
.option("--no-maven", "skip the Maven codec")
|
|
196
|
+
.option("--no-npm", "skip the npm codec")
|
|
197
|
+
.option("--no-yarn", "skip the Yarn codec")
|
|
198
|
+
.option("--no-nuget", "skip the NuGet (C#/.NET) codec")
|
|
199
|
+
.option("--no-composer", "skip the Composer (PHP) codec")
|
|
200
|
+
.option("--no-pypi", "skip the PyPI (Python) codec")
|
|
201
|
+
.option("--no-js", "alias: skip JS/npm/yarn manifests even if present (Maven-only)")
|
|
192
202
|
.option("--repo <url...>", "extra Maven repository URL(s) to try before Maven Central. Supports https://user:pass@host/path/. Repeatable.")
|
|
193
203
|
.option("--add-repo <name>", "persist a Maven repo: --add-repo <name> <url> [--auth user:pass]")
|
|
194
204
|
.option("--remove-repo <name>", "remove a persisted Maven repo by name")
|
|
@@ -202,6 +212,16 @@ const verbose = !!options.verbose;
|
|
|
202
212
|
// Read-only when no target is given. No need for an explicit --test flag.
|
|
203
213
|
const readOnly = !options.target;
|
|
204
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
|
+
|
|
205
225
|
if (options.src && options.target) {
|
|
206
226
|
const rel = path.relative(path.resolve(options.src), path.resolve(options.target));
|
|
207
227
|
const isSubdir = !rel || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
@@ -220,16 +240,16 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
220
240
|
try {
|
|
221
241
|
const hit = await existsInAny(repos, p, { userAgent: "fad-checker-existence" });
|
|
222
242
|
if (hit) return true;
|
|
223
|
-
console.log(
|
|
243
|
+
if (verbose) console.log(chalk.dim(` not on any repo: ${g}:${a}`));
|
|
224
244
|
return false;
|
|
225
245
|
} catch (err) {
|
|
226
|
-
console.info(`error querying repos: ${g}:${a} — ${err.message}`);
|
|
246
|
+
if (verbose) console.info(chalk.dim(` error querying repos: ${g}:${a} — ${err.message}`));
|
|
227
247
|
return false;
|
|
228
248
|
}
|
|
229
249
|
}
|
|
230
250
|
|
|
231
251
|
(async function main() {
|
|
232
|
-
|
|
252
|
+
ui.banner();
|
|
233
253
|
|
|
234
254
|
// Build the Maven repo list once: persisted repos (from ~/.fad-checker/config.json)
|
|
235
255
|
// + ad-hoc --repo URLs + Maven Central as final fallback. Used by transitive
|
|
@@ -238,167 +258,200 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
238
258
|
const { buildRepoList } = require("./lib/maven-repo");
|
|
239
259
|
const extraRepos = (options.repo || []).map(url => ({ url }));
|
|
240
260
|
const mavenRepos = buildRepoList(getMavenRepos(), extraRepos);
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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));
|
|
244
265
|
|
|
245
|
-
const pomFiles = core.findPomFiles(options.src);
|
|
246
|
-
const allPomMetadata = core.newMetadataStore();
|
|
247
|
-
const allPropsByPom = {};
|
|
248
266
|
let wrotePom = 0;
|
|
249
267
|
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
+
}
|
|
257
292
|
|
|
258
|
-
|
|
259
|
-
|
|
293
|
+
// --- Codec detection + selection ---
|
|
294
|
+
const { detectCodecs, allCodecs, getCodec } = require("./lib/codecs");
|
|
295
|
+
const { resolveActiveCodecs } = require("./lib/codecs/select");
|
|
296
|
+
const eco = (options.ecosystem || "auto").toLowerCase();
|
|
297
|
+
const detected = (eco === "auto") ? detectCodecs(options.src).map(c => c.id) : allCodecs().map(c => c.id);
|
|
298
|
+
const noCodecs = ["maven", "npm", "yarn", "nuget", "composer", "pypi"].filter(id => options[id] === false);
|
|
299
|
+
const activeIds = resolveActiveCodecs(eco, detected, { noCodecs, noJs: !options.js });
|
|
300
|
+
const runMaven = activeIds.includes("maven");
|
|
301
|
+
const runNpm = activeIds.includes("npm") || activeIds.includes("yarn");
|
|
302
|
+
|
|
303
|
+
// --- Collect deps from every active codec into one Map (coordKeys never collide) ---
|
|
304
|
+
const resolved = new Map();
|
|
305
|
+
let mavenCtx = null;
|
|
306
|
+
const collectWarnings = [];
|
|
307
|
+
for (const id of activeIds) {
|
|
308
|
+
if (id === "yarn") continue; // the npm codec already collects yarn.lock
|
|
309
|
+
const codec = getCodec(id);
|
|
310
|
+
let res;
|
|
311
|
+
try {
|
|
312
|
+
res = await codec.collect(options.src, { ignoreTest: !!options.ignoreTest, deps2Exclude, verbose });
|
|
313
|
+
} catch (err) {
|
|
314
|
+
console.warn(chalk.red(`❌ ${id} collect failed:`), chalk.dim(err.message));
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
for (const [k, v] of res.deps) resolved.set(k, v);
|
|
318
|
+
if (res.warnings?.length) collectWarnings.push(...res.warnings);
|
|
319
|
+
if (id === "maven") mavenCtx = res._maven;
|
|
260
320
|
}
|
|
261
321
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
+
}
|
|
265
338
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
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
|
+
}
|
|
272
353
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
catch (err) { console.error(`❌ inheritance failed for ${pom}:`, err.message); }
|
|
277
|
-
}
|
|
354
|
+
if (!readOnly) {
|
|
355
|
+
try { await rimraf(options.target); } catch (_) { /* fresh dir */ }
|
|
356
|
+
}
|
|
278
357
|
|
|
279
|
-
|
|
358
|
+
// Maven POM rewrite (cleanup feature). Parse + inheritance already happened
|
|
359
|
+
// inside the maven codec's collect(); we reuse its metadata store here.
|
|
360
|
+
if (runMaven && mavenCtx) {
|
|
361
|
+
const { store, propsByPom, pomFiles } = mavenCtx;
|
|
280
362
|
const rewriteOpts = { srcRoot: options.src, targetRoot: options.target, deps2Exclude, verbose, readOnly };
|
|
281
363
|
for (const pom of pomFiles) {
|
|
282
364
|
try {
|
|
283
|
-
if (await core.rewritePoms(pom,
|
|
365
|
+
if (await core.rewritePoms(pom, store, propsByPom, rewriteOpts)) wrotePom++;
|
|
284
366
|
} catch (err) {
|
|
285
|
-
console.error(chalk.red(
|
|
367
|
+
console.error(chalk.red(` ✗ rewrite failed for ${pom}:`), err.message);
|
|
286
368
|
}
|
|
287
369
|
}
|
|
288
370
|
}
|
|
289
371
|
|
|
290
|
-
// ----------
|
|
372
|
+
// ---------- Maven POM analysis summary (parents missing / excluded) ----------
|
|
291
373
|
let privateLibIds = [];
|
|
292
|
-
if (runMaven) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
console.log(chalk.cyanBright("─────────────────────────────────────────────\n"));
|
|
296
|
-
|
|
297
|
-
const missingParents = Object.keys(allPomMetadata.missingById)
|
|
298
|
-
.filter(id => {
|
|
299
|
-
const parts = id.split(":");
|
|
300
|
-
if (parts.length === 2) return false;
|
|
301
|
-
return !(allPomMetadata.byId[id] || allPomMetadata.byId[`${parts[0]}:${parts[1]}`]);
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
if (missingParents.length) {
|
|
305
|
-
console.log(chalk.yellowBright("⚠️ Parents libs Maven manquants ( si ces lib sont privées snyk plantera ) :"));
|
|
306
|
-
console.log(missingParents.map(id => chalk.yellow(" • ") + id).join("\n"));
|
|
307
|
-
} else {
|
|
308
|
-
console.log(chalk.greenBright("✅ Aucun parent Maven manquant."));
|
|
309
|
-
}
|
|
374
|
+
if (runMaven && mavenCtx) {
|
|
375
|
+
const allPomMetadata = mavenCtx.store; // reuse the codec's parsed metadata
|
|
376
|
+
ui.section("Maven POM analysis");
|
|
310
377
|
|
|
311
|
-
|
|
312
|
-
const anyMissingLibs = Object.keys(allPomMetadata.anyMissingById)
|
|
313
|
-
.filter(id => {
|
|
314
|
-
const parts = id.split(":");
|
|
315
|
-
if (parts.length === 3) return false;
|
|
316
|
-
return !(allPomMetadata.byId[id] || allPomMetadata.byId[`${parts[0]}:${parts[1]}`]);
|
|
317
|
-
});
|
|
318
|
-
const limit = pLimit(10);
|
|
319
|
-
console.log(chalk.magentaBright("\n🚫 Libs absentes de Maven Central :"));
|
|
320
|
-
const results = await Promise.all(anyMissingLibs.map(id => {
|
|
321
|
-
const [g, a] = id.split(":");
|
|
322
|
-
return limit(async () => ({ id, found: await checkMavenLibExist(g, a, mavenRepos) }));
|
|
323
|
-
}));
|
|
324
|
-
for (const r of results) if (r && r.found === false) privateLibIds.push(r.id);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (deps2Exclude) {
|
|
328
|
-
const excludedLibs = Object.keys(allPomMetadata.excludedById)
|
|
378
|
+
const missingParents = Object.keys(allPomMetadata.missingById)
|
|
329
379
|
.filter(id => {
|
|
330
380
|
const parts = id.split(":");
|
|
331
381
|
if (parts.length === 2) return false;
|
|
332
382
|
return !(allPomMetadata.byId[id] || allPomMetadata.byId[`${parts[0]}:${parts[1]}`]);
|
|
333
383
|
});
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
-
|
|
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`));
|
|
337
388
|
} else {
|
|
338
|
-
|
|
389
|
+
ui.ok("no missing Maven parent POMs");
|
|
390
|
+
}
|
|
391
|
+
|
|
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
|
+
}
|
|
339
410
|
}
|
|
340
|
-
} else {
|
|
341
|
-
console.log(chalk.magentaBright("\n🚫 Bibliothèques exclues ( privées ) et manquantes : "));
|
|
342
|
-
console.log(chalk.magenta(" • ") + "Pas d'exclusions ( on considère donc que toutes les deps hors parents sont publiques )");
|
|
343
|
-
}
|
|
344
411
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
+
}
|
|
350
431
|
|
|
351
432
|
// ---------- Report flow (CVE / EOL / Obsolete) ----------
|
|
352
433
|
if (options.report) {
|
|
353
|
-
await runReportFlow(
|
|
434
|
+
await runReportFlow(resolved, { activeIds, runMaven, runNpm, privateLibIds, mavenRepos, collectWarnings });
|
|
354
435
|
} else if (!readOnly) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
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`));
|
|
358
439
|
}
|
|
359
440
|
})();
|
|
360
441
|
|
|
361
|
-
async function runReportFlow(
|
|
362
|
-
const { runMaven = true, runNpm = false, privateLibIds = [], mavenRepos = [] } = ecoFlags;
|
|
363
|
-
const {
|
|
364
|
-
const { ensureCveIndex } = require("./lib/cve-download");
|
|
442
|
+
async function runReportFlow(resolved, ecoFlags = {}) {
|
|
443
|
+
const { activeIds = [], runMaven = true, runNpm = false, privateLibIds = [], mavenRepos = [], collectWarnings = [] } = ecoFlags;
|
|
444
|
+
const { expandWithTransitives } = require("./lib/cve-match");
|
|
365
445
|
const { writeReports, computeStats } = require("./lib/cve-report");
|
|
446
|
+
const { getCodec } = require("./lib/codecs");
|
|
366
447
|
const outdated = require("./lib/outdated");
|
|
448
|
+
const { getNvdApiKey } = require("./lib/config");
|
|
367
449
|
const offline = !!options.offline;
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
// Collect deps + parent POMs
|
|
373
|
-
const resolved = runMaven
|
|
374
|
-
? collectResolvedDeps(allPomMetadata, allPropsByPom, {
|
|
375
|
-
ignoreTest: !!options.ignoreTest,
|
|
376
|
-
deps2Exclude,
|
|
377
|
-
})
|
|
378
|
-
: new Map();
|
|
379
|
-
const mavenCount = resolved.size;
|
|
380
|
-
if (runMaven) console.log(chalk.blue(`📚 ${mavenCount} dépendances Maven directes (incl. parent POMs)`));
|
|
381
|
-
|
|
382
|
-
// Merge npm/yarn deps into the same Map (keys are "npm:name", no collision with "g:a")
|
|
383
|
-
let npmWarnings = [];
|
|
450
|
+
|
|
451
|
+
// Collection counts already shown in the "Collection" section by main();
|
|
452
|
+
// for --import-anonymized they were shown in the "Anonymized descriptor" section.
|
|
453
|
+
const npmWarnings = collectWarnings || [];
|
|
384
454
|
let scanWarnings = [];
|
|
385
|
-
if (runNpm) {
|
|
386
|
-
const { collectNpmDeps } = require("./lib/npm/collect");
|
|
387
|
-
const npmDeps = collectNpmDeps(options.src, {
|
|
388
|
-
ignoreTest: !!options.ignoreTest,
|
|
389
|
-
deps2Exclude,
|
|
390
|
-
verbose,
|
|
391
|
-
});
|
|
392
|
-
for (const [k, v] of npmDeps) resolved.set(k, v);
|
|
393
|
-
console.log(chalk.blue(`📦 ${npmDeps.size} dépendances npm/yarn ajoutées (total: ${resolved.size})`));
|
|
394
|
-
npmWarnings = npmDeps.warnings || [];
|
|
395
|
-
if (npmWarnings.length) {
|
|
396
|
-
console.log(chalk.yellow(`⚠️ ${npmWarnings.length} JS manifest(s) sans lockfile — non scannés :`));
|
|
397
|
-
for (const w of npmWarnings) {
|
|
398
|
-
console.log(chalk.yellow(` • ${w.manifestPath} — ${w.message}`));
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
455
|
const directCount = resolved.size;
|
|
403
456
|
|
|
404
457
|
// Scan-completeness signals: BOMs and unresolved-version deps mean fad-checker
|
|
@@ -406,20 +459,34 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
|
|
|
406
459
|
if (runMaven) {
|
|
407
460
|
const { detectScanCompletenessWarnings } = require("./lib/scan-completeness");
|
|
408
461
|
scanWarnings = detectScanCompletenessWarnings(resolved, { ranSnyk: !!options.snyk, ranTransitive: !!options.transitive });
|
|
409
|
-
if (scanWarnings.length) {
|
|
410
|
-
console.log(chalk.yellow(`\n⚠️ ${scanWarnings.length} scan-completeness alert(s) — a real Maven/Snyk run may surface more findings:`));
|
|
411
|
-
for (const w of scanWarnings) {
|
|
412
|
-
console.log(chalk.yellow(` • [${w.type}] ${w.message}`));
|
|
413
|
-
if (w.items?.length) {
|
|
414
|
-
const shown = w.items.slice(0, 6);
|
|
415
|
-
for (const it of shown) console.log(chalk.gray(` · ${it}`));
|
|
416
|
-
if (w.items.length > shown.length) console.log(chalk.gray(` · …and ${w.items.length - shown.length} more`));
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
462
|
}
|
|
421
463
|
|
|
422
|
-
|
|
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)");
|
|
423
490
|
await expandWithTransitives(resolved, {
|
|
424
491
|
verbose,
|
|
425
492
|
offline,
|
|
@@ -427,53 +494,71 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
|
|
|
427
494
|
includeTestDeps: !options.ignoreTest,
|
|
428
495
|
repos: mavenRepos,
|
|
429
496
|
});
|
|
430
|
-
|
|
497
|
+
st.done(`+${resolved.size - directCount} transitive (total ${resolved.size})`);
|
|
431
498
|
}
|
|
432
499
|
|
|
433
|
-
// 1. CVE
|
|
500
|
+
// 1. CVE — native scanner contributed by the maven codec (local cvelistV5 index).
|
|
434
501
|
let cveMatches = [];
|
|
435
502
|
let cveDataDate = null;
|
|
436
|
-
if (
|
|
503
|
+
if (willCve) {
|
|
504
|
+
const st = progress.start("CVE index (CVEProject)");
|
|
437
505
|
try {
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
});
|
|
443
|
-
cveDataDate = idx?.meta?.builtAt || null;
|
|
444
|
-
cveMatches = matchDepsAgainstCves(resolved, idx);
|
|
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)}` : ""}`);
|
|
445
510
|
} catch (err) {
|
|
446
|
-
|
|
511
|
+
st.fail(err.message);
|
|
447
512
|
}
|
|
448
513
|
}
|
|
449
514
|
|
|
450
|
-
// 2. EOL frameworks
|
|
515
|
+
// 2. EOL frameworks (endoflife.date) — always a step.
|
|
451
516
|
let eolResults = [];
|
|
452
|
-
|
|
453
|
-
|
|
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
|
+
}
|
|
454
522
|
|
|
455
|
-
// 3. Obsolete / deprecated
|
|
523
|
+
// 3. Obsolete / deprecated — local curated list, instant (no network step).
|
|
456
524
|
let obsoleteResults = [];
|
|
457
525
|
try { obsoleteResults = outdated.checkObsoleteDeps(resolved); }
|
|
458
|
-
catch (err) {
|
|
526
|
+
catch (err) { ui.warn(`obsolete check skipped: ${err.message}`); }
|
|
459
527
|
|
|
460
|
-
// 4. Outdated (latest Maven Central)
|
|
528
|
+
// 4. Outdated (latest Maven Central) — gated by --all-libs.
|
|
461
529
|
let outdatedResults = [];
|
|
462
|
-
if (
|
|
463
|
-
|
|
464
|
-
|
|
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); }
|
|
465
536
|
}
|
|
466
537
|
|
|
467
|
-
// 4a. npm registry — deprecation (always, authoritative
|
|
468
|
-
//
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
+
}
|
|
550
|
+
|
|
551
|
+
// 4b. Per-codec registry for ecosystems beyond maven/npm (composer/pypi/nuget).
|
|
552
|
+
for (const id of otherRegistryIds) {
|
|
553
|
+
const codec = getCodec(id);
|
|
554
|
+
const st = progress.start(`${codec.label || id} registry`);
|
|
555
|
+
try {
|
|
556
|
+
const reg = await codec.checkRegistry(resolved, { verbose, offline, allLibs: options.allLibs, onProgress: (p, t) => st.tick(p, t) });
|
|
557
|
+
obsoleteResults = obsoleteResults.concat(reg.deprecated || []);
|
|
558
|
+
outdatedResults = outdatedResults.concat(reg.outdated || []);
|
|
559
|
+
st.done(`${(reg.deprecated || []).length} deprecated, ${(reg.outdated || []).length} outdated`);
|
|
560
|
+
} catch (err) { st.fail(err.message); }
|
|
561
|
+
}
|
|
477
562
|
|
|
478
563
|
// Cross-section dedup: drop entries from outdated that already appear in EOL/Obsolete
|
|
479
564
|
const eolKeys = new Set(eolResults.map(r => `${r.dep.groupId}:${r.dep.artifactId}`));
|
|
@@ -484,51 +569,60 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
|
|
|
484
569
|
});
|
|
485
570
|
|
|
486
571
|
// 4b. OSV.dev — Maven-native CVE+GHSA feed (huge recall win over raw CVEProject)
|
|
487
|
-
if (
|
|
572
|
+
if (willOsv) {
|
|
573
|
+
const st = progress.start("OSV.dev");
|
|
488
574
|
try {
|
|
489
575
|
const { queryOsvForDeps } = require("./lib/osv");
|
|
490
|
-
const osvMatches = await queryOsvForDeps(resolved, { verbose, offline });
|
|
576
|
+
const osvMatches = await queryOsvForDeps(resolved, { verbose, offline, onProgress: (p, t) => st.tick(p, t) });
|
|
491
577
|
const before = cveMatches.length;
|
|
492
578
|
cveMatches = mergeBySource(cveMatches, osvMatches);
|
|
493
|
-
|
|
579
|
+
st.done(`${osvMatches.length} vulns · +${cveMatches.length - before} after merge`);
|
|
494
580
|
} catch (err) {
|
|
495
|
-
|
|
581
|
+
st.fail(err.message);
|
|
496
582
|
}
|
|
497
583
|
}
|
|
498
584
|
|
|
499
|
-
// 4c. NVD enrichment — canonical description + full CVSS for matched CVEs
|
|
500
|
-
if (
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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); }
|
|
519
605
|
}
|
|
520
606
|
}
|
|
521
607
|
|
|
522
|
-
// 5. retire.js —
|
|
523
|
-
//
|
|
608
|
+
// 5. retire.js — native "vendored" scanner contributed by the npm codec. Scans
|
|
609
|
+
// vendored JS files (jquery copies, bootstrap, pdf.js, …) that live in the
|
|
610
|
+
// source tree without any lockfile to back them.
|
|
611
|
+
// Not gated by an active npm ecosystem: retire scans the source tree for
|
|
612
|
+
// vendored .js (which can live in a Maven project's resources too). The
|
|
613
|
+
// scanner is owned by the npm codec but runs whenever --retire is on.
|
|
524
614
|
let retireMatches = [];
|
|
525
|
-
if (
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
615
|
+
if (willRetire) {
|
|
616
|
+
const st = progress.start("retire.js (vendored JS)");
|
|
617
|
+
const sc = (getCodec("npm").nativeScanners || []).find(s => s.kind === "vendored");
|
|
618
|
+
if (!sc) { st.skip("scanner unavailable"); }
|
|
619
|
+
else if (!options.src) { st.skip("no source tree (descriptor import)"); }
|
|
620
|
+
else {
|
|
621
|
+
try {
|
|
622
|
+
const r = await sc.scan(resolved, { src: options.src, verbose, retireRefresh: !!options.retireRefresh, offline });
|
|
623
|
+
retireMatches = r.matches || [];
|
|
624
|
+
st.done(`${retireMatches.length} finding(s)`);
|
|
625
|
+
} catch (err) { st.fail(err.message); }
|
|
532
626
|
}
|
|
533
627
|
}
|
|
534
628
|
|
|
@@ -536,16 +630,16 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
|
|
|
536
630
|
let snykMatches = [];
|
|
537
631
|
if (options.snyk) {
|
|
538
632
|
if (!options.target) {
|
|
539
|
-
|
|
633
|
+
ui.warn("--snyk requires --target (snyk runs on cleaned POMs)");
|
|
540
634
|
} else {
|
|
541
635
|
const snyk = require("./lib/snyk");
|
|
542
636
|
try {
|
|
543
637
|
const raw = await snyk.runSnykTest(options.target, { verbose });
|
|
544
638
|
snykMatches = snyk.parseSnykResults(raw);
|
|
545
639
|
cveMatches = snyk.mergeWithFadResults(cveMatches, snykMatches);
|
|
546
|
-
|
|
640
|
+
ui.ok(`Snyk: ${snykMatches.length} findings merged`);
|
|
547
641
|
} catch (err) {
|
|
548
|
-
|
|
642
|
+
ui.warn(`Snyk run failed: ${err.message}`);
|
|
549
643
|
}
|
|
550
644
|
}
|
|
551
645
|
}
|
|
@@ -563,50 +657,65 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
|
|
|
563
657
|
|
|
564
658
|
const stats = computeStats(prodActive);
|
|
565
659
|
const devStats = computeStats(devActive);
|
|
566
|
-
|
|
567
|
-
console.log(` critical=${stats.critical} high=${stats.high} medium=${stats.medium} low=${stats.low} unknown=${stats.unknown}`);
|
|
660
|
+
const sev = ui.sevColor;
|
|
568
661
|
const depLabel = d => d.ecosystem === "npm" ? `npm:${d.artifactId}` : `${d.groupId}:${d.artifactId}`;
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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}`));
|
|
572
677
|
}
|
|
573
|
-
if (prodActive.length >
|
|
574
|
-
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`));
|
|
575
680
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
}
|
|
580
|
-
if (
|
|
581
|
-
console.log(chalk.bold.cyan(`\n 3. Vendored JS (retire.js): ${retireMatches.length}`));
|
|
582
|
-
for (const m of retireMatches.slice(0, 10)) {
|
|
583
|
-
console.log(` ${chalk.red((m.cve.severity || "?").padEnd(8))} ${m.cve.id} ${m.dep.artifactId}@${m.dep.version} ← ${m.dep.vendoredFile}`);
|
|
584
|
-
}
|
|
585
|
-
if (retireMatches.length > 10) console.log(` ... and ${retireMatches.length - 10} more (see report)`);
|
|
586
|
-
}
|
|
681
|
+
heading("CVE · dev", devActive.length, devActive.length ? fmtStats(devStats) : "");
|
|
682
|
+
|
|
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`));
|
|
587
686
|
|
|
588
|
-
|
|
589
|
-
const
|
|
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`));
|
|
590
690
|
|
|
591
|
-
|
|
592
|
-
for (const
|
|
593
|
-
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`));
|
|
594
694
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
+
}
|
|
598
700
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
+
}
|
|
604
710
|
|
|
605
711
|
const reportDir = options.reportOutput || "./fad-checker-report";
|
|
606
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;
|
|
607
716
|
const projectInfo = {
|
|
608
|
-
name: path.basename(
|
|
609
|
-
src: path
|
|
717
|
+
name: srcResolved ? path.basename(srcResolved) : "anonymized-descriptor",
|
|
718
|
+
src: srcResolved || "(anonymized descriptor — source path withheld)",
|
|
610
719
|
generatedAt: new Date().toISOString(),
|
|
611
720
|
toolVersion: pkg.version,
|
|
612
721
|
cveDataDate,
|
|
@@ -638,7 +747,10 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
|
|
|
638
747
|
}] : []),
|
|
639
748
|
],
|
|
640
749
|
});
|
|
641
|
-
|
|
750
|
+
ui.section("Report");
|
|
751
|
+
ui.ok(chalk.white(htmlPath));
|
|
752
|
+
ui.ok(chalk.white(docPath));
|
|
753
|
+
console.log();
|
|
642
754
|
}
|
|
643
755
|
|
|
644
756
|
/**
|