fad-checker 2.2.0 → 2.2.2
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 +32 -117
- package/completions/fad-checker.bash +1 -1
- package/completions/fad-checker.zsh +1 -0
- package/fad-checker.js +122 -16
- package/lib/codecs/npm.codec.js +2 -2
- package/lib/cve-match.js +10 -5
- package/lib/cve-report.js +65 -20
- package/lib/embedded.js +57 -0
- package/lib/json-export.js +7 -1
- package/lib/manifest-copy.js +75 -0
- package/lib/maven-bom.js +104 -0
- package/lib/outdated.js +19 -7
- package/lib/retire.js +95 -10
- package/lib/scan-completeness.js +15 -6
- package/lib/transitive.js +9 -2
- package/lib/ui.js +4 -3
- package/lib/version-overlay.js +193 -0
- package/package.json +1 -1
- package/fad-checker-report/cve-report.doc +0 -1168
- package/fad-checker-report/cve-report.html +0 -1293
package/README.md
CHANGED
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
`fad-checker` scans **Maven**, **npm**, **Yarn**, **Composer (PHP)**, **PyPI (Python)**, **NuGet (C#/.NET)**, **Go**, **Ruby**, **vendored JavaScript** and **committed native binaries** in any source tree — multi-module, monorepo, polyglot, whatever you've got — and produces a single self-contained HTML report with CVE (prioritised by **EPSS + CISA KEV**), EOL, obsolete, outdated and **license** findings, plus per-ecosystem fix recipes. It also exports a **CycloneDX 1.6 SBOM** and a **CSAF 2.0 VEX**.
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
🌐 **[Project site & docs →](https://n8tz.github.io/fad-checker/)**
|
|
13
14
|
|
|
14
|
-
<p align="center"><img src="docs/assets/cli.png" alt="fad-checker terminal output — a [n/N] checklist warming each vulnerability database, then CVE findings coloured by severity"
|
|
15
|
+
<p align="center"><img src="docs/assets/cli.png" alt="fad-checker terminal output — a [n/N] checklist warming each vulnerability database, then CVE findings coloured by severity" height="760"></p>
|
|
15
16
|
|
|
16
17
|
It runs against the source files alone. **No `mvn`, no `npm install`, no `composer install`, no `pip`, no `dotnet restore`, no Docker.** It reads `pom.xml`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `composer.lock`, `poetry.lock`/`Pipfile.lock`/`uv.lock`/`pdm.lock`/`pyproject.toml`/`requirements.txt`, and `packages.lock.json`/`*.csproj`/`*.fsproj`/`*.vbproj`/`packages.config` directly.
|
|
17
18
|
|
|
@@ -55,7 +56,7 @@ Exactly **one** runtime dependencies must be on PATH (or installed automatically
|
|
|
55
56
|
| **4. EOL frameworks** | endoflife.date | Spring Boot 2.5, Hibernate 4.x, EOL JDKs, AngularJS, Laravel/Symfony, Django, .NET, etc. |
|
|
56
57
|
| **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` |
|
|
57
58
|
| **6. Outdated libraries** | Maven Central + npm / Packagist / PyPI / NuGet registries | Available newer versions, with release dates |
|
|
58
|
-
| **7. 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 |
|
|
59
|
+
| **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 |
|
|
59
60
|
| **8. Fix Recommendations** | computed | Per-ecosystem pin recipes: Maven `<dependencyManagement>`, npm `overrides`, yarn `resolutions`, `composer require`, `pip install`, `dotnet add package` |
|
|
60
61
|
|
|
61
62
|
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.
|
|
@@ -104,6 +105,9 @@ fad-checker -s ./proj --no-all-libs --no-transitive
|
|
|
104
105
|
# Fully offline (uses cached data only)
|
|
105
106
|
fad-checker -s ./proj --offline
|
|
106
107
|
|
|
108
|
+
# License + copyleft-policy chapter (off by default)
|
|
109
|
+
fad-checker -s ./proj --licenses
|
|
110
|
+
|
|
107
111
|
# Pick ecosystems — --ecosystem is a list: auto (default) | all | comma list
|
|
108
112
|
fad-checker -s ./proj --ecosystem maven # Maven only
|
|
109
113
|
fad-checker -s ./proj --ecosystem maven,npm,pypi # several
|
|
@@ -127,36 +131,22 @@ Run `fad-checker --help` for the full flag list.
|
|
|
127
131
|
The console prints a summary; the full detail lives in the self-contained HTML/`.doc`:
|
|
128
132
|
|
|
129
133
|
```
|
|
134
|
+
╭───────────────────────────────────────────────────╮
|
|
135
|
+
│ fad-checker v2.2.1 · Autonomous Dependency Checker │
|
|
136
|
+
╰───────────────────────────────────────────────────╯
|
|
130
137
|
Executive Summary [CRITICAL] — 1708 dependencies scanned
|
|
131
138
|
• 81 CVE in production deps (critical=5, high=53, medium=12, low=11)
|
|
132
|
-
• 32 CVE in dev/test deps
|
|
133
|
-
•
|
|
134
|
-
• 2 end-of-life frameworks
|
|
135
|
-
• 13 obsolete / deprecated libs
|
|
136
|
-
• 172 outdated libs
|
|
139
|
+
• 32 CVE in dev/test deps · 17 vulnerable vendored JS (retire.js)
|
|
140
|
+
• 2 EOL frameworks · 13 obsolete/deprecated · 172 outdated
|
|
137
141
|
• 4 scan-completeness alerts — see chapter 0
|
|
138
|
-
|
|
139
|
-
0. Warnings & scan-completeness (4)
|
|
140
|
-
1. CVE Vulnerabilities — production (81)
|
|
141
|
-
1.a Maven (49)
|
|
142
|
-
1.a.0 All (49)
|
|
143
|
-
By pom.xml (14 files)
|
|
144
|
-
build/building/pom.xml (17)
|
|
145
|
-
services/api/pom.xml (17)
|
|
146
|
-
… 12 more
|
|
147
|
-
1.b npm (package-lock) (32)
|
|
148
|
-
1.b.0 All (32)
|
|
149
|
-
By package-lock.json (1 file)
|
|
150
|
-
web/package-lock.json (32)
|
|
151
|
-
2. CVE in dev dependencies (32)
|
|
152
|
-
3. Vendored JS scan — retire.js (17)
|
|
153
|
-
4. End-of-Life Frameworks (2)
|
|
154
|
-
5. Obsolete / Deprecated Libraries (13)
|
|
155
|
-
6. Outdated Libraries (172)
|
|
156
|
-
7. Fix Recommendations
|
|
157
142
|
```
|
|
158
143
|
|
|
159
|
-
|
|
144
|
+
The HTML report organises this into chapters (0 Warnings, 1 CVE prod, 1B embedded JARs,
|
|
145
|
+
1C native binaries, 1D vendored-JS inventory, 2 vendored-JS vulns, 3 dev CVE, 4 EOL,
|
|
146
|
+
5 obsolete, 6 outdated, 7 Licenses *(opt-in)*, 8 Fix Recommendations), each grouped by
|
|
147
|
+
ecosystem and manifest. Every CVE row carries: severity badge · CVE/GHSA id · coord &
|
|
148
|
+
version · declaring manifest · source(s) · fix-version · priority (KEV/EPSS) — click for
|
|
149
|
+
the full panel (CVSS vectors, references, transitive paths, CPE configs).
|
|
160
150
|
|
|
161
151
|
---
|
|
162
152
|
|
|
@@ -209,54 +199,17 @@ fad-checker --completion zsh > ~/.zsh/completions/_fad-checker
|
|
|
209
199
|
| Ruby | `Gemfile.lock` (`specs:`) | the lockfile |
|
|
210
200
|
| Vendored JS / binaries | the committed `.js` / `.jar` / `.so` files themselves | n/a (read in place) |
|
|
211
201
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
- **npm / Yarn / pnpm** — `package-lock.json` (v1, v2, v3), `yarn.lock` (v1 + Berry/v2+, via `js-yaml`) and `pnpm-lock.yaml` (v5/v6/v9, via `js-yaml`) are parsed directly. Lockfiles already contain every transitive version. No `node_modules/` traversal, no `npm install`.
|
|
216
|
-
- **Composer (PHP)** — `composer.lock` (`packages` + `packages-dev`) gives concrete + transitive versions; `composer.json` alone is best-effort.
|
|
217
|
-
- **PyPI (Python)** — `poetry.lock` / `Pipfile.lock` / `uv.lock` / `pdm.lock` are parsed (TOML via `smol-toml`, or JSON); `pyproject.toml` (PEP 621 `[project]` + `[tool.poetry]`) and `requirements.txt` (following `-r`/`-c` includes recursively, with `-c` constraint pins applied to ranges) are best-effort on exact pins. Package names are PEP 503-normalised (`Flask-SQLAlchemy` → `flask-sqlalchemy`).
|
|
218
|
-
- **NuGet (C#/.NET)** — `packages.lock.json` is authoritative; otherwise `*.csproj` / `*.fsproj` / `*.vbproj` `<PackageReference>` (resolving Central Package Management against `Directory.Packages.props`) and legacy `packages.config`. Ids are case-insensitive.
|
|
219
|
-
- **Go** — `go.mod` `require` entries are the selected versions (full pruned graph on Go ≥1.17; `// indirect` → transitive), `go.sum` as fallback. OSV "Go" ecosystem for recall, the Go module proxy for outdated.
|
|
220
|
-
- **Ruby** — `Gemfile.lock` `specs:` give the resolved gem set. OSV "RubyGems" for recall, the RubyGems API for outdated + licenses.
|
|
221
|
-
- **Lockfile-first, best-effort fallback** — when a lockfile is present it wins. When it's absent, the loose manifest (`package.json` / `composer.json` / `pyproject.toml` / `requirements.txt` / `*.csproj`) is still parsed for its **pinned exact versions**, with ranges skipped and a `no-lockfile` warning in chapter 0 flagging the partial coverage.
|
|
222
|
-
- **Vendored JavaScript** — `retire.js` shells out (with `--verbose`) and scans `.js` / `.min.js` files by signature. Two outputs: a full **inventory of every identified standalone lib** — jQuery, Bootstrap, Angular, PDF.js copies that no lockfile knows about, *vulnerable or not* (chapter 1D, a cyber-hygiene constat on unmanaged third-party code; `--no-vendored-js-inventory` to skip) — and the **vulnerable subset** with its CVEs/advisories (chapter 2).
|
|
223
|
-
- **Embedded JARs** — committed `.jar` / `.war` / `.ear` archives are unzipped **in memory** (via `fflate` — nested fat-jar libs are recursed without ever touching disk, so there's no zip-slip risk) and each artifact's Maven coordinate is read from `META-INF/maven/.../pom.properties` (authoritative), then `MANIFEST.MF`, then the file name. Those coordinates run through the same CVE/OSV/NVD matching as declared deps but report in their own **Embedded binaries** chapter, grouped by containing archive. An archive whose coordinate can't be resolved is flagged in chapter 0 rather than scanned blindly. Auto when archives are present; disable with `--no-jars`. (Embedded coords don't trigger Maven Central transitive resolution — a fat-jar already ships its dependencies, which the recursion finds directly.)
|
|
224
|
-
- **Committed native binaries** — `.dll` / `.exe` / `.so` / `.dylib` files are found by a magic-byte-confirmed walk (extension **and** PE/ELF/Mach-O signature must agree, so an image renamed `.so` or any asset is rejected), hashed (SHA-1 + SHA-256), then **identified by checksum** online: **deps.dev** query-by-hash returns the exact package coordinate (so the file is byte-identical to a published artifact → *pristine*, and ought to be a declared dependency), and **CIRCL hashlookup** recognises known OS/distro/CDN/NSRL files (→ *known-good*) plus a free `KnownMalicious` flag. Files no source knows are flagged *unknown*; a filename that disagrees with the resolved identity is flagged *name≠checksum*. Reported in their own **Unmanaged / vendored binaries** chapter (1C) and the JSON export. Cached + `--offline`-aware; disable with `--no-binaries`. No malware/AV lane and no binary-metadata parsing — identity is hash-lookup, integrity is hash-comparison.
|
|
225
|
-
- **CVE data** — three independent sources merged:
|
|
226
|
-
- **CVEProject** (the canonical `cvelistV5` bundle, filtered to Maven-relevant entries)
|
|
227
|
-
- **OSV.dev** (Google + GitHub Security Lab, multi-ecosystem)
|
|
228
|
-
- **NVD** (official NIST records, used for enrichment: full CVSS, references, CPE configurations)
|
|
229
|
-
- **CPE refinement** — once a CVE is matched, its NVD CPE configurations are checked against the dep version range. A match outside the vulnerable range is flagged `cpeFiltered: true` (likely false positive). A curated `data/cpe-coord-map.json` maps CPE `vendor:product` to Maven `g:a` (60+ entries seeded: log4j, jackson, spring, tomcat, jetty, netty, …).
|
|
230
|
-
- **Prioritization** — each matched CVE is enriched with **EPSS** (FIRST.org exploit-prediction percentile) and **CISA KEV** (known-exploited catalogue), then scored: KEV (exploited in the wild) outranks EPSS-weighted CVSS. The report sorts by this composite priority and badges KEV/EPSS.
|
|
231
|
-
- **Licenses** — each dependency's license is resolved (registry metadata, no extra request; Maven from cached POMs), normalised to SPDX and classified against a copyleft policy (`data/license-policy.json`) — permissive / weak / strong / network copyleft / proprietary / unknown.
|
|
232
|
-
- **Unified outputs** — one `--report-<type>` flag per output, each with an OPTIONAL path (omit it → a default name under `--report-output`): `--report-html`, `--report-doc`, plus the machine-readable `--report-sbom` (**CycloneDX 1.6**, vulnerabilities inline / VDR), `--report-csaf` (**CSAF 2.0 VEX**), `--report-json` (flat findings, diff-friendly) and `--report-sarif` (**SARIF 2.1.0** for GitHub/GitLab code scanning). With no `--report-*` flag, HTML + `.doc` are written by default; `--no-report` writes nothing (gate-only). purls per ecosystem.
|
|
233
|
-
- **CI gating & triage** — `--fail-on <low|medium|high|critical|kev>` sets a non-zero exit code (`kev` = fail only on a CISA-known-exploited finding). `--ignore <file>` (CVE/coord/glob rules) and `--vex <file>` (ingest a CSAF VEX) suppress accepted-risk / false-positive findings from the report and the gate, while keeping them flagged in the exports — so re-audits stay signal-rich.
|
|
202
|
+
Highlights of the matching layer: **three CVE sources merged** (CVEProject + OSV.dev + NVD), **CPE/version cross-check** to drop false positives, **EPSS + CISA KEV** prioritisation, lockfile-first with a **best-effort pinned-version fallback** when no lockfile, in-memory **embedded-JAR** unzip (no disk, no zip-slip), and **checksum identity** for native binaries. Outputs: HTML + `.doc` by default, plus opt-in `--report-sbom`/`-csaf`/`-json`/`-sarif`; CI gating via `--fail-on` with `--ignore`/`--vex` triage.
|
|
203
|
+
|
|
204
|
+
> Full per-ecosystem and per-stage detail lives in **[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)** and **[`docs/USAGE.md`](docs/USAGE.md)**.
|
|
234
205
|
|
|
235
206
|
---
|
|
236
207
|
|
|
237
208
|
## Caching
|
|
238
209
|
|
|
239
|
-
All cached data lives in `~/.fad-checker
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
| --- | --- | --- |
|
|
243
|
-
| Maven CVE index (CVEProject bundle, filtered) | `cve-data/maven-cve-index.json` | 24 h |
|
|
244
|
-
| OSV per-dep lookups | `osv-cache/<ecosystem>__<g>__<a>__<v>.json` | 12 h |
|
|
245
|
-
| OSV vuln details | `osv-cache/vuln_<id>.json` | 12 h |
|
|
246
|
-
| NVD CVE records | `nvd-cache/<cveId>.json` | 7 d |
|
|
247
|
-
| EPSS scores (FIRST.org) | `epss-cache.json` | 24 h |
|
|
248
|
-
| CISA KEV catalogue | `kev-cache.json` | 24 h |
|
|
249
|
-
| Go module proxy (latest) | `go-proxy-cache.json` | 24 h |
|
|
250
|
-
| RubyGems (latest + licenses) | `rubygems-cache.json` | 24 h |
|
|
251
|
-
| Binary identity (deps.dev + CIRCL by hash) | `hash-id-cache.json` | 24 h |
|
|
252
|
-
| endoflife.date cycles | `eol-cache.json` | 7 d |
|
|
253
|
-
| Maven Central latest versions | `version-cache.json` | 24 h |
|
|
254
|
-
| Transitive POMs from Maven Central | `poms-cache/<g>__<a>__<v>.pom` | ∞ (immutable) |
|
|
255
|
-
| retire.js findings | `retire-cache/<md5(src)>.json` | 24 h |
|
|
256
|
-
| retire.js signature DB | `retire-signatures/jsrepository-v5.json` | warmed online, used offline |
|
|
257
|
-
| User config (NVD key) | `config.json` (mode 0600) | — |
|
|
258
|
-
|
|
259
|
-
Export the lot to share between machines:
|
|
210
|
+
All cached data lives in `~/.fad-checker/` — every source is hit at most once per its
|
|
211
|
+
TTL (12 h–7 d, transitive POMs forever as they're immutable). Full per-cache TTL table
|
|
212
|
+
in [`docs/USAGE.md`](docs/USAGE.md). Export the lot to share between machines:
|
|
260
213
|
|
|
261
214
|
```bash
|
|
262
215
|
fad-checker --export-cache fad-cache.tar.gz
|
|
@@ -317,66 +270,28 @@ DB is warmed online (phase 2) and carried by `--export-cache`.
|
|
|
317
270
|
|
|
318
271
|
## Custom repositories & registries
|
|
319
272
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
Custom registries for **`maven`, `npm`, `pypi`, `ruby`, `go`**:
|
|
273
|
+
Register private **Nexus / Artifactory / Verdaccio / devpi / GOPROXY** registries (for **`maven`, `npm`, `pypi`, `ruby`, `go`**) so transitive resolution, outdated/deprecation and license lookups reach them:
|
|
323
274
|
|
|
324
275
|
```bash
|
|
325
|
-
|
|
326
|
-
fad-checker --add-repo
|
|
327
|
-
fad-checker --
|
|
328
|
-
fad-checker --add-repo pypi devpi https://pypi.acme.com/root/pypi/+simple/ --auth alice:s3cr3t
|
|
329
|
-
fad-checker --list-repos # grouped by ecosystem
|
|
330
|
-
fad-checker --remove-repo npm verdaccio
|
|
331
|
-
|
|
332
|
-
# One-off (not persisted) — repeatable, always ecosystem-scoped as <eco>=<url>
|
|
333
|
-
fad-checker -s ./proj --repo npm=https://npm.acme.com/ --repo maven=https://nexus.acme.com/repository/maven-public/
|
|
334
|
-
# Inline auth in the URL also works:
|
|
335
|
-
fad-checker -s ./proj --repo maven=https://alice:s3cr3t@nexus.acme.com/repository/maven-public/
|
|
276
|
+
fad-checker --add-repo maven nexus https://nexus.acme.com/repository/maven-public/ --auth alice:s3cr3t
|
|
277
|
+
fad-checker --add-repo npm verdaccio https://npm.acme.com/ --token "$NPM_TOKEN"
|
|
278
|
+
fad-checker -s ./proj --repo npm=https://npm.acme.com/ # one-off, repeatable
|
|
336
279
|
```
|
|
337
280
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
> **PyPI / Ruby caveat:** the custom base must expose the **same JSON API** as the public one (`<base>/<pkg>/json`, `<base>/<gem>.json`). A pure PEP 503 *simple-index* mirror that only lists files won't yield latest/yanked/license metadata — point at the JSON-capable endpoint (Artifactory/devpi/Nexus all have one). **NuGet** and **Composer** private feeds aren't supported yet (their service-index / `packages.json` discovery is a separate follow-up).
|
|
281
|
+
Tried in declared order, public registry last; auth via `--auth user:pass` / `--token TOK` / inline URL; responses cached per coordinate. NuGet/Composer private feeds aren't supported yet. **Full details, the PyPI/Ruby JSON-API caveat and `--list-repos`/`--remove-repo` → [`docs/USAGE.md`](docs/USAGE.md).**
|
|
341
282
|
|
|
342
283
|
---
|
|
343
284
|
|
|
344
285
|
## Configuration file & environment
|
|
345
286
|
|
|
346
|
-
Don't retype flags every run. `fad-checker`
|
|
347
|
-
|
|
348
|
-
| Layer | Where | Format |
|
|
349
|
-
| --- | --- | --- |
|
|
350
|
-
| **CLI flags** | the command line | flags (always win) |
|
|
351
|
-
| **Config file** | `--config <file.json>`, else `./.fad-env.json` | **JSON** object, keys = option names |
|
|
352
|
-
| **`FAD_CHECKER_ENV`** | environment variable | a **string of CLI flags** (what you'd type) |
|
|
353
|
-
| **Global config** | `~/.fad-checker/config.json` | persisted NVD key + `registries` |
|
|
354
|
-
|
|
355
|
-
Effective precedence: **CLI flag > config file > `FAD_CHECKER_ENV` > global config > built-in defaults.** A file/env value only fills an option you did *not* pass on the CLI; **registries are merged (unioned) across every layer**, never overridden.
|
|
356
|
-
|
|
357
|
-
```jsonc
|
|
358
|
-
// ./.fad-env.json — JSON, keys mirror the CLI options (camelCase)
|
|
359
|
-
{
|
|
360
|
-
"source": "./my-project", // alias of --src (so is "src")
|
|
361
|
-
"exclude": "^(com\\.acme|client)\\.",
|
|
362
|
-
"excludePath": ["packages/legacy/**", "**/fixtures/**"],
|
|
363
|
-
"failOn": "high",
|
|
364
|
-
"noNuget": true,
|
|
365
|
-
"registries": {
|
|
366
|
-
"npm": [{ "name": "verdaccio", "url": "https://npm.acme.com/", "token": "…" }],
|
|
367
|
-
"maven":[{ "name": "nexus", "url": "https://nexus.acme.com/repository/maven-public/", "auth": "user:pass" }]
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
```
|
|
287
|
+
Don't retype flags every run. Precedence: **CLI flag > `--config <file.json>` / `./.fad-env.json` (JSON, keys = camelCase options) > `FAD_CHECKER_ENV` (a string of CLI flags) > `~/.fad-checker/config.json` > defaults.** A file/env value only fills an option you didn't pass; **registries are merged across every layer**.
|
|
371
288
|
|
|
372
289
|
```bash
|
|
373
|
-
fad-checker --config ./ci/fad-env.json
|
|
374
|
-
FAD_CHECKER_ENV='--fail-on high --no-nuget
|
|
290
|
+
fad-checker --config ./ci/fad-env.json
|
|
291
|
+
FAD_CHECKER_ENV='--fail-on high --no-nuget' fad-checker -s ./proj
|
|
375
292
|
```
|
|
376
293
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
> **`-e/--exclude` vs `--exclude-path`:** `--exclude` is a regex on the **coordinate** (groupId/name) — it drops matching *dependencies*. `--exclude-path` is a gitignore-style glob on the **directory path** (relative to `--src`) — it prunes the *walk* so nothing under it is read. They compose. `--exclude-path` is repeatable, unioned across config layers, and `--no-default-excludes` lets you walk the normally-pruned dirs (`node_modules`, `vendor`, `target`, `.git`, …).
|
|
294
|
+
`-s` / `--src` / `--source` are aliases. **`-e/--exclude`** is a regex on the **coordinate** (drops deps); **`--exclude-path`** is a gitignore-style glob on the **path** (prunes the walk). Full `.fad-env.json` schema → [`docs/USAGE.md`](docs/USAGE.md).
|
|
380
295
|
|
|
381
296
|
---
|
|
382
297
|
|
|
@@ -6,7 +6,7 @@ _fad_check_complete() {
|
|
|
6
6
|
COMPREPLY=()
|
|
7
7
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
8
8
|
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
|
9
|
-
opts="--src --target --exclude --verbose --no-report --no-transitive --no-all-libs --no-osv --no-nvd --no-epss --no-kev --
|
|
9
|
+
opts="--src --target --exclude --verbose --no-report --no-transitive --no-all-libs --no-osv --no-nvd --no-epss --no-kev --licenses --no-retire --retire-refresh --report-output --report-html --report-doc --report-sbom --report-csaf --report-json --report-sarif --fail-on --ignore --vex --ignore-test --cve-refresh --cve-offline --snyk --transitive-depth --offline --ecosystem --no-maven --no-npm --no-yarn --no-nuget --no-composer --no-pypi --no-go --no-ruby --no-jars --no-js --set-nvd-key --show-config --completion --help --version -s -t -e -v"
|
|
10
10
|
case "$prev" in
|
|
11
11
|
--src|-s|--target|-t|--report-output)
|
|
12
12
|
COMPREPLY=( $(compgen -d -- "$cur") )
|
|
@@ -42,6 +42,7 @@ _fad_check() {
|
|
|
42
42
|
'--no-pypi[skip the PyPI codec]'
|
|
43
43
|
'--no-js[alias: skip JS/npm/yarn]'
|
|
44
44
|
'--no-retire[skip retire.js vendored-JS scan]'
|
|
45
|
+
'--licenses[run license detection + copyleft policy (off by default)]'
|
|
45
46
|
'--completion[print shell completion]:shell:(bash zsh)'
|
|
46
47
|
)
|
|
47
48
|
_arguments $opts
|
package/fad-checker.js
CHANGED
|
@@ -25,6 +25,18 @@ const core = require("./lib/core");
|
|
|
25
25
|
// (from $bunfs/root) and crashes with ENOENT. Keeps the bun builds fully standalone.
|
|
26
26
|
const pkg = require("./package.json");
|
|
27
27
|
|
|
28
|
+
// -------- compiled-binary retire mode --------
|
|
29
|
+
// The bun-compiled single binary has no node_modules to spawn the retire CLI from,
|
|
30
|
+
// and an air-gapped box has no `retire` on PATH. So lib/retire.js re-execs THIS
|
|
31
|
+
// binary with __FAD_RETIRE__ set; here we hand off to the statically-bundled retire
|
|
32
|
+
// CLI (it self-runs, reading process.argv — bun's argv mirrors node's). The entire
|
|
33
|
+
// normal CLI body is gated behind `else` so fad's own commander setup never runs in
|
|
34
|
+
// retire mode (retire shares commander's singleton `program`). Lets vendored-JS
|
|
35
|
+
// scanning work fully offline from the one binary, no external retire needed.
|
|
36
|
+
if (process.env.__FAD_RETIRE__) {
|
|
37
|
+
require("retire/lib/cli.js");
|
|
38
|
+
} else {
|
|
39
|
+
|
|
28
40
|
// -------- bash/zsh completion shortcut (must run before required-options parse) --------
|
|
29
41
|
if (process.argv.includes("--completion")) {
|
|
30
42
|
const shellIdx = process.argv.indexOf("--completion") + 1;
|
|
@@ -210,7 +222,7 @@ program
|
|
|
210
222
|
.option("--fail-on <level>", "exit non-zero if a production finding meets <level>: low|medium|high|critical|kev|none", "none")
|
|
211
223
|
.option("--ignore <file>", "suppress findings listed in <file> (CVE ids / coords / globs, one per line)")
|
|
212
224
|
.option("--vex <file>", "ingest a CSAF VEX: suppress CVEs marked not_affected/fixed")
|
|
213
|
-
.option("--
|
|
225
|
+
.option("--licenses", "run license detection + copyleft policy check (off by default)")
|
|
214
226
|
.option("--offline", "no network: use cached CVE/OSV/NVD/EPSS/KEV/POM data only")
|
|
215
227
|
.option("--set-nvd-key <key>", "save NVD API key to ~/.fad-checker/config.json (10× faster NVD enrichment)")
|
|
216
228
|
.option("--show-config", "print the persisted ~/.fad-checker/config.json")
|
|
@@ -248,6 +260,10 @@ program
|
|
|
248
260
|
.option("--auth <user:pass>", "Basic auth for --add-repo")
|
|
249
261
|
.option("--token <token>", "Bearer token for --add-repo")
|
|
250
262
|
.option("--completion <shell>", "print shell completion script (bash|zsh)");
|
|
263
|
+
// Back-compat: license detection is now OFF by default (enable with --licenses).
|
|
264
|
+
// A legacy `--no-licenses` is therefore a no-op — drop it so old invocations don't
|
|
265
|
+
// trip commander's unknown-option error.
|
|
266
|
+
if (process.argv.includes("--no-licenses")) process.argv = process.argv.filter(a => a !== "--no-licenses");
|
|
251
267
|
program.parse(process.argv);
|
|
252
268
|
|
|
253
269
|
const options = program.opts();
|
|
@@ -396,7 +412,7 @@ async function timedPhase(label, fn) {
|
|
|
396
412
|
}
|
|
397
413
|
|
|
398
414
|
(async function main() {
|
|
399
|
-
ui.banner();
|
|
415
|
+
ui.banner(pkg.version);
|
|
400
416
|
|
|
401
417
|
// Build the Maven repo list once: persisted repos (from ~/.fad-checker/config.json)
|
|
402
418
|
// + ad-hoc --repo URLs + Maven Central as final fallback. Used by transitive
|
|
@@ -432,7 +448,12 @@ async function timedPhase(label, fn) {
|
|
|
432
448
|
].filter(Boolean))];
|
|
433
449
|
const defaultExcludes = options.defaultExcludes !== false;
|
|
434
450
|
const walkOpts = { excludePath, defaultExcludes };
|
|
435
|
-
|
|
451
|
+
// PASSI phase 1 (--export-anonymized) is a purely local operation — parse the tree,
|
|
452
|
+
// emit public coordinates, exit. Force offline so no source ever touches the network.
|
|
453
|
+
if (options.exportAnonymized) options.offline = true;
|
|
454
|
+
const runMode = options.exportAnonymized ? "offline (PASSI phase 1 · export descriptor)"
|
|
455
|
+
: options.importAnonymized ? "import descriptor"
|
|
456
|
+
: (options.offline ? "offline" : "online");
|
|
436
457
|
if (options.src) ui.kv("source", chalk.white(options.src));
|
|
437
458
|
if (mavenRepos.length > 1) ui.kv("repos", chalk.white(mavenRepos.map(r => r.name).join(chalk.dim(" → "))));
|
|
438
459
|
const otherRegs = Object.keys(regMap).filter(e => e !== "maven" && regMap[e].length);
|
|
@@ -544,6 +565,17 @@ async function timedPhase(label, fn) {
|
|
|
544
565
|
ui.ok(`${chalk.bold(descriptor.summary.total)} dep(s) (${ecoSummary || "none"}) → ${chalk.white(options.exportAnonymized)}`);
|
|
545
566
|
ui.info(chalk.dim("public coordinates only — no paths/URLs/host info. Review before transfer."));
|
|
546
567
|
if (!descriptor.summary.total) ui.warn("no dependencies collected — descriptor is empty");
|
|
568
|
+
// Guide the operator through the remaining two phases (the descriptor file +
|
|
569
|
+
// this run's --src are filled in so the commands are copy-paste ready).
|
|
570
|
+
const descFile = options.exportAnonymized;
|
|
571
|
+
const srcShown = options.src || "./proj";
|
|
572
|
+
ui.section("Next steps");
|
|
573
|
+
ui.info(`${chalk.dim("Phase 2 —")} ${chalk.cyan("ONLINE")} ${chalk.dim("(any box, no --src): warm the caches, then bundle them")}`);
|
|
574
|
+
ui.info(" " + chalk.white(`fad-checker --import-anonymized ${descFile}`));
|
|
575
|
+
ui.info(" " + chalk.white("fad-checker --export-cache fad-cache.tar.gz"));
|
|
576
|
+
ui.info(`${chalk.dim("Phase 3 —")} ${chalk.cyan("OFFLINE")} ${chalk.dim("(back here): import the cache, then run the full report")}`);
|
|
577
|
+
ui.info(" " + chalk.white("fad-checker --import-cache fad-cache.tar.gz"));
|
|
578
|
+
ui.info(" " + chalk.white(`fad-checker -s ${srcShown} --offline`));
|
|
547
579
|
return;
|
|
548
580
|
}
|
|
549
581
|
|
|
@@ -565,6 +597,18 @@ async function timedPhase(label, fn) {
|
|
|
565
597
|
}
|
|
566
598
|
}
|
|
567
599
|
|
|
600
|
+
// Mirror every non-Maven lockfile/manifest (npm/yarn/pnpm, composer, pypi, nuget,
|
|
601
|
+
// go, ruby) into the cleaned tree so `snyk test --all-projects` scans the WHOLE
|
|
602
|
+
// polyglot project, not just the cleaned POMs. Maven POMs are the rewrite above.
|
|
603
|
+
let copiedManifests = 0;
|
|
604
|
+
if (!readOnly) {
|
|
605
|
+
try {
|
|
606
|
+
const { copyEcosystemManifests } = require("./lib/manifest-copy");
|
|
607
|
+
const r = await copyEcosystemManifests(options.src, options.target, { excludePath, defaultExcludes });
|
|
608
|
+
copiedManifests = r.copied;
|
|
609
|
+
} catch (err) { console.error(chalk.red(" ✗ manifest copy failed:"), err.message); }
|
|
610
|
+
}
|
|
611
|
+
|
|
568
612
|
// ---------- Maven POM analysis summary (parents missing / excluded) ----------
|
|
569
613
|
let privateLibIds = [];
|
|
570
614
|
if (runMaven && mavenCtx) {
|
|
@@ -642,11 +686,15 @@ async function timedPhase(label, fn) {
|
|
|
642
686
|
else ui.info(chalk.dim(`${wrotePom} POM(s) cleanable (read-only — pass -t <dir> to write them)`));
|
|
643
687
|
}
|
|
644
688
|
|
|
689
|
+
if (!readOnly && copiedManifests) {
|
|
690
|
+
ui.ok(`${chalk.bold(copiedManifests)} non-Maven lockfile/manifest(s) mirrored → ${chalk.white(options.target)} ${chalk.dim("(so snyk --all-projects scans every ecosystem)")}`);
|
|
691
|
+
}
|
|
692
|
+
|
|
645
693
|
// ---------- Scan flow (CVE / EOL / Obsolete) ----------
|
|
646
694
|
// The scan always runs — it feeds the terminal summary, the file outputs and the
|
|
647
695
|
// CI gate. Which files get written is decided by the --report-* family inside
|
|
648
696
|
// (HTML + .doc by default; --no-report writes nothing).
|
|
649
|
-
await runReportFlow(resolved, { activeIds, runMaven, runNpm, privateLibIds, mavenRepos, regMap, collectWarnings });
|
|
697
|
+
await runReportFlow(resolved, { activeIds, runMaven, runNpm, privateLibIds, mavenRepos, regMap, collectWarnings, mavenPropsByPom: mavenCtx?.propsByPom, mavenStore: mavenCtx?.store });
|
|
650
698
|
if (!readOnly) {
|
|
651
699
|
ui.section("Next step");
|
|
652
700
|
ui.info(`run Snyk on the cleaned tree:`);
|
|
@@ -655,7 +703,7 @@ async function timedPhase(label, fn) {
|
|
|
655
703
|
})();
|
|
656
704
|
|
|
657
705
|
async function runReportFlow(resolved, ecoFlags = {}) {
|
|
658
|
-
const { activeIds = [], runMaven = true, runNpm = false, privateLibIds = [], mavenRepos = [], regMap = {}, collectWarnings = [] } = ecoFlags;
|
|
706
|
+
const { activeIds = [], runMaven = true, runNpm = false, privateLibIds = [], mavenRepos = [], regMap = {}, collectWarnings = [], mavenPropsByPom = null, mavenStore = null } = ecoFlags;
|
|
659
707
|
const registriesFor = eco => regMap[eco] || [];
|
|
660
708
|
const { expandWithTransitives } = require("./lib/cve-match");
|
|
661
709
|
const { writeReports, computeStats } = require("./lib/cve-report");
|
|
@@ -669,13 +717,9 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
669
717
|
const npmWarnings = collectWarnings || [];
|
|
670
718
|
let scanWarnings = [];
|
|
671
719
|
const directCount = resolved.size;
|
|
672
|
-
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
if (runMaven) {
|
|
676
|
-
const { detectScanCompletenessWarnings } = require("./lib/scan-completeness");
|
|
677
|
-
scanWarnings = detectScanCompletenessWarnings(resolved, { ranSnyk: !!options.snyk, ranTransitive: !!options.transitive });
|
|
678
|
-
}
|
|
720
|
+
// NOTE: scan-completeness (unresolved-versions) is computed LATER — after the BOM
|
|
721
|
+
// version-resolution step backfills external-BOM-managed versions — so it reflects
|
|
722
|
+
// what's *genuinely* still unresolved, not what a Maven Central BOM fetch will fix.
|
|
679
723
|
|
|
680
724
|
// ---- Vulnerability database update (global step progress) ----
|
|
681
725
|
ui.section("Vulnerability database update");
|
|
@@ -694,6 +738,17 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
694
738
|
const otherRegistryIds = activeIds.filter(id => id !== "maven" && id !== "npm" && id !== "yarn" && getCodec(id)?.checkRegistry);
|
|
695
739
|
const willCve = !!cveScanner && (!(options.cveOffline || offline) || cveIndexExists);
|
|
696
740
|
const willTransitive = !!(options.transitive && runMaven);
|
|
741
|
+
// Per-module version mediation overlay: recover transitive versions the global
|
|
742
|
+
// transitive pass masks via cross-module depMgmt bleed. Runs after (and only when)
|
|
743
|
+
// the global pass runs, and needs the parsed store + per-module props.
|
|
744
|
+
const willOverlay = willTransitive && !!mavenPropsByPom && !!mavenStore;
|
|
745
|
+
// External import BOMs (e.g. spring-boot-dependencies): resolve their managed
|
|
746
|
+
// versions to backfill declared deps that pin no version of their own (the usual
|
|
747
|
+
// Spring Boot setup). Cached via poms-cache; offline-aware (uses warmed POMs,
|
|
748
|
+
// never the network — same as transitive resolution).
|
|
749
|
+
const importBoms = (runMaven && mavenPropsByPom)
|
|
750
|
+
? require("./lib/maven-bom").collectImportBoms(mavenPropsByPom) : [];
|
|
751
|
+
const willBom = importBoms.length > 0;
|
|
697
752
|
const willOsv = !!options.osv;
|
|
698
753
|
const willOutdated = !!options.allLibs;
|
|
699
754
|
const willNvd = !!options.nvd;
|
|
@@ -705,9 +760,18 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
705
760
|
const willBinaryId = [...resolved.values()].some(d => d.provenance === "binary");
|
|
706
761
|
// License detection piggybacks on the registry passes (same fetched metadata),
|
|
707
762
|
// so it adds no progress step of its own.
|
|
708
|
-
const totalSteps = [willTransitive, willCve, /*EOL*/ true, willOutdated, /*npm reg*/ true, ...otherRegistryIds.map(() => true), willOsv, willNvd, willEpss, willKev, willRetire, willBinaryId].filter(Boolean).length;
|
|
763
|
+
const totalSteps = [willBom, willTransitive, willOverlay, willCve, /*EOL*/ true, willOutdated, /*npm reg*/ true, ...otherRegistryIds.map(() => true), willOsv, willNvd, willEpss, willKev, willRetire, willBinaryId].filter(Boolean).length;
|
|
709
764
|
const progress = new ui.Progress(totalSteps);
|
|
710
765
|
|
|
766
|
+
if (willBom) {
|
|
767
|
+
const st = progress.start("BOM version resolution (Maven Central)");
|
|
768
|
+
try {
|
|
769
|
+
const { resolveAndBackfill } = require("./lib/maven-bom");
|
|
770
|
+
const r = await resolveAndBackfill(mavenPropsByPom, resolved, { repos: mavenRepos, offline, verbose });
|
|
771
|
+
st.done(`${r.filled} dep version(s) from ${r.boms} import BOM(s)`);
|
|
772
|
+
} catch (err) { st.fail(err.message); }
|
|
773
|
+
}
|
|
774
|
+
|
|
711
775
|
if (willTransitive) {
|
|
712
776
|
const st = progress.start("Transitive resolution (Maven Central)");
|
|
713
777
|
await expandWithTransitives(resolved, {
|
|
@@ -720,6 +784,32 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
720
784
|
st.done(`+${resolved.size - directCount} transitive (total ${resolved.size})`);
|
|
721
785
|
}
|
|
722
786
|
|
|
787
|
+
if (willOverlay) {
|
|
788
|
+
const st = progress.start("Per-module version mediation (masked transitives)");
|
|
789
|
+
try {
|
|
790
|
+
const { expandPerModuleOverlay } = require("./lib/version-overlay");
|
|
791
|
+
const ov = await expandPerModuleOverlay(resolved, mavenStore, mavenPropsByPom, {
|
|
792
|
+
verbose,
|
|
793
|
+
offline,
|
|
794
|
+
maxDepth: parseInt(options.transitiveDepth, 10) || 6,
|
|
795
|
+
includeTestDeps: !options.ignoreTest,
|
|
796
|
+
repos: mavenRepos,
|
|
797
|
+
});
|
|
798
|
+
st.done(`+${ov.appended} masked version(s) recovered across ${ov.modules} module(s)`);
|
|
799
|
+
if (verbose && ov.recovered.length) {
|
|
800
|
+
for (const r of ov.recovered) console.log(` ↳ ${r.coord}: +${r.version} (had ${r.had}) via ${r.module}`);
|
|
801
|
+
}
|
|
802
|
+
} catch (err) { st.fail(err.message); }
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Scan-completeness signals — computed NOW (after BOM backfill) so only the deps
|
|
806
|
+
// still without a concrete version (external BOM truly unreachable, or offline)
|
|
807
|
+
// are flagged, not the ones we just resolved from spring-boot-dependencies & co.
|
|
808
|
+
if (runMaven) {
|
|
809
|
+
const { detectScanCompletenessWarnings } = require("./lib/scan-completeness");
|
|
810
|
+
scanWarnings = detectScanCompletenessWarnings(resolved, { ranSnyk: !!options.snyk, ranTransitive: !!options.transitive });
|
|
811
|
+
}
|
|
812
|
+
|
|
723
813
|
// 1. CVE — native scanner contributed by the maven codec (local cvelistV5 index).
|
|
724
814
|
let cveMatches = [];
|
|
725
815
|
let cveDataDate = null;
|
|
@@ -887,6 +977,7 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
887
977
|
// scanner is owned by the npm codec but runs whenever --retire is on.
|
|
888
978
|
let retireMatches = [];
|
|
889
979
|
let vendoredJsInventory = [];
|
|
980
|
+
let retireWarnings = [];
|
|
890
981
|
if (willRetire) {
|
|
891
982
|
const st = progress.start("retire.js (vendored JS)");
|
|
892
983
|
const sc = (getCodec("npm").nativeScanners || []).find(s => s.kind === "vendored");
|
|
@@ -898,8 +989,15 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
898
989
|
retireMatches = r.matches || [];
|
|
899
990
|
if (options.vendoredJsInventory !== false) vendoredJsInventory = r.meta?.inventory || [];
|
|
900
991
|
const invN = vendoredJsInventory.length;
|
|
901
|
-
|
|
902
|
-
|
|
992
|
+
if (r.meta?.error) {
|
|
993
|
+
// A genuine scan failure — surface it instead of letting an empty
|
|
994
|
+
// vendored-JS chapter look like "nothing found".
|
|
995
|
+
st.fail(r.meta.error);
|
|
996
|
+
retireWarnings.push({ type: "retire-failed", message: `${r.meta.error} — the vendored-JS scan (chapters 1D / 2) could not run, so any vendored \`.js\` (jQuery, Bootstrap, …) is NOT covered. Re-run with \`-v\` for the exact error; check the \`--src\` path exists and is readable.` });
|
|
997
|
+
} else {
|
|
998
|
+
st.done(`${retireMatches.length} finding(s)${invN ? ` · ${invN} lib(s) inventoried` : ""}`);
|
|
999
|
+
}
|
|
1000
|
+
} catch (err) { st.fail(err.message); retireWarnings.push({ type: "retire-failed", message: `retire.js scan failed: ${err.message} — vendored-JS chapters (1D / 2) not covered.` }); }
|
|
903
1001
|
}
|
|
904
1002
|
}
|
|
905
1003
|
|
|
@@ -1038,7 +1136,12 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
1038
1136
|
ui.warn(`${scanWarnings.length} scan-completeness note(s) — a real Maven/Snyk run may surface more:`);
|
|
1039
1137
|
for (const w of scanWarnings) {
|
|
1040
1138
|
ui.info(chalk.dim(`[${w.type}] ${w.message}`));
|
|
1041
|
-
|
|
1139
|
+
// Items may be plain strings or { id, manifestPaths } objects (the
|
|
1140
|
+
// unresolved-versions warning carries the defining manifest paths).
|
|
1141
|
+
for (const it of (w.items || []).slice(0, 4)) {
|
|
1142
|
+
const id = typeof it === "string" ? it : it.id;
|
|
1143
|
+
console.log(" " + chalk.dim(`· ${id}`));
|
|
1144
|
+
}
|
|
1042
1145
|
if ((w.items || []).length > 4) console.log(" " + chalk.dim(`· …and ${w.items.length - 4} more`));
|
|
1043
1146
|
}
|
|
1044
1147
|
}
|
|
@@ -1102,6 +1205,7 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
1102
1205
|
}] : []),
|
|
1103
1206
|
...npmWarnings,
|
|
1104
1207
|
...scanWarnings,
|
|
1208
|
+
...retireWarnings,
|
|
1105
1209
|
...(privateLibIds.length ? [{
|
|
1106
1210
|
type: "private-libs",
|
|
1107
1211
|
count: privateLibIds.length,
|
|
@@ -1231,3 +1335,5 @@ function mergeBySource(existing, additions) {
|
|
|
1231
1335
|
});
|
|
1232
1336
|
return merged;
|
|
1233
1337
|
}
|
|
1338
|
+
|
|
1339
|
+
} // end: compiled-binary retire-mode guard (see top of file)
|
package/lib/codecs/npm.codec.js
CHANGED
|
@@ -21,8 +21,8 @@ const retireScanner = {
|
|
|
21
21
|
kind: "vendored", // résultats → chapitre vendored-JS (séparé des CVE)
|
|
22
22
|
async scan(_deps, opts = {}) {
|
|
23
23
|
const { scanWithRetireFull } = require("../retire");
|
|
24
|
-
const { matches, inventory } = await scanWithRetireFull(opts.src, { verbose: opts.verbose, force: !!opts.retireRefresh, offline: !!opts.offline });
|
|
25
|
-
return { matches, meta: { inventory } };
|
|
24
|
+
const { matches, inventory, error } = await scanWithRetireFull(opts.src, { verbose: opts.verbose, force: !!opts.retireRefresh, offline: !!opts.offline });
|
|
25
|
+
return { matches, meta: { inventory, error } };
|
|
26
26
|
},
|
|
27
27
|
};
|
|
28
28
|
|
package/lib/cve-match.js
CHANGED
|
@@ -180,12 +180,17 @@ function matchOne(dep, entries, confidence) {
|
|
|
180
180
|
// the same g:a), not just the representative highest — otherwise a CVE that
|
|
181
181
|
// only affects a lower-versioned profile variant would be missed.
|
|
182
182
|
const versions = (dep.versions && dep.versions.length) ? dep.versions : [dep.version];
|
|
183
|
+
// Only CONCRETE, resolved versions can be matched. A dep whose version is null
|
|
184
|
+
// or an unresolved `${property}` (typically pinned by an external BOM like
|
|
185
|
+
// spring-boot-dependencies) carries NO version we can compare — matching it
|
|
186
|
+
// against every "affected" range would flag it as vulnerable to ALL the
|
|
187
|
+
// coordinate's CVEs (a flood of false CRITICALs with version "?"). Skip it;
|
|
188
|
+
// it's already reported in chapter 0 as an "unresolved-versions" warning.
|
|
189
|
+
const concrete = versions.filter(v => v && !/\$\{/.test(String(v)));
|
|
190
|
+
if (!concrete.length) return matches;
|
|
183
191
|
for (const e of entries) {
|
|
184
|
-
for (const ver of
|
|
185
|
-
const affected = (e.ranges || []).some(r =>
|
|
186
|
-
if (!ver) return r.status === "affected"; // unknown version → assume affected
|
|
187
|
-
return isVersionAffected(ver, r);
|
|
188
|
-
});
|
|
192
|
+
for (const ver of concrete) {
|
|
193
|
+
const affected = (e.ranges || []).some(r => isVersionAffected(ver, r));
|
|
189
194
|
if (affected) {
|
|
190
195
|
const vdep = ver === dep.version ? dep : { ...dep, version: ver };
|
|
191
196
|
matches.push({ dep: vdep, cve: { ...e }, confidence });
|