fad-checker 1.0.0 → 1.0.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/CLAUDE.md +21 -21
- package/CRITICAL-REVIEW.md +343 -0
- package/README.md +37 -37
- package/completions/{fad-check.bash → fad-checker.bash} +3 -3
- package/completions/{fad-check.zsh → fad-checker.zsh} +2 -2
- package/docs/ARCHITECTURE.md +10 -10
- package/docs/USAGE.md +35 -35
- package/{fad-check.js → fad-checker.js} +25 -21
- package/lib/cache-archive.js +9 -9
- package/lib/config.js +3 -3
- package/lib/cpe.js +23 -5
- package/lib/cve-download.js +3 -3
- package/lib/cve-match.js +13 -7
- package/lib/cve-report.js +4 -4
- package/lib/maven-repo.js +3 -3
- package/lib/maven-version.js +6 -0
- package/lib/nvd.js +62 -17
- package/lib/osv.js +53 -28
- package/lib/outdated.js +25 -5
- package/lib/retire.js +5 -5
- package/lib/scan-completeness.js +2 -2
- package/lib/snyk.js +4 -4
- package/lib/transitive.js +15 -7
- package/package.json +6 -6
- package/test/core.test.js +4 -4
- package/test/cpe.test.js +18 -0
- package/test/cve-match.test.js +56 -0
- package/test/cve-report.test.js +1 -1
- package/test/maven-version.test.js +9 -0
- package/test/monorepo.test.js +1 -1
- package/test/snyk.test.js +1 -1
- package/test/transitive.test.js +2 -2
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Architecture
|
|
2
2
|
|
|
3
|
-
This is the deep-dive for anyone modifying `fad-
|
|
3
|
+
This is the deep-dive for anyone modifying `fad-checker`'s internals or wondering why a particular finding shows up the way it does. For day-to-day usage, see [`USAGE.md`](USAGE.md).
|
|
4
4
|
|
|
5
5
|
## Module map
|
|
6
6
|
|
|
7
7
|
```
|
|
8
|
-
fad-
|
|
8
|
+
fad-checker.js Thin CLI: commander parsing, orchestration only.
|
|
9
9
|
lib/core.js POM parsing, parent resolution, all-profile merge, rewrite.
|
|
10
10
|
lib/maven-version.js Maven version parsing + range comparison (no external deps).
|
|
11
11
|
lib/cve-download.js Bulk download of CVEProject/cvelistV5 + Maven-relevant index build.
|
|
@@ -21,10 +21,10 @@ lib/retire.js retire.js (vendored-JS scanner) wrapper + cache + n
|
|
|
21
21
|
lib/scan-completeness.js Warnings for deps we couldn't fully resolve.
|
|
22
22
|
lib/npm/parse.js package.json, package-lock.json (v1/2/3), yarn.lock v1 parsers.
|
|
23
23
|
lib/npm/collect.js Merge across JS manifests → unified resolvedDeps Map.
|
|
24
|
-
lib/cache-archive.js tar.gz / zip export & import of ~/.fad-
|
|
25
|
-
lib/config.js Persistent user config in ~/.fad-
|
|
24
|
+
lib/cache-archive.js tar.gz / zip export & import of ~/.fad-checker/.
|
|
25
|
+
lib/config.js Persistent user config in ~/.fad-checker/config.json (mode 0600).
|
|
26
26
|
data/ Curated JSON: known-obsolete, eol-mapping, cpe-coord-map, known-public-namespaces.
|
|
27
|
-
completions/ fad-
|
|
27
|
+
completions/ fad-checker.bash, fad-checker.zsh
|
|
28
28
|
test/ node:test suite + fixtures (simple, complex-enterprise, monorepo-mixed, …).
|
|
29
29
|
```
|
|
30
30
|
|
|
@@ -61,11 +61,11 @@ The Maven keyspace and npm keyspace never collide — `:lodash` (Maven groupId-l
|
|
|
61
61
|
3. `getAllInheritedProps()` — merges `<dependencies>`, `<dependencyManagement>`, `<properties>` from **every** `<profile>` (with `activeByDefault` properties winning for value conflicts), follows `<scope>import</scope>` BOMs to other local POMs, and recurses into resolved parents.
|
|
62
62
|
4. `rewritePoms()` — strips everything outside `nodeToKeep`, runs `cleanDeps()` to apply the `-e` regex, rewrites the `<parent>.relativePath` and `version` to the parent's value (not the child's). Skips disk writes when `readOnly`.
|
|
63
63
|
|
|
64
|
-
## Report pipeline (driven by `fad-
|
|
64
|
+
## Report pipeline (driven by `fad-checker.js` when `--report` is set)
|
|
65
65
|
|
|
66
66
|
1. **Collect** — `collectResolvedDeps()` dedupes by `groupId:artifactId`, keeps the highest version on conflict, includes external parent POMs as `scope='parent'`. `--ignore-test` honored. For npm, `collectNpmDeps()` walks JS manifests (lockfile-only — `package.json` without sibling lockfile is skipped + warned).
|
|
67
67
|
2. **Transitive expansion** (optional, `--transitive`) — `expandWithTransitives()` walks the Maven Central POM graph honouring exclusions, root depMgmt overrides, nearest-wins on version conflict, `--transitive-depth` cap. Skips test + optional scopes by default.
|
|
68
|
-
3. **CVE index** — `ensureCveIndex()` downloads the daily CVEProject zip (via `curl + unzip`, or falls back to `fetch()` + `unzip` / PowerShell `Expand-Archive`), filters to Maven-relevant entries, caches the compact index to `~/.fad-
|
|
68
|
+
3. **CVE index** — `ensureCveIndex()` downloads the daily CVEProject zip (via `curl + unzip`, or falls back to `fetch()` + `unzip` / PowerShell `Expand-Archive`), filters to Maven-relevant entries, caches the compact index to `~/.fad-checker/cve-data/maven-cve-index.json`. Fresh for 24h. `--cve-refresh` forces rebuild, `--cve-offline` uses cache only.
|
|
69
69
|
4. **CVE matching** — `matchDepsAgainstCves()` runs three tiers:
|
|
70
70
|
- `exact`: `byPackageName["g:a"]` hit
|
|
71
71
|
- `probable`: `byProduct[artifactId]` + vendor matches groupId (`apache` ↔ `org.apache.*`)
|
|
@@ -76,13 +76,13 @@ The Maven keyspace and npm keyspace never collide — `:lodash` (Maven groupId-l
|
|
|
76
76
|
7. **CPE refinement** — `refineMatchesWithCpe()` walks NVD's `configurations[].nodes[]` against each matched dep:
|
|
77
77
|
- Confirms the dep version actually falls in the vulnerable range (else `cpeFiltered: true` — likely false positive).
|
|
78
78
|
- Upgrades match `confidence` from `possible` → `probable` → `exact` when a curated `cpe-coord-map.json` entry confirms vendor:product → dep coord.
|
|
79
|
-
8. **retire.js** (default on) — shells out to `retire --outputformat json --jspath <src>`. Output normalised to fad-
|
|
79
|
+
8. **retire.js** (default on) — shells out to `retire --outputformat json --jspath <src>`. Output normalised to fad-checker match shape, with the vendored file path attached so the report can show where the offending `.js` lives. Cache: `~/.fad-checker/retire-cache/<md5(src)>.json`, 24h TTL.
|
|
80
80
|
9. **EOL / Obsolete / Outdated** — `lib/outdated.js`:
|
|
81
81
|
- **EOL**: matches dep coord against `data/eol-mapping.json`, fetches the cycle list from endoflife.date (cached 7d), flags cycles past their EOL date.
|
|
82
82
|
- **Obsolete**: lookup in `data/known-obsolete.json` (curated: log4j 1.x, jackson-mapper-asl, joda-time, commons-httpclient 3.x, …).
|
|
83
83
|
- **Outdated**: Maven Central Solr query for the latest version. Cache 24h. Concurrency 8.
|
|
84
84
|
10. **Snyk** (optional, `--snyk`) — runs `snyk test --all-projects --json` against the cleaned target dir. Normalised + merged. Findings in both sources tagged `source: "both"`.
|
|
85
|
-
11. **Render** — `writeReports()` produces `cve-report.html` (self-contained, inline CSS, no external assets) and `cve-report.doc` (same HTML with Office XML namespace meta tags so Word opens it natively). Default output dir: `./fad-
|
|
85
|
+
11. **Render** — `writeReports()` produces `cve-report.html` (self-contained, inline CSS, no external assets) and `cve-report.doc` (same HTML with Office XML namespace meta tags so Word opens it natively). Default output dir: `./fad-checker-report/`.
|
|
86
86
|
|
|
87
87
|
## Report structure
|
|
88
88
|
|
|
@@ -130,7 +130,7 @@ The Maven keyspace and npm keyspace never collide — `:lodash` (Maven groupId-l
|
|
|
130
130
|
- The CVE bundle from CVEProject is ~500 MB unpacked. We shell out to `curl + unzip` (Node built-in fallback to `fetch()` + system `unzip` / PowerShell `Expand-Archive`). The extracted JSON is deleted after the index is built.
|
|
131
131
|
- The bundle ships as `cves.zip.zip` (a zip whose sole content is another zip). `extractZip()` recurses up to 3 levels.
|
|
132
132
|
- `endoflife.date` API responses are cached locally for 7 days; Maven Central version lookups for 24 hours.
|
|
133
|
-
- **Persistent config**: `~/.fad-
|
|
133
|
+
- **Persistent config**: `~/.fad-checker/config.json` (mode 0600) stores per-user state, currently the NVD API key. Set via `fad-checker --set-nvd-key <KEY>`.
|
|
134
134
|
- **`--offline` umbrella flag**: skips every network call (CVE index download, OSV queries, NVD enrichment, endoflife.date lookups, Maven Central version queries, transitive POM fetches, retire.js scans). Falls back to whatever is already cached. Per-source variants (`--cve-offline`, `--no-osv`, `--no-nvd`, `--no-retire`) still work independently.
|
|
135
135
|
- `snyk` is not a dependency — we shell out via `execFile`. `snyk` exits 1 when it finds vulnerabilities, which is expected (the JSON is still on stdout).
|
|
136
136
|
- The cleaned POM is the union of every profile's deps. Counts will therefore be larger than the source POM. This is intentional — verify your reasoning before "reducing" them.
|
package/docs/USAGE.md
CHANGED
|
@@ -5,7 +5,7 @@ Every flag, every common workflow, with copy-pasteable commands.
|
|
|
5
5
|
## Synopsis
|
|
6
6
|
|
|
7
7
|
```text
|
|
8
|
-
fad-
|
|
8
|
+
fad-checker -s <src> [-t <target>] [-e <regex>] [other options]
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
- `-s, --src <src>` — **required**. Root of the source tree to scan. Contains `pom.xml` and/or `package(-lock).json` / `yarn.lock`.
|
|
@@ -13,21 +13,21 @@ fad-check -s <src> [-t <target>] [-e <regex>] [other options]
|
|
|
13
13
|
|
|
14
14
|
## Output
|
|
15
15
|
|
|
16
|
-
By default the HTML + Word reports land in `./fad-
|
|
16
|
+
By default the HTML + Word reports land in `./fad-checker-report/`. Override with `--report-output <dir>`.
|
|
17
17
|
|
|
18
18
|
## Ecosystem selection
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
21
|
# Auto-detect (default): scan whatever pom.xml / package(-lock).json / yarn.lock exists
|
|
22
|
-
fad-
|
|
22
|
+
fad-checker -s .
|
|
23
23
|
|
|
24
24
|
# Force one ecosystem
|
|
25
|
-
fad-
|
|
26
|
-
fad-
|
|
27
|
-
fad-
|
|
25
|
+
fad-checker -s . --ecosystem maven
|
|
26
|
+
fad-checker -s . --ecosystem npm
|
|
27
|
+
fad-checker -s . --ecosystem both # scan both even if only one is auto-detected
|
|
28
28
|
|
|
29
29
|
# Disable JS scanning entirely (Maven-only)
|
|
30
|
-
fad-
|
|
30
|
+
fad-checker -s . --no-js
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
## Filtering deps
|
|
@@ -35,8 +35,8 @@ fad-check -s . --no-js
|
|
|
35
35
|
`-e <regex>` filters out coords whose **groupId** (Maven) or **name** (npm) matches the regex. Useful for private/internal libs that you know aren't on a public registry.
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
fad-
|
|
39
|
-
fad-
|
|
38
|
+
fad-checker -s . -e "^(com\.acme|org\.private)\."
|
|
39
|
+
fad-checker -s . -e "^@acme/"
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
The excluded coords are listed at the end of the run so you can audit the regex.
|
|
@@ -59,18 +59,18 @@ Each data source can be disabled independently:
|
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
61
|
# Use cached data only, no network (works for everything)
|
|
62
|
-
fad-
|
|
62
|
+
fad-checker -s . --offline
|
|
63
63
|
|
|
64
64
|
# Per-source offline
|
|
65
|
-
fad-
|
|
66
|
-
fad-
|
|
67
|
-
fad-
|
|
65
|
+
fad-checker -s . --cve-offline # use cached CVE index only
|
|
66
|
+
fad-checker -s . --cve-refresh # force re-download of CVE bundle
|
|
67
|
+
fad-checker -s . --retire-refresh # force re-scan with retire.js (ignore cache)
|
|
68
68
|
|
|
69
69
|
# Cache export / import (useful for air-gapped boxes)
|
|
70
|
-
fad-
|
|
71
|
-
fad-
|
|
72
|
-
fad-
|
|
73
|
-
fad-
|
|
70
|
+
fad-checker --export-cache fad-cache.tar.gz
|
|
71
|
+
fad-checker --export-cache fad-cache.tar.gz --include-config # bundle NVD key too
|
|
72
|
+
fad-checker --import-cache fad-cache.tar.gz
|
|
73
|
+
fad-checker --import-cache fad-cache.tar.gz --force # replace existing without backup
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
## NVD API key
|
|
@@ -79,24 +79,24 @@ NVD's public rate limit is 5 requests / 30s without a key. The free key bumps it
|
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
81
|
# Get a key in 30 seconds: https://nvd.nist.gov/developers/request-an-api-key
|
|
82
|
-
fad-
|
|
83
|
-
fad-
|
|
82
|
+
fad-checker --set-nvd-key YOUR_KEY # stored in ~/.fad-checker/config.json (mode 0600)
|
|
83
|
+
fad-checker --show-config # confirm it's persisted (key masked)
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
Or pass it ad-hoc via the `NVD_API_KEY` env var.
|
|
87
87
|
|
|
88
88
|
## Snyk integration
|
|
89
89
|
|
|
90
|
-
If you have `snyk` installed and authenticated, `fad-
|
|
90
|
+
If you have `snyk` installed and authenticated, `fad-checker` can drive it:
|
|
91
91
|
|
|
92
92
|
```bash
|
|
93
|
-
fad-
|
|
93
|
+
fad-checker -s ./proj -t ../proj-clean -e "^com\.acme\." --snyk
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
This:
|
|
97
97
|
1. Generates the cleaned POM tree at `../proj-clean/`.
|
|
98
98
|
2. Runs `snyk test --all-projects --json` against it.
|
|
99
|
-
3. Merges Snyk's findings into the report — entries present in both `fad-
|
|
99
|
+
3. Merges Snyk's findings into the report — entries present in both `fad-checker` and Snyk are tagged `source: "both"`.
|
|
100
100
|
|
|
101
101
|
`--snyk` requires `-t` (Snyk needs a real POM tree to scan).
|
|
102
102
|
|
|
@@ -104,7 +104,7 @@ This:
|
|
|
104
104
|
|
|
105
105
|
| Mode | Trigger | Disk writes |
|
|
106
106
|
| --- | --- | --- |
|
|
107
|
-
| Read-only | `-t` omitted (default) | Only `~/.fad-
|
|
107
|
+
| Read-only | `-t` omitted (default) | Only `~/.fad-checker/` caches and the report dir |
|
|
108
108
|
| Write | `-t <dir>` provided | Above + the cleaned POM tree at `<dir>` (and `<dir>` is `rimraf`'d first!) |
|
|
109
109
|
|
|
110
110
|
The `--target` guardrails refuse:
|
|
@@ -114,30 +114,30 @@ The `--target` guardrails refuse:
|
|
|
114
114
|
## Verbosity
|
|
115
115
|
|
|
116
116
|
```bash
|
|
117
|
-
fad-
|
|
117
|
+
fad-checker -s . -v # progress per source (OSV batches, NVD pages, retire scan, …)
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
## Shell completion
|
|
121
121
|
|
|
122
122
|
```bash
|
|
123
|
-
fad-
|
|
124
|
-
fad-
|
|
123
|
+
fad-checker --completion bash > /etc/bash_completion.d/fad-checker
|
|
124
|
+
fad-checker --completion zsh > ~/.zsh/completions/_fad-checker
|
|
125
125
|
```
|
|
126
126
|
|
|
127
127
|
## All flags at a glance
|
|
128
128
|
|
|
129
129
|
```bash
|
|
130
|
-
fad-
|
|
130
|
+
fad-checker --help
|
|
131
131
|
```
|
|
132
132
|
|
|
133
133
|
## Recipes
|
|
134
134
|
|
|
135
135
|
### CI gate: fail the build on any CRITICAL prod CVE
|
|
136
136
|
|
|
137
|
-
`fad-
|
|
137
|
+
`fad-checker` exits 0 even when CVEs are found (it's a reporter, not a gate). Wire your own:
|
|
138
138
|
|
|
139
139
|
```bash
|
|
140
|
-
fad-
|
|
140
|
+
fad-checker -s . --no-nvd > /dev/null
|
|
141
141
|
# Then grep the report or parse the structured output (planned).
|
|
142
142
|
```
|
|
143
143
|
|
|
@@ -148,7 +148,7 @@ fad-check -s . --no-nvd > /dev/null
|
|
|
148
148
|
Keep dated copies of the report:
|
|
149
149
|
|
|
150
150
|
```bash
|
|
151
|
-
fad-
|
|
151
|
+
fad-checker -s . --report-output reports/$(date +%F)
|
|
152
152
|
diff reports/2026-04-01/cve-report.html reports/2026-05-01/cve-report.html
|
|
153
153
|
```
|
|
154
154
|
|
|
@@ -157,15 +157,15 @@ diff reports/2026-04-01/cve-report.html reports/2026-05-01/cve-report.html
|
|
|
157
157
|
On a connected machine:
|
|
158
158
|
|
|
159
159
|
```bash
|
|
160
|
-
fad-
|
|
161
|
-
fad-
|
|
160
|
+
fad-checker -s ./dummy-empty-dir # populates ~/.fad-checker/ caches
|
|
161
|
+
fad-checker --export-cache fad-cache.tar.gz --include-config
|
|
162
162
|
```
|
|
163
163
|
|
|
164
164
|
Move `fad-cache.tar.gz` to the air-gapped box, then:
|
|
165
165
|
|
|
166
166
|
```bash
|
|
167
|
-
fad-
|
|
168
|
-
fad-
|
|
167
|
+
fad-checker --import-cache fad-cache.tar.gz
|
|
168
|
+
fad-checker -s ./real-project --offline
|
|
169
169
|
```
|
|
170
170
|
|
|
171
171
|
### Monorepo with Maven + JS + vendored JS
|
|
@@ -173,7 +173,7 @@ fad-check -s ./real-project --offline
|
|
|
173
173
|
The melino-style project (Java backend, React frontend in `web/`, vendored jQuery/PDF.js under `web/src/main/webapp/`):
|
|
174
174
|
|
|
175
175
|
```bash
|
|
176
|
-
fad-
|
|
176
|
+
fad-checker -s . -e "^(com\.captcha|org\.voxaly|com\.voxaly)\."
|
|
177
177
|
# → finds CVE in Maven deps, in web/package-lock.json deps,
|
|
178
178
|
# AND in the vendored .js files under webapp/.
|
|
179
179
|
```
|
|
@@ -27,7 +27,7 @@ if (process.argv.includes("--completion")) {
|
|
|
27
27
|
const shell = process.argv[shellIdx] && !process.argv[shellIdx].startsWith("-")
|
|
28
28
|
? process.argv[shellIdx]
|
|
29
29
|
: "bash";
|
|
30
|
-
const completionPath = path.join(__dirname, "completions", `fad-
|
|
30
|
+
const completionPath = path.join(__dirname, "completions", `fad-checker.${shell}`);
|
|
31
31
|
try {
|
|
32
32
|
process.stdout.write(fs.readFileSync(completionPath, "utf8"));
|
|
33
33
|
process.exit(0);
|
|
@@ -87,7 +87,7 @@ if (process.argv.includes("--add-repo") || process.argv.includes("--remove-repo"
|
|
|
87
87
|
const url = process.argv[idx + 2];
|
|
88
88
|
if (!name || name.startsWith("-") || !url || url.startsWith("-")) {
|
|
89
89
|
console.error(chalk.red("❌ --add-repo requires <name> <url>"));
|
|
90
|
-
console.error(" Example: fad-
|
|
90
|
+
console.error(" Example: fad-checker --add-repo nexus https://nexus.acme.com/repository/maven-public/");
|
|
91
91
|
console.error(" Optional auth: --add-repo nexus https://nexus.acme.com/repository/maven-public/ --auth user:pass");
|
|
92
92
|
process.exit(1);
|
|
93
93
|
}
|
|
@@ -121,7 +121,7 @@ if (process.argv.includes("--export-cache") || process.argv.includes("--import-c
|
|
|
121
121
|
if (exportIdx !== -1) {
|
|
122
122
|
const dest = process.argv[exportIdx + 1];
|
|
123
123
|
if (!dest || dest.startsWith("-")) {
|
|
124
|
-
console.error(chalk.red("❌ --export-cache requires a destination path (e.g. fad-
|
|
124
|
+
console.error(chalk.red("❌ --export-cache requires a destination path (e.g. fad-checker-cache.tar.gz)"));
|
|
125
125
|
process.exit(1);
|
|
126
126
|
}
|
|
127
127
|
const includeConfig = process.argv.includes("--include-config");
|
|
@@ -150,11 +150,11 @@ if (process.argv.includes("--export-cache") || process.argv.includes("--import-c
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
const USAGE = `
|
|
153
|
-
(1) fad-
|
|
154
|
-
(2) fad-
|
|
155
|
-
(3) fad-
|
|
156
|
-
(4) fad-
|
|
157
|
-
(5) fad-
|
|
153
|
+
(1) fad-checker -s ./proj # read-only: full report (CVE + EOL + obsolete + outdated + transitive)
|
|
154
|
+
(2) fad-checker -s ./proj -e "^(org.private|client)" # same, with regex exclusion of private deps
|
|
155
|
+
(3) fad-checker -s ./proj -t ../pom-clean -e "^(org.private|client)" # write cleaned POMs + full report
|
|
156
|
+
(4) fad-checker -s ./proj --no-transitive --no-all-libs # faster, only direct deps, no Maven Central queries
|
|
157
|
+
(5) fad-checker -s ./proj -t ../pom-clean -e "^..." --snyk # also run snyk and merge findings
|
|
158
158
|
`;
|
|
159
159
|
|
|
160
160
|
program
|
|
@@ -173,13 +173,13 @@ program
|
|
|
173
173
|
.option("--no-osv", "skip OSV.dev (Google/GitHub aggregated Maven CVE feed)")
|
|
174
174
|
.option("--no-nvd", "skip NIST NVD enrichment of matched CVEs")
|
|
175
175
|
.option("--offline", "no network: use cached CVE/OSV/NVD/POM data only")
|
|
176
|
-
.option("--set-nvd-key <key>", "save NVD API key to ~/.fad-
|
|
177
|
-
.option("--show-config", "print the persisted ~/.fad-
|
|
178
|
-
.option("--export-cache <file>", "tar.gz/zip the ~/.fad-
|
|
179
|
-
.option("--import-cache <file>", "restore ~/.fad-
|
|
176
|
+
.option("--set-nvd-key <key>", "save NVD API key to ~/.fad-checker/config.json (10× faster NVD enrichment)")
|
|
177
|
+
.option("--show-config", "print the persisted ~/.fad-checker/config.json")
|
|
178
|
+
.option("--export-cache <file>", "tar.gz/zip the ~/.fad-checker/ caches to <file> (excludes config.json by default)")
|
|
179
|
+
.option("--import-cache <file>", "restore ~/.fad-checker/ from a previously exported archive (existing dir is moved to .bak unless --force)")
|
|
180
180
|
.option("--include-config", "with --export-cache: also bundle config.json (contains the NVD API key)")
|
|
181
|
-
.option("--force", "with --import-cache: replace ~/.fad-
|
|
182
|
-
.option("--report-output <dir>", "report output directory", "./fad-
|
|
181
|
+
.option("--force", "with --import-cache: replace ~/.fad-checker/ without backup")
|
|
182
|
+
.option("--report-output <dir>", "report output directory", "./fad-checker-report")
|
|
183
183
|
.option("--ignore-test", "skip test-scoped dependencies in report")
|
|
184
184
|
.option("--cve-refresh", "force re-download of CVE database")
|
|
185
185
|
.option("--cve-offline", "use cached CVE index only (no download)")
|
|
@@ -218,7 +218,7 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
218
218
|
const p = `${g.replace(/\./g, "/")}/${a}/maven-metadata.xml`;
|
|
219
219
|
const { existsInAny } = require("./lib/maven-repo");
|
|
220
220
|
try {
|
|
221
|
-
const hit = await existsInAny(repos, p, { userAgent: "fad-
|
|
221
|
+
const hit = await existsInAny(repos, p, { userAgent: "fad-checker-existence" });
|
|
222
222
|
if (hit) return true;
|
|
223
223
|
console.log(`❌ NOT found on any repo: ${g}:${a}`);
|
|
224
224
|
return false;
|
|
@@ -231,7 +231,7 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
231
231
|
(async function main() {
|
|
232
232
|
console.log(chalk.bold.cyan("\n🚀 Fucking Autonomous Dependency Checker\n") + chalk.gray("─────────────────────────────"));
|
|
233
233
|
|
|
234
|
-
// Build the Maven repo list once: persisted repos (from ~/.fad-
|
|
234
|
+
// Build the Maven repo list once: persisted repos (from ~/.fad-checker/config.json)
|
|
235
235
|
// + ad-hoc --repo URLs + Maven Central as final fallback. Used by transitive
|
|
236
236
|
// resolution, outdated-version check, and existence check.
|
|
237
237
|
const { getMavenRepos } = require("./lib/config");
|
|
@@ -401,7 +401,7 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
|
|
|
401
401
|
}
|
|
402
402
|
const directCount = resolved.size;
|
|
403
403
|
|
|
404
|
-
// Scan-completeness signals: BOMs and unresolved-version deps mean fad-
|
|
404
|
+
// Scan-completeness signals: BOMs and unresolved-version deps mean fad-checker
|
|
405
405
|
// has gone as far as it can without running Maven/Snyk itself.
|
|
406
406
|
if (runMaven) {
|
|
407
407
|
const { detectScanCompletenessWarnings } = require("./lib/scan-completeness");
|
|
@@ -541,8 +541,11 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
|
|
|
541
541
|
|
|
542
542
|
// Split prod vs dev based on the dep's isDev flag (set at collection time
|
|
543
543
|
// from Maven scope=test/provided and npm dev/devOptional/optional).
|
|
544
|
-
|
|
545
|
-
|
|
544
|
+
// CPE-filtered matches are excluded from the CLI headline — they're surfaced
|
|
545
|
+
// in the HTML report's "Likely false positives" appendix instead.
|
|
546
|
+
const prodMatches = cveMatches.filter(m => !m.dep?.isDev && !m.cpeFiltered);
|
|
547
|
+
const devMatches = cveMatches.filter(m => m.dep?.isDev && !m.cpeFiltered);
|
|
548
|
+
const cpeFilteredCount = cveMatches.filter(m => m.cpeFiltered).length;
|
|
546
549
|
|
|
547
550
|
const stats = computeStats(prodMatches);
|
|
548
551
|
const devStats = computeStats(devMatches);
|
|
@@ -554,6 +557,7 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
|
|
|
554
557
|
console.log(` ${chalk.red(sev)} ${m.cve.id} ${depLabel(m.dep)}:${m.dep.version}`);
|
|
555
558
|
}
|
|
556
559
|
if (prodMatches.length > 20) console.log(` ... and ${prodMatches.length - 20} more (see report)`);
|
|
560
|
+
if (cpeFilteredCount) console.log(chalk.gray(` (${cpeFilteredCount} likely false positives moved to report appendix)`));
|
|
557
561
|
|
|
558
562
|
if (devMatches.length) {
|
|
559
563
|
console.log(chalk.bold.cyan(`\n 2. CVE in dev dependencies (${devMatches.length})`));
|
|
@@ -581,7 +585,7 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
|
|
|
581
585
|
if (!outdatedResults.length && options.allLibs) console.log(chalk.gray(" (none)"));
|
|
582
586
|
if (!options.allLibs) console.log(chalk.gray(" (re-run with -a/--allLibs to query Maven Central)"));
|
|
583
587
|
|
|
584
|
-
const reportDir = options.reportOutput || "./fad-
|
|
588
|
+
const reportDir = options.reportOutput || "./fad-checker-report";
|
|
585
589
|
await fs.promises.mkdir(reportDir, { recursive: true });
|
|
586
590
|
const projectInfo = {
|
|
587
591
|
name: path.basename(path.resolve(options.src)),
|
|
@@ -613,7 +617,7 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
|
|
|
613
617
|
const paths = (dep?.pomPaths || []).map(p => path.relative(options.src, p));
|
|
614
618
|
return { id, manifestPaths: paths };
|
|
615
619
|
}),
|
|
616
|
-
message: `${privateLibIds.length} Maven coord(s) not found on Maven Central — they are private/internal libraries. Their CVEs (if any) cannot be detected by fad-
|
|
620
|
+
message: `${privateLibIds.length} Maven coord(s) not found on Maven Central — they are private/internal libraries. Their CVEs (if any) cannot be detected by fad-checker; if you have an internal CVE feed, audit them separately.`,
|
|
617
621
|
}] : []),
|
|
618
622
|
],
|
|
619
623
|
});
|
package/lib/cache-archive.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/cache-archive.js — export / import the entire ~/.fad-
|
|
2
|
+
* lib/cache-archive.js — export / import the entire ~/.fad-checker/ directory.
|
|
3
3
|
*
|
|
4
4
|
* Use case:
|
|
5
5
|
* - Move the warmed-up CVE/OSV/NVD/POM caches between machines
|
|
@@ -20,17 +20,17 @@ const { execFile } = require("child_process");
|
|
|
20
20
|
const { promisify } = require("util");
|
|
21
21
|
const execFileP = promisify(execFile);
|
|
22
22
|
|
|
23
|
-
const FAD_CACHE_DIR = path.join(os.homedir(), ".fad-
|
|
23
|
+
const FAD_CACHE_DIR = path.join(os.homedir(), ".fad-checker");
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
* Files inside ~/.fad-
|
|
26
|
+
* Files inside ~/.fad-checker/ that hold secrets and should NOT be shipped by default.
|
|
27
27
|
* Override by passing `includeConfig: true`.
|
|
28
28
|
*/
|
|
29
29
|
const SENSITIVE_FILES = ["config.json"];
|
|
30
30
|
|
|
31
31
|
async function exportCache(destFile, opts = {}) {
|
|
32
32
|
const { verbose, includeConfig } = opts;
|
|
33
|
-
if (!fs.existsSync(FAD_CACHE_DIR)) throw new Error(`no ~/.fad-
|
|
33
|
+
if (!fs.existsSync(FAD_CACHE_DIR)) throw new Error(`no ~/.fad-checker/ directory to export`);
|
|
34
34
|
const abs = path.resolve(destFile);
|
|
35
35
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
36
36
|
|
|
@@ -49,7 +49,7 @@ async function exportCache(destFile, opts = {}) {
|
|
|
49
49
|
if (verbose) console.log(`📦 Compress-Archive -Path ${FAD_CACHE_DIR}\\* -DestinationPath ${abs}`);
|
|
50
50
|
// Powershell Compress-Archive doesn't have a clean exclude flag — manual copy
|
|
51
51
|
await execFileP("powershell", ["-NoProfile", "-Command",
|
|
52
|
-
`$src='${FAD_CACHE_DIR}'; $dst='${abs}'; ${includeConfig ? `Compress-Archive -Path "$src\\*" -DestinationPath $dst -Force` : `$tmp=Join-Path $env:TEMP "fad-
|
|
52
|
+
`$src='${FAD_CACHE_DIR}'; $dst='${abs}'; ${includeConfig ? `Compress-Archive -Path "$src\\*" -DestinationPath $dst -Force` : `$tmp=Join-Path $env:TEMP "fad-checker-stage-$(Get-Random)"; Copy-Item $src $tmp -Recurse; ${SENSITIVE_FILES.map(f => `Remove-Item -Force -ErrorAction SilentlyContinue (Join-Path $tmp '${f}')`).join("; ")}; Compress-Archive -Path "$tmp\\*" -DestinationPath $dst -Force; Remove-Item $tmp -Recurse -Force`}`]);
|
|
53
53
|
} else {
|
|
54
54
|
const args = ["-r", "-q", abs, path.basename(FAD_CACHE_DIR)];
|
|
55
55
|
for (const e of excludes) { args.push("-x"); args.push(e); }
|
|
@@ -70,13 +70,13 @@ async function importCache(srcFile, opts = {}) {
|
|
|
70
70
|
if (!fs.existsSync(abs)) throw new Error(`archive not found: ${abs}`);
|
|
71
71
|
|
|
72
72
|
if (fs.existsSync(FAD_CACHE_DIR) && !force) {
|
|
73
|
-
// Move existing aside as .fad-
|
|
73
|
+
// Move existing aside as .fad-checker.bak-<timestamp>
|
|
74
74
|
const backup = `${FAD_CACHE_DIR}.bak-${Date.now()}`;
|
|
75
75
|
fs.renameSync(FAD_CACHE_DIR, backup);
|
|
76
|
-
if (verbose) console.log(`💾 existing ~/.fad-
|
|
76
|
+
if (verbose) console.log(`💾 existing ~/.fad-checker/ moved to ${backup}`);
|
|
77
77
|
} else if (force && fs.existsSync(FAD_CACHE_DIR)) {
|
|
78
78
|
fs.rmSync(FAD_CACHE_DIR, { recursive: true, force: true });
|
|
79
|
-
if (verbose) console.log(`🗑 --force: existing ~/.fad-
|
|
79
|
+
if (verbose) console.log(`🗑 --force: existing ~/.fad-checker/ removed`);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
const parent = path.dirname(FAD_CACHE_DIR);
|
|
@@ -99,7 +99,7 @@ async function importCache(srcFile, opts = {}) {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
if (!fs.existsSync(FAD_CACHE_DIR)) {
|
|
102
|
-
throw new Error(`import completed but ~/.fad-
|
|
102
|
+
throw new Error(`import completed but ~/.fad-checker/ was not created — was the archive built with fad-checker --export-cache?`);
|
|
103
103
|
}
|
|
104
104
|
return { dir: FAD_CACHE_DIR };
|
|
105
105
|
}
|
package/lib/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/config.js — persistent user config in ~/.fad-
|
|
2
|
+
* lib/config.js — persistent user config in ~/.fad-checker/config.json
|
|
3
3
|
*
|
|
4
4
|
* Stores credentials and per-user preferences that should survive across runs.
|
|
5
5
|
* Currently: NVD API key (so users don't have to re-export the env var).
|
|
@@ -8,7 +8,7 @@ const fs = require("fs");
|
|
|
8
8
|
const path = require("path");
|
|
9
9
|
const os = require("os");
|
|
10
10
|
|
|
11
|
-
const CONFIG_DIR = path.join(os.homedir(), ".fad-
|
|
11
|
+
const CONFIG_DIR = path.join(os.homedir(), ".fad-checker");
|
|
12
12
|
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
13
13
|
|
|
14
14
|
function load() {
|
|
@@ -35,7 +35,7 @@ function get(key) {
|
|
|
35
35
|
return load()[key];
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/** NVD API key resolution: env var first, then ~/.fad-
|
|
38
|
+
/** NVD API key resolution: env var first, then ~/.fad-checker/config.json. */
|
|
39
39
|
function getNvdApiKey() {
|
|
40
40
|
return process.env.NVD_API_KEY || get("nvd_api_key") || null;
|
|
41
41
|
}
|
package/lib/cpe.js
CHANGED
|
@@ -67,6 +67,17 @@ function parseCpe23(uri) {
|
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// Memoize parseCpe23 on the cpeMatch object so subsequent passes (the
|
|
71
|
+
// confidence walk inside evaluateCveForDep) don't re-tokenize the same URI.
|
|
72
|
+
// `_parsedCpe` uses `null` (parse failed) vs `undefined` (not yet parsed) to
|
|
73
|
+
// distinguish empty result from missing cache.
|
|
74
|
+
function parseCpe23Cached(cpeMatch) {
|
|
75
|
+
if (!cpeMatch || typeof cpeMatch !== "object") return parseCpe23(cpeMatch?.criteria || "");
|
|
76
|
+
if (cpeMatch._parsedCpe !== undefined) return cpeMatch._parsedCpe;
|
|
77
|
+
cpeMatch._parsedCpe = parseCpe23(cpeMatch.criteria || "");
|
|
78
|
+
return cpeMatch._parsedCpe;
|
|
79
|
+
}
|
|
80
|
+
|
|
70
81
|
/**
|
|
71
82
|
* Match a dep version against a single NVD cpeMatch entry.
|
|
72
83
|
* The entry has the shape:
|
|
@@ -81,12 +92,18 @@ function parseCpe23(uri) {
|
|
|
81
92
|
function matchVersionRange(depVersion, cpeMatch) {
|
|
82
93
|
if (!cpeMatch) return false;
|
|
83
94
|
if (!depVersion) return true; // unknown version → assume affected
|
|
84
|
-
const parsed =
|
|
95
|
+
const parsed = parseCpe23Cached(cpeMatch);
|
|
85
96
|
if (!parsed) return false;
|
|
86
97
|
|
|
87
98
|
// Hard pin in the criteria URI itself
|
|
88
99
|
if (parsed.version && parsed.version !== "*" && parsed.version !== "-") {
|
|
89
|
-
|
|
100
|
+
// CPE 2.3 update field qualifies the version (`:1.0.0:beta1:` ≡ `1.0.0-beta1`).
|
|
101
|
+
// Without folding it in, a release like 1.0.0 would incorrectly match a pin
|
|
102
|
+
// of 1.0.0:rc1 — see H5 in CRITICAL-REVIEW.md.
|
|
103
|
+
const pinned = (parsed.update && parsed.update !== "*" && parsed.update !== "-")
|
|
104
|
+
? `${parsed.version}-${parsed.update}`
|
|
105
|
+
: parsed.version;
|
|
106
|
+
try { return compareMavenVersions(depVersion, pinned) === 0; }
|
|
90
107
|
catch { return false; }
|
|
91
108
|
}
|
|
92
109
|
|
|
@@ -112,7 +129,7 @@ function nodeAffectsDep(node, dep, cpeCoordMap) {
|
|
|
112
129
|
const matches = node.cpeMatch || node.cpe_match || [];
|
|
113
130
|
for (const m of matches) {
|
|
114
131
|
if (m.vulnerable === false) continue;
|
|
115
|
-
const parsed =
|
|
132
|
+
const parsed = parseCpe23Cached(m);
|
|
116
133
|
if (!parsed) continue;
|
|
117
134
|
if (!cpeMatchesDep(parsed, dep, cpeCoordMap)) continue;
|
|
118
135
|
if (!matchVersionRange(dep.version, m)) continue;
|
|
@@ -168,8 +185,9 @@ function cpeMatchesDep(cpe, dep, cpeCoordMap) {
|
|
|
168
185
|
const p = (cpe.product || "").toLowerCase();
|
|
169
186
|
if (p !== a) return false;
|
|
170
187
|
if (g === v) return true;
|
|
188
|
+
// Mirrors vendorMatchesGroup: dot-segment membership only, no substring
|
|
189
|
+
// fallback. Unbounded substring matching leaked FPs (M4 in CRITICAL-REVIEW.md).
|
|
171
190
|
if (g.split(".").includes(v)) return true;
|
|
172
|
-
if (g.includes(v) || v.includes(g)) return true;
|
|
173
191
|
}
|
|
174
192
|
return false;
|
|
175
193
|
}
|
|
@@ -213,7 +231,7 @@ function evaluateCveForDep(cveRecord, dep, cpeCoordMap) {
|
|
|
213
231
|
for (const n of nodes) {
|
|
214
232
|
for (const m of n.cpeMatch || n.cpe_match || []) {
|
|
215
233
|
if (m.vulnerable === false) continue;
|
|
216
|
-
const parsed =
|
|
234
|
+
const parsed = parseCpe23Cached(m);
|
|
217
235
|
if (!parsed) continue;
|
|
218
236
|
if (!cpeMatchesDep(parsed, dep, map)) continue;
|
|
219
237
|
if (!matchVersionRange(dep.version, m)) continue;
|
package/lib/cve-download.js
CHANGED
|
@@ -13,7 +13,7 @@ const { execFile } = require("child_process");
|
|
|
13
13
|
const { promisify } = require("util");
|
|
14
14
|
const execFileP = promisify(execFile);
|
|
15
15
|
|
|
16
|
-
const CVE_DATA_DIR = path.join(os.homedir(), ".fad-
|
|
16
|
+
const CVE_DATA_DIR = path.join(os.homedir(), ".fad-checker", "cve-data");
|
|
17
17
|
const CVE_INDEX_PATH = path.join(CVE_DATA_DIR, "maven-cve-index.json");
|
|
18
18
|
const CVE_META_PATH = path.join(CVE_DATA_DIR, "meta.json");
|
|
19
19
|
const CVE_EXTRACT_DIR = path.join(CVE_DATA_DIR, "extracted");
|
|
@@ -174,7 +174,7 @@ async function commandExists(name) {
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
async function fetchLatestRelease() {
|
|
177
|
-
const headers = { "User-Agent": "fad-
|
|
177
|
+
const headers = { "User-Agent": "fad-checker-cve-downloader" };
|
|
178
178
|
if (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
|
179
179
|
const res = await fetch("https://api.github.com/repos/CVEProject/cvelistV5/releases/latest", { headers });
|
|
180
180
|
if (!res.ok) throw new Error(`GitHub API HTTP ${res.status}`);
|
|
@@ -341,7 +341,7 @@ async function downloadCveData(opts = {}) {
|
|
|
341
341
|
}
|
|
342
342
|
|
|
343
343
|
/**
|
|
344
|
-
* Single entry point used by fad-
|
|
344
|
+
* Single entry point used by fad-checker.js: returns a usable CVE index,
|
|
345
345
|
* downloading/refreshing as needed.
|
|
346
346
|
*/
|
|
347
347
|
async function ensureCveIndex(opts = {}) {
|
package/lib/cve-match.js
CHANGED
|
@@ -166,16 +166,22 @@ function vendorMatchesGroup(vendor, groupId) {
|
|
|
166
166
|
const v = vendor.toLowerCase();
|
|
167
167
|
const g = groupId.toLowerCase();
|
|
168
168
|
if (g === v) return true;
|
|
169
|
-
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
169
|
+
// org.apache.commons matches vendor "apache" via dot-segment membership.
|
|
170
|
+
// Unbounded substring checks (g.includes(v) / v.includes(g)) used to leak
|
|
171
|
+
// FPs — e.g. vendor "open" matching org.opensaml.* because "opensaml"
|
|
172
|
+
// contains "open". Dot-segment membership is strict enough.
|
|
173
|
+
if (g.split(".").includes(v)) return true;
|
|
174
|
+
return false;
|
|
173
175
|
}
|
|
174
176
|
|
|
175
|
-
function matchDepsAgainstCves(resolvedDeps, cveIndex) {
|
|
177
|
+
function matchDepsAgainstCves(resolvedDeps, cveIndex, opts = {}) {
|
|
176
178
|
if (!cveIndex) return [];
|
|
177
179
|
const byPackage = cveIndex.byPackageName || {};
|
|
178
180
|
const byProduct = cveIndex.byProduct || {};
|
|
181
|
+
// "possible" tier (product matches but vendor doesn't) is FP-heavy and
|
|
182
|
+
// hidden by default. The CPE refinement step can still rescue a real hit
|
|
183
|
+
// later; opt-in shows the raw bucket for triage.
|
|
184
|
+
const includePossibleTier = !!opts.includePossibleTier;
|
|
179
185
|
|
|
180
186
|
const all = [];
|
|
181
187
|
for (const dep of resolvedDeps.values()) {
|
|
@@ -187,12 +193,12 @@ function matchDepsAgainstCves(resolvedDeps, cveIndex) {
|
|
|
187
193
|
// Tier 1: exact packageName match
|
|
188
194
|
const t1 = matchOne(dep, byPackage[key], "exact");
|
|
189
195
|
all.push(...t1);
|
|
190
|
-
// Tier 2
|
|
196
|
+
// Tier 2: product match scoped by vendor heuristic
|
|
191
197
|
const productMatches = byProduct[dep.artifactId.toLowerCase()] || [];
|
|
192
198
|
for (const e of productMatches) {
|
|
193
199
|
if (vendorMatchesGroup(e.vendor, dep.groupId)) {
|
|
194
200
|
all.push(...matchOne(dep, [e], "probable"));
|
|
195
|
-
} else {
|
|
201
|
+
} else if (includePossibleTier) {
|
|
196
202
|
all.push(...matchOne(dep, [e], "possible"));
|
|
197
203
|
}
|
|
198
204
|
}
|