fad-checker 2.2.4 → 2.3.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/README.md +12 -9
- package/fad-checker.js +59 -22
- package/lib/codecs/composer.codec.js +4 -1
- package/lib/codecs/go.codec.js +4 -1
- package/lib/codecs/gradle/catalog.js +91 -0
- package/lib/codecs/gradle/parse.js +211 -0
- package/lib/codecs/gradle.codec.js +155 -0
- package/lib/codecs/index.js +3 -2
- package/lib/codecs/maven.codec.js +1 -1
- package/lib/codecs/npm/collect.js +3 -0
- package/lib/codecs/npm.codec.js +1 -1
- package/lib/codecs/nuget.codec.js +8 -2
- package/lib/codecs/pypi.codec.js +37 -14
- package/lib/codecs/recipes.js +18 -1
- package/lib/codecs/ruby.codec.js +3 -1
- package/lib/cpe.js +39 -2
- package/lib/cve-match.js +20 -1
- package/lib/cve-report.js +58 -7
- package/lib/deps-descriptor.js +2 -2
- package/lib/osv-db.js +1 -1
- package/lib/osv.js +19 -5
- package/lib/retire.js +1 -1
- package/lib/sarif-export.js +1 -1
- package/package.json +5 -6
package/README.md
CHANGED
|
@@ -2,25 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/fad-checker)
|
|
4
4
|
[](https://www.npmjs.com/package/fad-checker)
|
|
5
|
-
[](https://github.com/
|
|
5
|
+
[](https://github.com/9pings/fad-checker/blob/main/package.json)
|
|
6
6
|
[](https://nodejs.org)
|
|
7
7
|
|
|
8
8
|
> **F**abulous **A**utonomous **D**ependency **C**hecker<br>
|
|
9
9
|
> AKA **F**uckin' **A**utonomous **D**ependency **C**hecker<br>
|
|
10
10
|
|
|
11
|
-
`fad-checker` audits **Maven · npm · Yarn · pnpm · Composer · PyPI · NuGet · Go · Ruby**, vendored JavaScript and committed native binaries in any source tree — multi-module, monorepo, polyglot — and produces a self-contained **HTML + Word report** (CVE prioritised by EPSS + CISA KEV, EOL, obsolete, outdated, licenses) plus **CycloneDX SBOM / CSAF VEX / SARIF / JSON** exports. **No build tools, no Docker, no network needed** — it reads lockfiles and manifests straight off disk.
|
|
11
|
+
`fad-checker` audits **Maven · Gradle · npm · Yarn · pnpm · Composer · PyPI · NuGet · Go · Ruby**, vendored JavaScript and committed native binaries in any source tree — multi-module, monorepo, polyglot — and produces a self-contained **HTML + Word report** (CVE prioritised by EPSS + CISA KEV, EOL, obsolete, outdated, licenses) plus **CycloneDX SBOM / CSAF VEX / SARIF / JSON** exports. **No build tools, no Docker, no network needed** — it reads lockfiles and manifests straight off disk.
|
|
12
12
|
|
|
13
|
-
🌐 **[Project site & docs →](https://
|
|
13
|
+
🌐 **[Project site & docs →](https://9pings.github.io/fad-checker/)**
|
|
14
|
+
|
|
15
|
+
> [!WARNING]
|
|
16
|
+
> **Young project — expect rough edges.** fad-checker is new and under active development, so it may still contain bugs (including false positives and false negatives). Treat its output as a strong first pass, **double-check anything critical**, and please [report issues](https://github.com/9pings/fad-checker/issues) — they get fixed fast.
|
|
14
17
|
|
|
15
18
|
<p align="center"><img src="docs/assets/demo.gif" alt="fad-checker animated terminal demo — a [n/N] checklist warming each vulnerability database, then CVE findings coloured by severity with KEV badges" height="600"></p>
|
|
16
19
|
|
|
17
20
|
## Features
|
|
18
21
|
|
|
19
|
-
- **
|
|
20
|
-
- **No build tools** — reads `pom.xml`, `package-lock`/`yarn.lock`/`pnpm-lock`, `composer.lock`, `poetry`/`Pipfile`/`uv`/`pdm` locks, `packages.lock.json`/`*.csproj`, `go.mod`, `Gemfile.lock` directly. No `mvn`/`npm install`/`pip`/`dotnet restore`/`go build`/`bundle`, no `node_modules/`. → [how it stays build-free](docs/COMPARISON.md#how-its-autonomous-no-build-tools)
|
|
22
|
+
- **10 ecosystems in one pass** — Maven, **Gradle**, npm/Yarn/pnpm, Composer (PHP), PyPI, NuGet, Go, Ruby; plus **vendored JS** (retire.js), committed **native binaries** (`.dll`/`.exe`/`.so`/`.dylib`, identified by checksum via deps.dev + CIRCL) and **embedded JARs** (fat-jars/war/ear, unzipped in-memory).
|
|
23
|
+
- **No build tools** — reads `pom.xml`, `build.gradle(.kts)`/`gradle.lockfile`/`libs.versions.toml`, `package-lock`/`yarn.lock`/`pnpm-lock`, `composer.lock`, `poetry`/`Pipfile`/`uv`/`pdm` locks, `packages.lock.json`/`*.csproj`, `go.mod`, `Gemfile.lock` directly. No `mvn`/`gradle`/`npm install`/`pip`/`dotnet restore`/`go build`/`bundle`, no `node_modules/`. → [how it stays build-free](docs/COMPARISON.md#how-its-autonomous-no-build-tools)
|
|
21
24
|
- **CVE, merged & prioritised** — CVEProject + OSV.dev + NVD, CPE/version cross-checked to cut false positives, ranked **CISA KEV → EPSS → CVSS**.
|
|
22
25
|
- **Per-module Maven version mediation** — recovers vulnerable transitive versions that a global `<dependencyManagement>` pin hides in another module (lifted Snyk-corroborated coverage **156 → 181** on a real 25-module reactor, finding CVEs a single Snyk scan missed).
|
|
23
|
-
- **Air-gapped
|
|
26
|
+
- **Air-gapped** — **zero network under `--offline`** (regression-tested), offline Maven transitive resolution, and `--osv-db` for cache-independent offline OSV recall. → [Air-gapped](#air-gapped-audits)
|
|
24
27
|
- **Supply-chain risk** — known-**malicious** advisories (`MAL-`, always block the CI gate) + suspected **typosquats** (`--typosquat`).
|
|
25
28
|
- **Lifecycle** — EOL (endoflife.date), obsolete/deprecated, outdated — across every ecosystem.
|
|
26
29
|
- **Licenses** *(opt-in `--licenses`)* — SPDX-normalised, copyleft/proprietary flagged.
|
|
@@ -63,13 +66,13 @@ A single self-contained binary (no Node), from-source install and shell completi
|
|
|
63
66
|
| **5. Obsolete libraries** | curated list (Maven) + registry maintainer flags | log4j 1.x, jackson-mapper-asl, joda-time, …; npm `deprecated`, Composer `abandoned`, PyPI `yanked`/inactive, NuGet `deprecation` |
|
|
64
67
|
| **6. Outdated libraries** | Maven Central + npm / Packagist / PyPI / NuGet registries | Available newer versions, with release dates |
|
|
65
68
|
| **7. Licenses** *(opt-in: `--licenses`)* | registry metadata + Maven POMs → SPDX policy | Each dep's license normalised to SPDX and classified; copyleft (GPL/AGPL/LGPL/MPL), proprietary and unknown flagged for review |
|
|
66
|
-
| **8. Fix Recommendations** | computed | Per-ecosystem pin recipes: Maven `<dependencyManagement>`, npm `overrides`, yarn `resolutions`, `composer require`, `pip install`, `dotnet add package` |
|
|
69
|
+
| **8. Fix Recommendations** | computed | Per-ecosystem pin recipes: Maven `<dependencyManagement>`, Gradle `constraints { }`, npm `overrides`, yarn `resolutions`, `composer require`, `pip install`, `dotnet add package` |
|
|
67
70
|
|
|
68
71
|
The HTML report opens in any browser, contains every detail (CVSS vectors, references, full descriptions, CPE configurations, via-paths for transitives) and ships a Word-compatible `.doc` twin. Every match carries a **composite priority** (KEV-exploited > EPSS likelihood > CVSS severity), and the run can additionally emit a **CycloneDX 1.6 SBOM** (`--report-sbom`, vulnerabilities inline) and a **CSAF 2.0 VEX** (`--report-csaf`) for downstream tooling.
|
|
69
72
|
|
|
70
73
|
<p align="center"><img src="docs/assets/report.png" alt="fad-checker HTML report — executive summary with severity tiles and a detailed CVE table with CWE, descriptions and fix versions" width="900"></p>
|
|
71
74
|
|
|
72
|
-
## Air-gapped
|
|
75
|
+
## Air-gapped audits
|
|
73
76
|
|
|
74
77
|
> **Zero-data-sent guarantee.** Under `--offline`, fad-checker makes **no network calls
|
|
75
78
|
> whatsoever** — it reads only the warmed `~/.fad-checker/` caches and never transmits a
|
|
@@ -80,7 +83,7 @@ The HTML report opens in any browser, contains every detail (CVSS vectors, refer
|
|
|
80
83
|
> mainstream OSS scanners, fad also resolves the **Maven transitive graph** offline — so on
|
|
81
84
|
> an air-gapped multi-module project it finds the transitive CVEs they can't.
|
|
82
85
|
|
|
83
|
-
When the audited system is **offline / confidential** (typical of a
|
|
86
|
+
When the audited system is **offline / confidential** (typical of a regulated or air-gapped audit) it
|
|
84
87
|
can't reach OSV / NVD / Maven Central / npm. Split the work across machines while keeping
|
|
85
88
|
**zero environment information** off the secure enclave: an anonymized descriptor carries
|
|
86
89
|
only **public package coordinates** — no filesystem paths, no registry URLs, no
|
package/fad-checker.js
CHANGED
|
@@ -229,7 +229,7 @@ program
|
|
|
229
229
|
.option("--export-cache <file>", "tar.gz/zip the ~/.fad-checker/ caches to <file> (excludes config.json by default)")
|
|
230
230
|
.option("--import-cache <file>", "restore ~/.fad-checker/ from a previously exported archive (existing dir is moved to .bak unless --force)")
|
|
231
231
|
.option("--include-config", "with --export-cache: also bundle config.json (contains the NVD API key)")
|
|
232
|
-
.option("--export-anonymized <file>", "offline: write an anonymized dependency descriptor (public coordinates only, no paths/URLs) for
|
|
232
|
+
.option("--export-anonymized <file>", "offline: write an anonymized dependency descriptor (public coordinates only, no paths/URLs) for air-gapped audits, then exit")
|
|
233
233
|
.option("--import-anonymized <file>", "online: scan an anonymized descriptor (no --src) to warm the caches; pair with --export-cache for offline reporting")
|
|
234
234
|
.option("--force", "with --import-cache: replace ~/.fad-checker/ without backup")
|
|
235
235
|
.option("--report-output <dir>", "report output directory", "./fad-checker-report")
|
|
@@ -246,6 +246,7 @@ program
|
|
|
246
246
|
.option("--transitive-depth <n>", "max transitive depth", "6")
|
|
247
247
|
.option("--ecosystem <list>", "codecs to run: auto|all|<comma list> e.g. maven,npm,nuget,composer,pypi,go,ruby (default: auto = detected)", "auto")
|
|
248
248
|
.option("--no-maven", "skip the Maven codec")
|
|
249
|
+
.option("--no-gradle", "skip the Gradle codec")
|
|
249
250
|
.option("--no-npm", "skip the npm codec")
|
|
250
251
|
.option("--no-yarn", "skip the Yarn codec")
|
|
251
252
|
.option("--no-nuget", "skip the NuGet (C#/.NET) codec")
|
|
@@ -451,10 +452,10 @@ async function timedPhase(label, fn) {
|
|
|
451
452
|
].filter(Boolean))];
|
|
452
453
|
const defaultExcludes = options.defaultExcludes !== false;
|
|
453
454
|
const walkOpts = { excludePath, defaultExcludes };
|
|
454
|
-
//
|
|
455
|
+
// Anonymized export (phase 1, --export-anonymized) is a purely local operation — parse the tree,
|
|
455
456
|
// emit public coordinates, exit. Force offline so no source ever touches the network.
|
|
456
457
|
if (options.exportAnonymized) options.offline = true;
|
|
457
|
-
const runMode = options.exportAnonymized ? "offline (
|
|
458
|
+
const runMode = options.exportAnonymized ? "offline (anonymized export · phase 1)"
|
|
458
459
|
: options.importAnonymized ? "import descriptor"
|
|
459
460
|
: (options.offline ? "offline" : "online");
|
|
460
461
|
if (options.src) ui.kv("source", chalk.white(options.src));
|
|
@@ -467,7 +468,7 @@ async function timedPhase(label, fn) {
|
|
|
467
468
|
|
|
468
469
|
let wrotePom = 0;
|
|
469
470
|
|
|
470
|
-
// ---
|
|
471
|
+
// --- Anonymized phase 2: import a descriptor instead of collecting ---
|
|
471
472
|
// Scans the descriptor's public coordinates online to WARM the coordinate-keyed
|
|
472
473
|
// caches (OSV/NVD/CVE/registry/EOL) + retire signatures. Pair with --export-cache.
|
|
473
474
|
if (options.importAnonymized) {
|
|
@@ -488,6 +489,12 @@ async function timedPhase(label, fn) {
|
|
|
488
489
|
const { warmRetireSignatures } = require("./lib/retire");
|
|
489
490
|
await warmRetireSignatures({ verbose });
|
|
490
491
|
}
|
|
492
|
+
// --import-anonymized is a cache-WARMING step (pair with --export-cache), not a
|
|
493
|
+
// reporting one: the path-bearing report is produced later, offline, from the warmed
|
|
494
|
+
// cache against the real source tree. So suppress the default HTML+doc output here
|
|
495
|
+
// (still honor an explicit --report-<type> if a user really wants a path-free one).
|
|
496
|
+
const anyReportRequested = [options.reportHtml, options.reportDoc, options.reportSbom, options.reportCsaf, options.reportJson, options.reportSarif].some(v => v !== undefined);
|
|
497
|
+
if (!anyReportRequested) options.report = false;
|
|
491
498
|
await runReportFlow(resolved, { activeIds, runMaven, runNpm, privateLibIds: [], mavenRepos, regMap, collectWarnings: [] });
|
|
492
499
|
return;
|
|
493
500
|
}
|
|
@@ -503,11 +510,12 @@ async function timedPhase(label, fn) {
|
|
|
503
510
|
// project), and detectCodecs' manifest-glob matcher misses versioned sonames
|
|
504
511
|
// (libz.so.1). Always make it a candidate in auto mode; --no-binaries removes it.
|
|
505
512
|
if (eco === "auto" && !detected.includes("binary")) detected.push("binary");
|
|
506
|
-
const noCodecs = ["maven", "npm", "yarn", "nuget", "composer", "pypi", "go", "ruby"].filter(id => options[id] === false);
|
|
513
|
+
const noCodecs = ["maven", "gradle", "npm", "yarn", "nuget", "composer", "pypi", "go", "ruby"].filter(id => options[id] === false);
|
|
507
514
|
// `--no-binaries` maps to options.binaries (plural) but the codec id is `binary`.
|
|
508
515
|
if (options.binaries === false) noCodecs.push("binary");
|
|
509
516
|
const activeIds = resolveActiveCodecs(eco, detected, { noCodecs, noJs: !options.js });
|
|
510
517
|
const runMaven = activeIds.includes("maven");
|
|
518
|
+
const runGradle = activeIds.includes("gradle");
|
|
511
519
|
const runNpm = activeIds.includes("npm") || activeIds.includes("yarn");
|
|
512
520
|
|
|
513
521
|
// --- Collect deps from every active codec into one Map (coordKeys never collide) ---
|
|
@@ -516,7 +524,13 @@ async function timedPhase(label, fn) {
|
|
|
516
524
|
ui.section("Collection");
|
|
517
525
|
const resolved = new Map();
|
|
518
526
|
let mavenCtx = null;
|
|
527
|
+
let gradleCtx = null;
|
|
519
528
|
const collectWarnings = [];
|
|
529
|
+
// Every descriptor file each codec actually parsed (tagged with its ecosystemType =
|
|
530
|
+
// codec id), so the report's "Scanned dependency descriptors" appendix is a COMPLETE
|
|
531
|
+
// inventory — including files parsed that contributed no scannable dep (ranges-only /
|
|
532
|
+
// no lockfile), which would otherwise be invisible.
|
|
533
|
+
const parsedManifests = [];
|
|
520
534
|
for (const id of activeIds) {
|
|
521
535
|
if (id === "yarn") continue; // the npm codec already collects yarn.lock
|
|
522
536
|
const codec = getCodec(id);
|
|
@@ -529,7 +543,9 @@ async function timedPhase(label, fn) {
|
|
|
529
543
|
}
|
|
530
544
|
for (const [k, v] of res.deps) resolved.set(k, v);
|
|
531
545
|
if (res.warnings?.length) collectWarnings.push(...res.warnings);
|
|
546
|
+
for (const p of (res.parsedManifests || [])) parsedManifests.push({ path: p, ecosystemType: id });
|
|
532
547
|
if (id === "maven") mavenCtx = res._maven;
|
|
548
|
+
if (id === "gradle") gradleCtx = res._gradle;
|
|
533
549
|
}
|
|
534
550
|
|
|
535
551
|
// --- Collection summary ---
|
|
@@ -539,12 +555,15 @@ async function timedPhase(label, fn) {
|
|
|
539
555
|
for (const d of resolved.values()) {
|
|
540
556
|
if (d.provenance === "embedded") { embeddedCount++; continue; } // counted separately below
|
|
541
557
|
if (d.provenance === "binary") { binaryCount++; continue; } // committed native libs, no manifest
|
|
542
|
-
|
|
558
|
+
// Key by ecosystemType so Gradle (ecosystem "maven") gets its own line/label.
|
|
559
|
+
const ecoKey = d.ecosystemType || d.ecosystem;
|
|
560
|
+
ecoCount[ecoKey] = (ecoCount[ecoKey] || 0) + 1;
|
|
543
561
|
}
|
|
544
562
|
if (runMaven) ui.ok(`${chalk.bold("Maven".padEnd(8))} ${mavenCtx ? mavenCtx.pomFiles.length + " module(s) · " : ""}${ecoCount.maven || 0} direct dep(s)`);
|
|
563
|
+
if (runGradle) ui.ok(`${chalk.bold("Gradle".padEnd(8))} ${ecoCount.gradle || 0} direct dep(s)`);
|
|
545
564
|
if (runNpm) ui.ok(`${chalk.bold("npm/yarn".padEnd(8))} ${ecoCount.npm || 0} dep(s)`);
|
|
546
565
|
for (const [id, n] of Object.entries(ecoCount)) {
|
|
547
|
-
if (id === "maven" || id === "npm") continue;
|
|
566
|
+
if (id === "maven" || id === "gradle" || id === "npm") continue;
|
|
548
567
|
ui.ok(`${chalk.bold(((getCodec(id)?.label) || id).padEnd(8))} ${n} dep(s)`);
|
|
549
568
|
}
|
|
550
569
|
if (embeddedCount) ui.ok(`${chalk.bold("Embedded".padEnd(8))} ${embeddedCount} coord(s) in .jar/.war/.ear`);
|
|
@@ -556,7 +575,7 @@ async function timedPhase(label, fn) {
|
|
|
556
575
|
if (collectWarnings.length > 5) ui.info(chalk.dim(`…and ${collectWarnings.length - 5} more`));
|
|
557
576
|
}
|
|
558
577
|
|
|
559
|
-
// ---
|
|
578
|
+
// --- Anonymized phase 1: export a descriptor and exit (no network, no report) ---
|
|
560
579
|
if (options.exportAnonymized) {
|
|
561
580
|
const { serializeDeps } = require("./lib/deps-descriptor");
|
|
562
581
|
const pkgVersion = require("./package.json").version;
|
|
@@ -697,7 +716,7 @@ async function timedPhase(label, fn) {
|
|
|
697
716
|
// The scan always runs — it feeds the terminal summary, the file outputs and the
|
|
698
717
|
// CI gate. Which files get written is decided by the --report-* family inside
|
|
699
718
|
// (HTML + .doc by default; --no-report writes nothing).
|
|
700
|
-
await runReportFlow(resolved, { activeIds, runMaven, runNpm, privateLibIds, mavenRepos, regMap, collectWarnings, mavenPropsByPom: mavenCtx?.propsByPom, mavenStore: mavenCtx?.store });
|
|
719
|
+
await runReportFlow(resolved, { activeIds, runMaven, runGradle, runNpm, privateLibIds, mavenRepos, regMap, collectWarnings, mavenPropsByPom: mavenCtx?.propsByPom, mavenStore: mavenCtx?.store, gradlePlatformBoms: gradleCtx?.platformBoms || [], parsedManifests });
|
|
701
720
|
if (!readOnly) {
|
|
702
721
|
ui.section("Next step");
|
|
703
722
|
ui.info(`run Snyk on the cleaned tree:`);
|
|
@@ -706,7 +725,7 @@ async function timedPhase(label, fn) {
|
|
|
706
725
|
})();
|
|
707
726
|
|
|
708
727
|
async function runReportFlow(resolved, ecoFlags = {}) {
|
|
709
|
-
const { activeIds = [], runMaven = true, runNpm = false, privateLibIds = [], mavenRepos = [], regMap = {}, collectWarnings = [], mavenPropsByPom = null, mavenStore = null } = ecoFlags;
|
|
728
|
+
const { activeIds = [], runMaven = true, runGradle = false, runNpm = false, privateLibIds = [], mavenRepos = [], regMap = {}, collectWarnings = [], mavenPropsByPom = null, mavenStore = null, gradlePlatformBoms = [], parsedManifests = [] } = ecoFlags;
|
|
710
729
|
const registriesFor = eco => regMap[eco] || [];
|
|
711
730
|
const { expandWithTransitives } = require("./lib/cve-match");
|
|
712
731
|
const { writeReports, computeStats } = require("./lib/cve-report");
|
|
@@ -736,11 +755,14 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
736
755
|
}
|
|
737
756
|
|
|
738
757
|
// Decide which update steps will run (from flags) so the [n/N] counter is accurate.
|
|
739
|
-
|
|
758
|
+
// Gradle deps are Maven coordinates (ecosystem "maven"), so the Maven CVE-index scanner
|
|
759
|
+
// and the Maven-Central transitive/BOM/outdated/EOL passes cover them — they're EXCLUDED
|
|
760
|
+
// from the per-codec registry loop below to avoid double-processing.
|
|
761
|
+
const cveScanner = (runMaven || runGradle) ? (getCodec("maven").nativeScanners || []).find(s => s.kind === "cve") : null;
|
|
740
762
|
const cveIndexExists = fs.existsSync(require("./lib/cve-download").CVE_INDEX_PATH);
|
|
741
|
-
const otherRegistryIds = activeIds.filter(id => id !== "maven" && id !== "npm" && id !== "yarn" && getCodec(id)?.checkRegistry);
|
|
763
|
+
const otherRegistryIds = activeIds.filter(id => id !== "maven" && id !== "gradle" && id !== "npm" && id !== "yarn" && getCodec(id)?.checkRegistry);
|
|
742
764
|
const willCve = !!cveScanner && (!(options.cveOffline || offline) || cveIndexExists);
|
|
743
|
-
const willTransitive = !!(options.transitive && runMaven);
|
|
765
|
+
const willTransitive = !!(options.transitive && (runMaven || runGradle));
|
|
744
766
|
// Per-module version mediation overlay: recover transitive versions the global
|
|
745
767
|
// transitive pass masks via cross-module depMgmt bleed. Runs after (and only when)
|
|
746
768
|
// the global pass runs, and needs the parsed store + per-module props.
|
|
@@ -751,11 +773,18 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
751
773
|
// never the network — same as transitive resolution).
|
|
752
774
|
const importBoms = (runMaven && mavenPropsByPom)
|
|
753
775
|
? require("./lib/maven-bom").collectImportBoms(mavenPropsByPom) : [];
|
|
754
|
-
|
|
776
|
+
// Gradle `platform(...)` BOMs are the same as Maven `<scope>import</scope>` BOMs — feed
|
|
777
|
+
// them through the same managed-version backfill (e.g. spring-boot-dependencies → all the
|
|
778
|
+
// versionless spring-boot-starter-* declared in build.gradle get their version).
|
|
779
|
+
const gradleBoms = (gradlePlatformBoms || [])
|
|
780
|
+
.map(b => ({ groupId: b.group, artifactId: b.name, version: b.version }))
|
|
781
|
+
.filter(b => b.groupId && b.artifactId && b.version);
|
|
782
|
+
const allBoms = importBoms.concat(gradleBoms);
|
|
783
|
+
const willBom = allBoms.length > 0;
|
|
755
784
|
const willOsv = !!options.osv;
|
|
756
785
|
// Local OSV DB import (Maven): offline-complete OSV recall, independent of the per-dep
|
|
757
786
|
// OSV.dev cache. Opt-in (downloads ~9 MB once); then matches online or offline.
|
|
758
|
-
const willOsvDb = !!options.osvDb && runMaven;
|
|
787
|
+
const willOsvDb = !!options.osvDb && (runMaven || runGradle);
|
|
759
788
|
const willOutdated = !!options.allLibs;
|
|
760
789
|
const willNvd = !!options.nvd;
|
|
761
790
|
const willEpss = !!options.epss;
|
|
@@ -772,9 +801,10 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
772
801
|
if (willBom) {
|
|
773
802
|
const st = progress.start("BOM version resolution (Maven Central)");
|
|
774
803
|
try {
|
|
775
|
-
const {
|
|
776
|
-
const
|
|
777
|
-
|
|
804
|
+
const { resolveBomManagedVersions, backfillVersions } = require("./lib/maven-bom");
|
|
805
|
+
const mgmt = await resolveBomManagedVersions(allBoms, { repos: mavenRepos, offline, verbose });
|
|
806
|
+
const filled = backfillVersions(resolved, mgmt);
|
|
807
|
+
st.done(`${filled} dep version(s) from ${allBoms.length} import BOM(s)`);
|
|
778
808
|
} catch (err) { st.fail(err.message); }
|
|
779
809
|
}
|
|
780
810
|
|
|
@@ -811,7 +841,7 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
811
841
|
// Scan-completeness signals — computed NOW (after BOM backfill) so only the deps
|
|
812
842
|
// still without a concrete version (external BOM truly unreachable, or offline)
|
|
813
843
|
// are flagged, not the ones we just resolved from spring-boot-dependencies & co.
|
|
814
|
-
if (runMaven) {
|
|
844
|
+
if (runMaven || runGradle) {
|
|
815
845
|
const { detectScanCompletenessWarnings } = require("./lib/scan-completeness");
|
|
816
846
|
scanWarnings = detectScanCompletenessWarnings(resolved, { ranSnyk: !!options.snyk, ranTransitive: !!options.transitive });
|
|
817
847
|
}
|
|
@@ -903,6 +933,11 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
903
933
|
const k = `${r.dep.groupId}:${r.dep.artifactId}`;
|
|
904
934
|
return !eolKeys.has(k) && !obsKeys.has(k);
|
|
905
935
|
});
|
|
936
|
+
// Outdated is a maintenance signal for deps you actually declare and can bump. An
|
|
937
|
+
// INDIRECT (transitive) dep's "latest" isn't directly actionable — you'd bump the parent
|
|
938
|
+
// — so drop transitives from the Outdated chapter (EOL/obsolete keep them: security).
|
|
939
|
+
// scope==="transitive" is the marker set by the Maven/Gradle resolver and the npm parser.
|
|
940
|
+
outdatedResults = outdatedResults.filter(r => r.dep.scope !== "transitive");
|
|
906
941
|
|
|
907
942
|
// 4b. OSV.dev — Maven-native CVE+GHSA feed (huge recall win over raw CVEProject)
|
|
908
943
|
if (willOsv) {
|
|
@@ -1192,7 +1227,7 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
1192
1227
|
let licenseResults = null;
|
|
1193
1228
|
if (willLicenses) {
|
|
1194
1229
|
try {
|
|
1195
|
-
if (runMaven) {
|
|
1230
|
+
if (runMaven || runGradle) {
|
|
1196
1231
|
const { collectMavenLicenses } = require("./lib/maven-license");
|
|
1197
1232
|
licenseFindings = licenseFindings.concat(collectMavenLicenses(resolved));
|
|
1198
1233
|
}
|
|
@@ -1265,7 +1300,7 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
1265
1300
|
const { htmlPath, docPath } = await writeReports({
|
|
1266
1301
|
cveMatches: prodMatches, devCveMatches: devMatches, embeddedMatches, retireMatches, vendoredJsInventory,
|
|
1267
1302
|
eolResults, obsoleteResults, outdatedResults, licenseResults,
|
|
1268
|
-
resolvedDeps: resolved, projectInfo, warnings: reportWarnings,
|
|
1303
|
+
resolvedDeps: resolved, projectInfo, warnings: reportWarnings, parsedManifests,
|
|
1269
1304
|
htmlPath: out.html, docPath: out.doc,
|
|
1270
1305
|
});
|
|
1271
1306
|
if (htmlPath) wrote.push(["HTML report", htmlPath]);
|
|
@@ -1311,7 +1346,9 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
1311
1346
|
ui.section("Output");
|
|
1312
1347
|
for (const [label, p] of wrote) ui.ok(`${label} → ${chalk.white(p)}`);
|
|
1313
1348
|
} else if (options.report === false) {
|
|
1314
|
-
ui.info(chalk.dim(
|
|
1349
|
+
ui.info(chalk.dim(options.importAnonymized
|
|
1350
|
+
? "import-anonymized: caches warmed, no report written (pair with --export-cache, then report offline from the real source tree)"
|
|
1351
|
+
: "--no-report: no files written (scan + gate only)"));
|
|
1315
1352
|
}
|
|
1316
1353
|
console.log();
|
|
1317
1354
|
|
|
@@ -50,8 +50,10 @@ module.exports = {
|
|
|
50
50
|
const { ignoreTest, deps2Exclude } = opts;
|
|
51
51
|
const out = new Map();
|
|
52
52
|
const warnings = [];
|
|
53
|
+
const parsedManifests = [];
|
|
53
54
|
for (const g of findComposerManifests(dir, dirFilter(dir, opts))) {
|
|
54
55
|
if (g.composerLock) {
|
|
56
|
+
parsedManifests.push(g.composerLock);
|
|
55
57
|
let parsed;
|
|
56
58
|
try { parsed = parseComposerLock(g.composerLock); }
|
|
57
59
|
catch (e) { warnings.push({ type: "parse-error", manifestPath: g.composerLock, message: `composer.lock parse failed: ${e.message}` }); continue; }
|
|
@@ -64,6 +66,7 @@ module.exports = {
|
|
|
64
66
|
}
|
|
65
67
|
} else if (g.composerJson) {
|
|
66
68
|
// No lockfile → best-effort: pinned exact versions only + warning.
|
|
69
|
+
parsedManifests.push(g.composerJson);
|
|
67
70
|
let parsed;
|
|
68
71
|
try { parsed = parseComposerJson(g.composerJson); }
|
|
69
72
|
catch (e) { warnings.push({ type: "parse-error", manifestPath: g.composerJson, message: `composer.json parse failed: ${e.message}` }); continue; }
|
|
@@ -83,7 +86,7 @@ module.exports = {
|
|
|
83
86
|
warnings.push({ type: "no-lockfile", manifestPath: g.composerJson, message: `composer.json without composer.lock — best-effort: ${pinned} pinned, ${ranges} range(s) skipped (run "composer install")` });
|
|
84
87
|
}
|
|
85
88
|
}
|
|
86
|
-
return { deps: out, warnings };
|
|
89
|
+
return { deps: out, warnings, parsedManifests };
|
|
87
90
|
},
|
|
88
91
|
|
|
89
92
|
coordKey(d) { return coordKeyFor("composer", d.namespace || "", d.name); },
|
package/lib/codecs/go.codec.js
CHANGED
|
@@ -44,6 +44,7 @@ module.exports = {
|
|
|
44
44
|
const { deps2Exclude } = opts;
|
|
45
45
|
const out = new Map();
|
|
46
46
|
const warnings = [];
|
|
47
|
+
const parsedManifests = [];
|
|
47
48
|
const add = (d, manifestPath) => {
|
|
48
49
|
if (deps2Exclude && deps2Exclude.test(d.name)) return;
|
|
49
50
|
out.set(coordKeyFor("go", "", d.name), makeDepRecord({ ecosystem: "go", namespace: "", name: d.name, version: d.version, manifestPath, scope: d.scope, isDev: false }));
|
|
@@ -51,6 +52,7 @@ module.exports = {
|
|
|
51
52
|
for (const g of findGoDirs(dir, dirFilter(dir, opts))) {
|
|
52
53
|
if (g.names.has("go.mod")) {
|
|
53
54
|
const fp = path.join(g.dir, "go.mod");
|
|
55
|
+
parsedManifests.push(fp);
|
|
54
56
|
try {
|
|
55
57
|
const r = G.parseGoModFile(fp);
|
|
56
58
|
if (r.deps.length) { for (const d of r.deps) add(d, fp); continue; }
|
|
@@ -59,11 +61,12 @@ module.exports = {
|
|
|
59
61
|
}
|
|
60
62
|
if (g.names.has("go.sum")) {
|
|
61
63
|
const fp = path.join(g.dir, "go.sum");
|
|
64
|
+
parsedManifests.push(fp);
|
|
62
65
|
try { const r = G.parseGoSumFile(fp); for (const d of r.deps) add(d, fp); }
|
|
63
66
|
catch (e) { warnings.push({ type: "parse-error", manifestPath: fp, message: `go.sum parse failed: ${e.message}` }); }
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
|
-
return { deps: out, warnings };
|
|
69
|
+
return { deps: out, warnings, parsedManifests };
|
|
67
70
|
},
|
|
68
71
|
|
|
69
72
|
coordKey(d) { return coordKeyFor("go", "", d.name); },
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/codecs/gradle/catalog.js — Gradle version catalog (gradle/libs.versions.toml).
|
|
3
|
+
*
|
|
4
|
+
* Parses the TOML [versions]/[libraries] tables into a normalized shape and resolves
|
|
5
|
+
* the Kotlin/Groovy DSL type-safe accessors (`libs.foo.bar` → alias `foo-bar`) plus the
|
|
6
|
+
* `findVersion(ref)`/`findLibrary(alias)` forms used inside convention plugins.
|
|
7
|
+
*
|
|
8
|
+
* A Gradle accessor normalizes `-`, `_` and `.` to the same separator, so `groovy-core`,
|
|
9
|
+
* `groovy.core` and `groovy_core` are the same library. We normalize both the alias keys
|
|
10
|
+
* and the looked-up accessor with `[-_.] → "."` so the reverse lookup is unambiguous.
|
|
11
|
+
*
|
|
12
|
+
* @author: N.BRAUN
|
|
13
|
+
* @email: pp9ping@gmail.com
|
|
14
|
+
*/
|
|
15
|
+
const TOML = require("smol-toml");
|
|
16
|
+
|
|
17
|
+
function normKey(s) { return String(s || "").replace(/[-_.]/g, "."); }
|
|
18
|
+
|
|
19
|
+
// A library entry's version may be an inline string, a { ref = "alias" } pointer into
|
|
20
|
+
// [versions], or absent (BOM-managed). Return the concrete string or null.
|
|
21
|
+
function resolveLibVersion(versionField, versions) {
|
|
22
|
+
if (versionField == null) return null;
|
|
23
|
+
if (typeof versionField === "string") return versionField || null;
|
|
24
|
+
if (typeof versionField === "object") {
|
|
25
|
+
if (typeof versionField.ref === "string") return versions[versionField.ref] || null;
|
|
26
|
+
// { strictly = "x" } / { require = "x" } / { prefer = "x" } rich version constraints.
|
|
27
|
+
return versionField.strictly || versionField.require || versionField.prefer || null;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse a libs.versions.toml text → { versions: {alias→str}, libraries: {alias→{group,name,version}},
|
|
34
|
+
* plugins: {alias→{id,version}}, _byAccessor: {normalizedAlias→libraryEntry} }.
|
|
35
|
+
*/
|
|
36
|
+
function parseVersionCatalog(text) {
|
|
37
|
+
let data = {};
|
|
38
|
+
try { data = TOML.parse(String(text || "")); } catch { data = {}; }
|
|
39
|
+
const versions = {};
|
|
40
|
+
for (const [k, v] of Object.entries(data.versions || {})) {
|
|
41
|
+
versions[k] = typeof v === "string" ? v : resolveLibVersion(v, {});
|
|
42
|
+
}
|
|
43
|
+
const libraries = {};
|
|
44
|
+
for (const [alias, raw] of Object.entries(data.libraries || {})) {
|
|
45
|
+
let group = null, name = null;
|
|
46
|
+
if (typeof raw === "string") {
|
|
47
|
+
// shorthand "group:name:version"
|
|
48
|
+
const parts = raw.split(":");
|
|
49
|
+
group = parts[0] || null; name = parts[1] || null;
|
|
50
|
+
libraries[alias] = { group, name, version: parts[2] || null };
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (raw && typeof raw === "object") {
|
|
54
|
+
if (typeof raw.module === "string") {
|
|
55
|
+
const i = raw.module.indexOf(":");
|
|
56
|
+
group = i >= 0 ? raw.module.slice(0, i) : raw.module;
|
|
57
|
+
name = i >= 0 ? raw.module.slice(i + 1) : null;
|
|
58
|
+
} else {
|
|
59
|
+
group = raw.group || null;
|
|
60
|
+
name = raw.name || null;
|
|
61
|
+
}
|
|
62
|
+
libraries[alias] = { group, name, version: resolveLibVersion(raw.version, versions) };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const plugins = {};
|
|
66
|
+
for (const [alias, raw] of Object.entries(data.plugins || {})) {
|
|
67
|
+
if (typeof raw === "string") { const [id, version] = raw.split(":"); plugins[alias] = { id: id || null, version: version || null }; continue; }
|
|
68
|
+
if (raw && typeof raw === "object") plugins[alias] = { id: raw.id || null, version: resolveLibVersion(raw.version, versions) };
|
|
69
|
+
}
|
|
70
|
+
const byAccessor = {};
|
|
71
|
+
for (const [alias, entry] of Object.entries(libraries)) byAccessor[normKey(alias)] = entry;
|
|
72
|
+
return { versions, libraries, plugins, _byAccessor: byAccessor };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Resolve a DSL accessor (the part after `libs.`, e.g. "clamav.client") → library entry or null. */
|
|
76
|
+
function resolveLibraryAccessor(catalog, accessor) {
|
|
77
|
+
if (!catalog) return null;
|
|
78
|
+
return catalog._byAccessor[normKey(accessor)] || null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Resolve a version alias (libs.findVersion("spring-boot")) → string or null. */
|
|
82
|
+
function findCatalogVersion(catalog, ref) {
|
|
83
|
+
if (!catalog || !ref) return null;
|
|
84
|
+
if (catalog.versions[ref] != null) return catalog.versions[ref];
|
|
85
|
+
// tolerate accessor-style refs
|
|
86
|
+
const want = normKey(ref);
|
|
87
|
+
for (const [k, v] of Object.entries(catalog.versions)) if (normKey(k) === want) return v;
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { parseVersionCatalog, resolveLibraryAccessor, findCatalogVersion, normKey };
|