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.
@@ -1,11 +1,11 @@
1
1
  # Architecture
2
2
 
3
- This is the deep-dive for anyone modifying `fad-check`'s internals or wondering why a particular finding shows up the way it does. For day-to-day usage, see [`USAGE.md`](USAGE.md).
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-check.js Thin CLI: commander parsing, orchestration only.
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-check/.
25
- lib/config.js Persistent user config in ~/.fad-check/config.json (mode 0600).
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-check.bash, fad-check.zsh
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-check.js` when `--report` is set)
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-check/cve-data/maven-cve-index.json`. Fresh for 24h. `--cve-refresh` forces rebuild, `--cve-offline` uses cache only.
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-check match shape, with the vendored file path attached so the report can show where the offending `.js` lives. Cache: `~/.fad-check/retire-cache/<md5(src)>.json`, 24h TTL.
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-check-report/`.
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-check/config.json` (mode 0600) stores per-user state, currently the NVD API key. Set via `fad-check --set-nvd-key <KEY>`.
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-check -s <src> [-t <target>] [-e <regex>] [other options]
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-check-report/`. Override with `--report-output <dir>`.
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-check -s .
22
+ fad-checker -s .
23
23
 
24
24
  # Force one ecosystem
25
- fad-check -s . --ecosystem maven
26
- fad-check -s . --ecosystem npm
27
- fad-check -s . --ecosystem both # scan both even if only one is auto-detected
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-check -s . --no-js
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-check -s . -e "^(com\.acme|org\.private)\."
39
- fad-check -s . -e "^@acme/"
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-check -s . --offline
62
+ fad-checker -s . --offline
63
63
 
64
64
  # Per-source offline
65
- fad-check -s . --cve-offline # use cached CVE index only
66
- fad-check -s . --cve-refresh # force re-download of CVE bundle
67
- fad-check -s . --retire-refresh # force re-scan with retire.js (ignore cache)
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-check --export-cache fad-cache.tar.gz
71
- fad-check --export-cache fad-cache.tar.gz --include-config # bundle NVD key too
72
- fad-check --import-cache fad-cache.tar.gz
73
- fad-check --import-cache fad-cache.tar.gz --force # replace existing without backup
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-check --set-nvd-key YOUR_KEY # stored in ~/.fad-check/config.json (mode 0600)
83
- fad-check --show-config # confirm it's persisted (key masked)
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-check` can drive it:
90
+ If you have `snyk` installed and authenticated, `fad-checker` can drive it:
91
91
 
92
92
  ```bash
93
- fad-check -s ./proj -t ../proj-clean -e "^com\.acme\." --snyk
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-check` and Snyk are tagged `source: "both"`.
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-check/` caches and the report dir |
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-check -s . -v # progress per source (OSV batches, NVD pages, retire scan, …)
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-check --completion bash > /etc/bash_completion.d/fad-check
124
- fad-check --completion zsh > ~/.zsh/completions/_fad-check
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-check --help
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-check` exits 0 even when CVEs are found (it's a reporter, not a gate). Wire your own:
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-check -s . --no-nvd > /dev/null
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-check -s . --report-output reports/$(date +%F)
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-check -s ./dummy-empty-dir # populates ~/.fad-check/ caches
161
- fad-check --export-cache fad-cache.tar.gz --include-config
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-check --import-cache fad-cache.tar.gz
168
- fad-check -s ./real-project --offline
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-check -s . -e "^(com\.captcha|org\.voxaly|com\.voxaly)\."
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-check.${shell}`);
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-check --add-repo nexus https://nexus.acme.com/repository/maven-public/");
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-check-cache.tar.gz)"));
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-check -s ./proj # read-only: full report (CVE + EOL + obsolete + outdated + transitive)
154
- (2) fad-check -s ./proj -e "^(org.private|client)" # same, with regex exclusion of private deps
155
- (3) fad-check -s ./proj -t ../pom-clean -e "^(org.private|client)" # write cleaned POMs + full report
156
- (4) fad-check -s ./proj --no-transitive --no-all-libs # faster, only direct deps, no Maven Central queries
157
- (5) fad-check -s ./proj -t ../pom-clean -e "^..." --snyk # also run snyk and merge findings
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-check/config.json (10× faster NVD enrichment)")
177
- .option("--show-config", "print the persisted ~/.fad-check/config.json")
178
- .option("--export-cache <file>", "tar.gz/zip the ~/.fad-check/ caches to <file> (excludes config.json by default)")
179
- .option("--import-cache <file>", "restore ~/.fad-check/ from a previously exported archive (existing dir is moved to .bak unless --force)")
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-check/ without backup")
182
- .option("--report-output <dir>", "report output directory", "./fad-check-report")
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-check-existence" });
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-check/config.json)
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-check
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
- const prodMatches = cveMatches.filter(m => !m.dep?.isDev);
545
- const devMatches = cveMatches.filter(m => m.dep?.isDev);
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-check-report";
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-check; if you have an internal CVE feed, audit them separately.`,
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
  });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * lib/cache-archive.js — export / import the entire ~/.fad-check/ directory.
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-check");
23
+ const FAD_CACHE_DIR = path.join(os.homedir(), ".fad-checker");
24
24
 
25
25
  /**
26
- * Files inside ~/.fad-check/ that hold secrets and should NOT be shipped by default.
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-check/ directory to export`);
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-check-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`}`]);
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-check.bak-<timestamp>
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-check/ moved to ${backup}`);
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-check/ removed`);
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-check/ was not created — was the archive built with fad-check --export-cache?`);
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-check/config.json
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-check");
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-check/config.json. */
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 = parseCpe23(cpeMatch.criteria || "");
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
- try { return compareMavenVersions(depVersion, parsed.version) === 0; }
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 = parseCpe23(m.criteria || "");
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 = parseCpe23(m.criteria || "");
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;
@@ -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-check", "cve-data");
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-check-cve-downloader" };
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-check.js: returns a usable CVE index,
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
- if (g.includes(v) || v.includes(g)) return true;
170
- // org.apache.commons matches vendor "apache"
171
- const parts = g.split(".");
172
- return parts.includes(v);
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/3: product match, scoped by vendor heuristic
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
  }