fad-checker 1.0.2 → 1.0.3

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/fad-checker.js CHANGED
@@ -540,27 +540,30 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
540
540
  }
541
541
 
542
542
  // Split prod vs dev based on the dep's isDev flag (set at collection time
543
- // from Maven scope=test/provided and npm dev/devOptional/optional).
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;
549
-
550
- const stats = computeStats(prodMatches);
551
- const devStats = computeStats(devMatches);
552
- console.log(chalk.bold.cyan(`\n 1. CVE Vulnerabilities (production: ${prodMatches.length})`));
543
+ // from Maven scope=test/provided and npm dev/devOptional/optional). Keep the
544
+ // full per-bucket list (including cpeFiltered) so the HTML report can render
545
+ // its "Likely false positives" appendix — only the CLI headline excludes
546
+ // cpeFiltered to avoid alarming on triaged-out matches.
547
+ const prodMatches = cveMatches.filter(m => !m.dep?.isDev);
548
+ const devMatches = cveMatches.filter(m => m.dep?.isDev);
549
+ const prodActive = prodMatches.filter(m => !m.cpeFiltered);
550
+ const devActive = devMatches.filter(m => !m.cpeFiltered);
551
+ const cpeFilteredCount = (prodMatches.length - prodActive.length) + (devMatches.length - devActive.length);
552
+
553
+ const stats = computeStats(prodActive);
554
+ const devStats = computeStats(devActive);
555
+ console.log(chalk.bold.cyan(`\n 1. CVE Vulnerabilities (production: ${prodActive.length})`));
553
556
  console.log(` critical=${stats.critical} high=${stats.high} medium=${stats.medium} low=${stats.low} unknown=${stats.unknown}`);
554
557
  const depLabel = d => d.ecosystem === "npm" ? `npm:${d.artifactId}` : `${d.groupId}:${d.artifactId}`;
555
- for (const m of prodMatches.slice(0, 20)) {
558
+ for (const m of prodActive.slice(0, 20)) {
556
559
  const sev = (m.cve.severity || "UNKNOWN").padEnd(8);
557
560
  console.log(` ${chalk.red(sev)} ${m.cve.id} ${depLabel(m.dep)}:${m.dep.version}`);
558
561
  }
559
- if (prodMatches.length > 20) console.log(` ... and ${prodMatches.length - 20} more (see report)`);
562
+ if (prodActive.length > 20) console.log(` ... and ${prodActive.length - 20} more (see report)`);
560
563
  if (cpeFilteredCount) console.log(chalk.gray(` (${cpeFilteredCount} likely false positives moved to report appendix)`));
561
564
 
562
- if (devMatches.length) {
563
- console.log(chalk.bold.cyan(`\n 2. CVE in dev dependencies (${devMatches.length})`));
565
+ if (devActive.length) {
566
+ console.log(chalk.bold.cyan(`\n 2. CVE in dev dependencies (${devActive.length})`));
564
567
  console.log(` critical=${devStats.critical} high=${devStats.high} medium=${devStats.medium} low=${devStats.low} unknown=${devStats.unknown}`);
565
568
  }
566
569
  if (retireMatches.length) {
package/lib/cve-match.js CHANGED
@@ -166,11 +166,15 @@ function vendorMatchesGroup(vendor, groupId) {
166
166
  const v = vendor.toLowerCase();
167
167
  const g = groupId.toLowerCase();
168
168
  if (g === v) return true;
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;
169
+ const segments = g.split(".");
170
+ // Plain dot-segment match (vendor "apache" ⊂ "org.apache.commons").
171
+ if (segments.includes(v)) return true;
172
+ // NVD/CVEProject often record vendor as a legal entity ("qos.ch sarl",
173
+ // "the apache software foundation"). Split on non-alphanumerics and accept
174
+ // the match if any token is a dot-segment of the groupId. Unbounded
175
+ // substring matching is still avoided.
176
+ const vendorTokens = v.split(/[^a-z0-9]+/).filter(Boolean);
177
+ if (vendorTokens.length > 1 && vendorTokens.some(t => segments.includes(t))) return true;
174
178
  return false;
175
179
  }
176
180
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fad-checker",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Fucking Autonomous Dependency Checker — multi-ecosystem CVE / EOL / outdated / vendored-JS scanner for Maven, npm and Yarn monorepos. Self-contained HTML + Word report.",
5
5
  "license": "MIT",
6
6
  "author": "N8tz <n8tz.js@gmail.com>",
@@ -61,6 +61,19 @@ test("vendorMatchesGroup rejects substring leaks (H2)", () => {
61
61
  assert.equal(vendorMatchesGroup("springframework", "org.springframework.boot"), true);
62
62
  });
63
63
 
64
+ test("vendorMatchesGroup tokenizes multi-word legal-entity vendors", () => {
65
+ // NVD records the logback vendor as "qos.ch sarl" — a legal entity name.
66
+ // Neither equality nor plain dot-segment catches it, but tokenization on
67
+ // non-alphanumerics finds "qos"/"ch" as group segments and confirms the
68
+ // match. Without this, real logback CVEs landed in tier "possible".
69
+ assert.equal(vendorMatchesGroup("qos.ch sarl", "ch.qos.logback"), true);
70
+ assert.equal(vendorMatchesGroup("the apache software foundation", "org.apache.commons"), true);
71
+ // A single-token vendor must NOT be tokenized into substrings — that
72
+ // would re-introduce the H2 leak (e.g. "open" → ["open"] alone never
73
+ // matches arbitrary "org.opensaml.*").
74
+ assert.equal(vendorMatchesGroup("open", "org.opensaml.core"), false);
75
+ });
76
+
64
77
  test("matchDepsAgainstCves hides 'possible' tier by default (H3)", () => {
65
78
  // product matches `log4j-core` but vendor `acme` doesn't match groupId.
66
79
  // Without opts.includePossibleTier we expect zero matches.
@@ -1,343 +0,0 @@
1
- # fad-checker — Analyse critique complète
2
-
3
- **Date :** 2026-05-22
4
- **Périmètre :** intégralité du code (`fad-checker.js` + `lib/**/*.js` + `data/*.json` + `test/`)
5
- **Méthode :** 4 audits parallèles à effort xhigh (reuse / quality / efficiency / **faux positifs**)
6
- **Verdict global :** architecture saine, mais **4 chemins fail-open en cascade dans le matcher CVE** produisent un taux de faux positifs estimé entre **20 % et 60 %** selon le niveau de confiance retenu. À traiter en priorité avant toute exploitation en production.
7
-
8
- ---
9
-
10
- ## TL;DR — Risque de faux positifs
11
-
12
- | Tier de confiance | Taux de FP estimé sur ~500 deps | Recommandation |
13
- |---|---|---|
14
- | `exact` uniquement | 5–10 % | Utilisable (triage léger) |
15
- | `exact` + `probable` | 20–30 % | Triage manuel obligatoire |
16
- | Tous tiers (`exact` + `probable` + `possible`) | **40–60 %** | Inexploitable sans tri humain |
17
-
18
- Quatre fail-open en cascade :
19
-
20
- 1. **H1** — `isVersionAffected` retourne `true` quand le CVE n'a aucune borne de version (`lib/maven-version.js:101-124`).
21
- 2. **H2** — `vendorMatchesGroup` fait du *substring matching* (`g.includes(v) || v.includes(g)`) — `vendor="open"` matche `org.opensaml.*` (`lib/cve-match.js:164-173`).
22
- 3. **H3** — Tier-3 `possible` émis pour toute collision d'`artifactId`, même quand le vendor est totalement étranger (`lib/cve-match.js:191-198`).
23
- 4. **H4** — Le filtre CPE (« primary FP filter ») se contente de **marquer** `cpeFiltered=true`, il ne supprime jamais (`lib/cpe.js:269-272`).
24
-
25
- Ces quatre interagissent de manière pathologique : H3 laisse passer une masse de matches `possible`, H2 en remonte beaucoup en `probable`, H1 valide toutes les versions, et H4 ne filtre plus rien en aval.
26
-
27
- ---
28
-
29
- ## A. Faux positifs (critère #1 demandé)
30
-
31
- ### HIGH
32
-
33
- #### H1 — `isVersionAffected` fail-open quand pas de bornes
34
- **Fichier :** `lib/maven-version.js:101-124`
35
-
36
- ```js
37
- if (spec.version && spec.version !== "0" && spec.version !== "*") { /* lower */ }
38
- if (spec.lessThan) { ... }
39
- if (spec.lessThanOrEqual) { ... }
40
- if (!spec.lessThan && !spec.lessThanOrEqual && spec.version && …) { /* exact */ }
41
- return true; // ← fall-through "affected"
42
- ```
43
-
44
- Empiriquement : `isVersionAffected("2.14", { status: "affected" })` → `true`. CVEProject contient régulièrement des entrées `versions:[{status:"affected"}]` sans bornes (stub mal formé). Combiné à H2, **toute dep qui hit le `byProduct` tier-2 avec un range sparse est flaggée sur toute version**.
45
-
46
- **FP concret :** CVE listant `affected: [{ product: "log4j", versions: [{ status: "affected" }] }]` → flag `log4j-core 2.99.0` (patché) car pas de borne haute, pas d'exact-match, fall-through → `true`.
47
-
48
- **Fix :**
49
- ```js
50
- if (!spec.version && !spec.lessThan && !spec.lessThanOrEqual) return false;
51
- ```
52
-
53
- ---
54
-
55
- #### H2 — `vendorMatchesGroup` substring-match → cross-vendor leak
56
- **Fichier :** `lib/cve-match.js:164-173`
57
-
58
- ```js
59
- if (g.includes(v) || v.includes(g)) return true;
60
- ```
61
-
62
- `vendorMatchesGroup("spring", "foospringbar")` → `true`. NVD/CVEProject contiennent régulièrement des vendors `"a"`, `"oss"`, `"com"`, `"java"`, `"go"`, `"web"`, `"open"`, `"ibm"`, `"net"` — tous présents en sous-chaîne dans des groupIds réels.
63
-
64
- **FP concrets :**
65
- - CVE `vendor="open"` matche `org.opensaml:opensaml-core` (`"opensaml".includes("open")`).
66
- - CVE `vendor="ibm"` matche `com.ibmcloudant:cloudant-client`.
67
- - CVE `vendor="spring"` matche `org.springframework.*` ET `com.foospringbar.*`.
68
-
69
- **Fix :** restreindre à (a) `g === v`, (b) `g.split(".").includes(v)`, (c) `v.includes(g)` seulement si `g.length >= 4`. Drop des branches substring non bornées.
70
-
71
- ---
72
-
73
- #### H3 — Tier-3 `possible` émis avec vendor totalement étranger
74
- **Fichier :** `lib/cve-match.js:191-198`
75
-
76
- ```js
77
- const productMatches = byProduct[dep.artifactId.toLowerCase()] || [];
78
- for (const e of productMatches) {
79
- if (vendorMatchesGroup(e.vendor, dep.groupId)) {
80
- all.push(...matchOne(dep, [e], "probable"));
81
- } else {
82
- all.push(...matchOne(dep, [e], "possible")); // ← émis quand même
83
- }
84
- }
85
- ```
86
-
87
- Toute dep dont l'`artifactId` collide avec un produit non relié produit un match `possible`. Collisions fréquentes : `core`, `common`, `api`, `client`, `utils`, `parser`, `web`. Une CVE avec `product="core"` flagge **toutes** les deps `*-core`.
88
-
89
- **Fix :** drop le tier `possible` ou ne l'afficher que si la refinement CPE (`refineMatchesWithCpe`) l'a confirmé.
90
-
91
- ---
92
-
93
- #### H4 — CPE refinement flagge sans supprimer
94
- **Fichier :** `lib/cpe.js:269-272`
95
-
96
- ```js
97
- if (!affected && rec.configurations.length) {
98
- m.cpeFiltered = true; // ← marqué, jamais supprimé
99
- }
100
- ```
101
-
102
- `cpeFiltered` est un soft tag. Si `lib/cve-report.js` ne strippe pas `cpeFiltered === true` (à vérifier dans le rendu), **les matches prouvés faux positifs restent dans la punch list**.
103
-
104
- De plus, le skip `if (!rec.configurations.length && !rec.cpes.length) continue;` (`cpe.js:266`) est fail-open : une CVE sans configurations NVD (CVE récente ou réservée) est laissée au tier que le matcher lui a donné.
105
-
106
- **Fix :** (a) filtrer par défaut `cpeFiltered=true` avec flag `--show-filtered`, ou (b) section séparée « likely false positive » dans le report.
107
-
108
- ---
109
-
110
- #### H5 — Le qualificateur CPE `update` (RC, alpha) est ignoré
111
- **Fichier :** `lib/cpe.js:81-100` (`matchVersionRange`)
112
-
113
- Le champ index 6 du CPE 2.3 (`update`) est parsé mais jamais comparé. NVD liste fréquemment les pré-releases via le champ update (`cpe:2.3:a:vendor:product:1.0.0:rc1:*…`). Le hard-pin compare littéralement la `version` ignorant `update` :
114
-
115
- ```js
116
- if (parsed.version && parsed.version !== "*" && parsed.version !== "-") {
117
- try { return compareMavenVersions(depVersion, parsed.version) === 0; }
118
- ```
119
-
120
- → `criteria: cpe:2.3:a:apache:foo:1.0.0:beta1:*…` matche `dep=1.0.0` (release). FP.
121
-
122
- **Fix :** si `update` n'est ni `*` ni `-`, concaténer (`${version}-${update}`) avant `compareMavenVersions` (qui gère déjà `1.0.0-beta1`).
123
-
124
- ### MEDIUM
125
-
126
- #### M1 — `compareMavenVersions` accepte du garbage sans broncher
127
- **Fichier :** `lib/maven-version.js:25-94`
128
-
129
- `compareMavenVersions("${foo}", "1.0")` → `-1`. Une variable Maven non résolue est comparée comme une vraie version. La pipeline filtre `/\$\{/.test(dep.version)` dans `expandWithTransitives`, `queryOsvForDeps`, `checkOutdatedDeps` — **mais pas dans `matchDepsAgainstCves`** (`cve-match.js:151-162`). Donc dep avec version `${foo}` → `isVersionAffected("${foo}", range)` → fall-through `true` (H1).
130
-
131
- **Fix :** skipper les deps avec `${…}` non résolu dans `matchDepsAgainstCves`.
132
-
133
- ---
134
-
135
- #### M2 — OSV : stubs cachés ré-émis sur la mauvaise version
136
- **Fichier :** `lib/osv.js:242-265` (`runMatches`)
137
-
138
- `vulnToMatch` ne refait pas de check range local. Le cache key `(g, a, v)` est versionné, donc pour la même dep+version c'est sûr. Mais le branch « stub only » émet `severity: "UNKNOWN"` sans description — vrai FP si la dep a été upgradée entre la build du cache et le scan.
139
-
140
- `dep.version` avec `1.0.0.Final` ou `1.0.0.RELEASE` est envoyée verbatim à OSV qui ne normalise pas → recall patchy (FN, pas FP).
141
-
142
- **Fix :** toujours évaluer `vuln.affected[].ranges` localement avant d'émettre. Drop des matches stub-only.
143
-
144
- ---
145
-
146
- #### M3 — `parseRange` exporté mais jamais appelé
147
- **Fichier :** `lib/maven-version.js:131-146`
148
-
149
- `parseRange` traite les ranges Maven (`[1.0,2.0)`) mais aucun caller. Une `<version>[1.0,2.0)</version>` est passée verbatim à `compareMavenVersions("[1.0,2.0)", spec.version)`. Le tokenizer split sur `[.\-]`, `[1` devient un segment string → ranking sub-release → typiquement FN, mais peut devenir FP via H1.
150
-
151
- **Fix :** câbler `parseRange` dans la pipeline ; sinon reporter ces deps en `unresolved-versions`.
152
-
153
- ---
154
-
155
- #### M4 — `cpeMatchesDep` ré-introduit le substring leak de H2
156
- **Fichier :** `lib/cpe.js:170-173`
157
-
158
- Le filtre CPE supposé *narrow* contient lui-même la même heuristique substring que H2. Après que H2 fait passer un match, ce leak l'empêche de le re-filtrer.
159
-
160
- ---
161
-
162
- #### M5 — retire.js passé sans validation locale
163
- **Fichier :** `lib/retire.js:148-193`
164
-
165
- Le wrapper a accès à `below`/`atOrAbove` mais ne les compare jamais. Les régressions historiques des signatures retire (matchs sur des bundles non reliés) passent. Pas d'équivalent à `--includeOsvData` / `--ignorefile`.
166
-
167
- retire émet aussi un CVE id synthétique `RETIRE-<component>-<version>` qui ne dédup jamais contre fad/osv/nvd → **même vuln rapportée 2 fois**.
168
-
169
- ---
170
-
171
- #### M6 — Cohérence des clés `byProduct`
172
- **Fichier :** `lib/cve-download.js:163-164` + `lib/cve-match.js:191`
173
-
174
- Index keyé sur `a.product` brut, lookup sur `.toLowerCase()`. Actuellement safe car `extractAffectedRanges` lowercase upstream — mais fragile. À documenter ou normaliser explicitement.
175
-
176
- ### LOW
177
-
178
- - **L1** — `data/known-obsolete.json:92-96` : `struts2-core` flaggé HIGH sans gate de version (Struts 2.5.30 patché remonte « obsolete »).
179
- - **L2** — `findCycleForVersion` (`lib/outdated.js:79-86`) prefix-match OK, pas de FP trouvé.
180
- - **L3** — Tests : aucun ne couvre les edge cases ci-dessus (no-bounds, substring vendor leak, possible tier, update CPE, garbage `${...}`, `parseRange`).
181
-
182
- ### Verdict faux positifs
183
-
184
- **Architecture saine, implémentation fail-open.** Les 4 cascades (H1+H2+H3+H4) doivent être patchées avant prod.
185
-
186
- Patches prioritaires (ordre) :
187
- 1. H1 → one-liner, plus gros gain
188
- 2. H2 → tightening de `vendorMatchesGroup`
189
- 3. H3 → cacher le tier `possible` par défaut
190
- 4. H4 → strip `cpeFiltered:true` du report par défaut
191
- 5. M1 → skip `${...}` dans `matchDepsAgainstCves`
192
- 6. M2 → re-eval local des OSV ranges
193
-
194
- ---
195
-
196
- ## B. Code reuse
197
-
198
- ### HIGH
199
-
200
- - **Helper de cache disque triplicé** : `lib/osv.js:25-44`, `lib/nvd.js:28-45`, `lib/retire.js:27-45`. Pattern identique (`_fetchedAt` + TTL + JSON). `lib/outdated.js:22-37` même chose avec shape différent.
201
- → Extraire `lib/cache-disk.js` exportant `makeCache(dir, ttlMs)`.
202
-
203
- - **`severityFromScore` + `SEVERITY_RANK` dupliqués 7+ fois** : `lib/nvd.js:64-71`, `lib/cve-download.js:82-89`, `lib/osv.js:60-66`, `lib/cve-match.js:9`, `lib/snyk.js:112`, `lib/cve-report.js:{634, 761, 1085, 1280}`, `fad-checker.js:657`.
204
- → `lib/severity.js` + `sortMatchesBySeverity()` helper.
205
-
206
- - **Merge-by-source logic dupliquée** : `fad-checker.js:628-665` (`mergeBySource`) vs `lib/snyk.js:97-120` (`mergeWithFadResults`). Le `"both"` de Snyk désaccorde avec le `"fad+osv"` ailleurs.
207
- → Promouvoir `mergeBySource` dans `lib/cve-match.js`.
208
-
209
- ### MEDIUM
210
-
211
- - **`depLabel` / `depToKey` réinventés 6 fois** : `fad-checker.js:551`, `lib/cpe.js:177-184`, `lib/osv.js:100-102`, `lib/cve-report.js:{648, 669, 773, 1296}`.
212
- - **Walker de répertoires + `SKIP_DIRS` dupliqués 4 fois** : `lib/core.js:9-37`, `lib/npm/parse.js:253-284`, `lib/npm/collect.js:202-222`, `lib/retire.js:77-81`.
213
- - **Deux parsers POM divergents** : `lib/core.js:49-117` vs `lib/transitive.js:111-158`. Légitime (besoins différents) mais extraction parent/coord en commun possible.
214
- - **CLI hand-roll argv parsing** : `fad-checker.js:25-150` walk de `process.argv` pour `--completion`, `--set-nvd-key`, etc. — devrait être des subcommands `commander`.
215
-
216
- ### LOW
217
-
218
- - 3 comparateurs de versions divergents : `compareMavenVersions`, `compareVersionsLoose` (`cve-report.js:705-715`), `semverCompare` (`npm/collect.js:25-37`).
219
- - `parseMavenMetadataLatest` (`outdated.js:212-224`) en regex au lieu d'xml2js (OK car XML trivial).
220
-
221
- ### Modules clean
222
- `maven-version.js`, `maven-repo.js`, `config.js`, `scan-completeness.js`, `cache-archive.js`.
223
-
224
- ---
225
-
226
- ## C. Code quality
227
-
228
- ### HIGH
229
-
230
- - **`fad-checker.js` (665 lignes) = god script** : pré-parse argv mélangé à l'orchestration, business logic dans la CLI.
231
- - **`lib/cve-report.js` (1455 lignes) = mega-module** : CSS inline (~200 lignes), JS inline (`TOGGLE_SCRIPT`), business logic (`buildFixRecommendations`, `versionJump`, `pickTopCriticalMatches`) et rendu HTML dans le même fichier. `RENDER_CTX` module-global (lignes 247-249) reset à chaque `buildBody` — comment auto-avoue le workaround.
232
- → Splitter en `lib/report/{css,html,recommendations,word}.js`. Passer `srcRoot` en paramètre.
233
- - **Stringly-typed partout** : `"fad" | "osv" | "nvd" | "snyk" | "retire"` en chaînes nues + class CSS `.source.snyk` qui couple le HTML à ces littéraux. Renommer une source casse silencieusement le CSS.
234
- → `lib/keys.js` + `SOURCES = Object.freeze({...})`.
235
-
236
- ### MEDIUM
237
-
238
- - **Param sprawl sur `writeReports` / `buildBody` / `renderExecutiveSummary`** — 10 champs disjoints recomposés à chaque appel.
239
- - **CVE matcher : duplication structurelle tier-2/tier-3** (`cve-match.js:175-219`) — flatten en single loop avec ternaire.
240
- - **Lockfile parsers v1 vs v2/v3 collés en un seul `parsePackageLock`** (`npm/parse.js:76-176`) — splitter.
241
- - **~30 `catch {}` silencieux** dans tout le code. Beaucoup légitimes, certains masquent des bugs réels (`osv.js:233`, `outdated.js:187,201`, `fad-checker.js:259`).
242
- - **`RENDER_CTX` global** → thread `srcRoot` via les signatures (3 niveaux max).
243
- - **Skip-dir lists copy-collées 4x** avec divergence `build/` (Maven keep, JS skip) — documenter une fois.
244
- - **`lib/core.js:230-235`** repeat parent-resolution dans `rewritePoms` — consolider.
245
-
246
- ### LOW
247
-
248
- - Cache helpers ré-implémentés (cf. reuse).
249
- - Commentaires narratifs sans valeur (`cve-report.js:404-406`, `cve-report.js:519-521`, `transitive.js:204-206`, `fad-checker.js:202`).
250
- - Pollution `byId` potentielle via `excludedById`/`missingById` (`core.js:240-285`) — appliquer le guard du CLAUDE.md.
251
- - `lib/osv.js:188` arithmétique d'index fragile (`(batchIdx - 1) * BATCH_SIZE + j`) — utiliser `i + j`.
252
- - Verbosité booléenne threadée 4+ niveaux → `lib/log.js`.
253
-
254
- ---
255
-
256
- ## D. Efficiency
257
-
258
- ### HIGH
259
-
260
- - **CVE index loadé eagerly** (`lib/cve-download.js:299-301`) : `JSON.parse(readFileSync)` synchrone même pour un projet de 3 deps. Charger async + lazy par bucket.
261
- - **`cpe.js` re-parse les CPE et walk 2 fois** (`cpe.js:194-245`) : `parseCpe23` appelé répétitivement, walk pour confidence après le walk d'évaluation. 2M+ string-splits sur un projet 800 deps × 50 matches.
262
- → Cache `m._parsed ||= parseCpe23(m.criteria)` + retour du match satisfaisant.
263
- - **NVD enrichment 100 % serial** (`nvd.js:186-203`) : `sleep(600|6000)` entre chaque appel. 200 CVEs uniques = 2 min avec clé, **20 min sans**. NIST policy = 50/30s window → permet parallélisme.
264
- - **TOCTOU `existsSync` + `readFile`** dans `transitive.js:52`, `nvd.js:34`, `osv.js:33`, `retire.js:32-37`, `outdated.js:22-25`. Doublé syscalls.
265
- - **`outdated.checkOutdatedDeps` cache reset global** (`outdated.js:230`) : `cache.entries = {}` si meta TTL périmé → 600 deps refetch même si entrées fraîches.
266
- → TTL par-entrée.
267
-
268
- ### MEDIUM
269
-
270
- - `findEolProduct` re-sort la prefix-list dans la boucle (`outdated.js:46-52`) — sort once at module load.
271
- - `outByKey` Map rebuild 6x dans `cve-report.js` (`:633-679`, `:760-795`, `:993-1003`) — builder once dans `buildBody`.
272
- - CVE bulk JSON `readFileSync` per file (`cve-download.js:274-287`) — borné par cache 24h, MEDIUM.
273
- - `cve-report.js` : `renderDetailPanel` (`:417-493`) + `groupExternalRefs` (`:340`) re-alloués par row → 45k allocations sur un report 5k rows. Pré-classifier à l'enrichment NVD.
274
- - **POMs parsés 2x sur le chemin `rewritePoms`** (`core.js:50` puis re-read à `:219`). `structuredClone` ou skip re-parse en read-only.
275
- - **`transitive.js:314` : `queue.shift()` O(n)** sur 4000+ transitives → 16M memmoves. Switch en cursor.
276
- - **Maven Central : pas de batch endpoint** (`outdated.js:176`) — solrsearch accepte OR boolean. 600 deps → 1 requête au lieu de 600.
277
- - **OSV detail fetch serial** dans `queryBatch` (`osv.js:218-236`) — `p-limit(10)` similaire à fad-checker.js.
278
-
279
- ### LOW
280
-
281
- - `getAllInheritedProps` rebuild via spread — fine en dessous de 10k POMs.
282
- - `xml2js` 3-5x plus lent que `fast-xml-parser` — hors scope.
283
- - Eager `require("chalk")` etc. — invisible.
284
-
285
- ### Note positive
286
- Le matcher CVE (`matchDepsAgainstCves`) est **Map-indexé correctement** (`byPackageName` / `byProduct` pré-bucketés) — pas de O(n²) sur le hot path.
287
-
288
- ---
289
-
290
- ## E. Recommandations priorisées
291
-
292
- | # | Action | Severity | Effort | Impact |
293
- |---|---|---|---|---|
294
- | 1 | Patch `isVersionAffected` fail-closed (H1) | HIGH FP | 1 ligne | Énorme |
295
- | 2 | Tighten `vendorMatchesGroup` substring (H2) | HIGH FP | 10 lignes | Énorme |
296
- | 3 | Drop ou hide tier `possible` (H3) | HIGH FP | 5 lignes | Énorme |
297
- | 4 | Strip `cpeFiltered:true` du report (H4) | HIGH FP | 3 lignes | Gros |
298
- | 5 | Handle CPE `update` qualifier (H5) | HIGH FP | 15 lignes | Moyen |
299
- | 6 | Skip `${...}` dans matcher (M1) | MED FP | 3 lignes | Moyen |
300
- | 7 | OSV ranges re-eval local (M2) | MED FP | 30 lignes | Moyen |
301
- | 8 | Extract `lib/severity.js` + `SEVERITY_RANK` | HIGH reuse | 1h | Maintenance |
302
- | 9 | Extract `lib/cache-disk.js` | HIGH reuse | 1h | Maintenance |
303
- | 10 | NVD enrichment parallèle (token-bucket) | HIGH perf | 30min | 10x speedup |
304
- | 11 | Batch Maven Central Solr (#13 efficiency) | HIGH perf | 1h | 100x speedup |
305
- | 12 | `parseCpe23` memoization (CPE perf) | HIGH perf | 10min | 2-3x speedup |
306
- | 13 | Splitter `cve-report.js` en sous-modules | HIGH quality | 2h | Maintenance |
307
- | 14 | Tests des edge cases FP (`update`, no-bounds, substring) | LOW | 2h | Filet de sécurité |
308
-
309
- **Quick wins (< 1 jour cumulé)** : #1, #2, #3, #4, #6, #12 → diviserait le taux de FP par ~3-5 et accélérerait le rendu de ~2-3×.
310
-
311
- ---
312
-
313
- ## F. Verdict final
314
-
315
- - **Architecture :** **bonne**. Séparation Maven/npm, 3-tier matching, post-CPE refinement, multi-source dedup, caches TTL — tout est en place.
316
- - **Implémentation matcher :** **fail-open systémique**. Les 4 cascades H1→H4 transforment un outil correctement architecturé en générateur de faux positifs.
317
- - **Performance :** correcte en CPU (Map-indexed), **mauvaise en I/O** (NVD serial, Solr unitaire, double-parse POM).
318
- - **Qualité :** 2 mega-modules (`cve-report.js` 1455 LoC, `fad-checker.js` 665 LoC) à splitter, ~30 `catch {}` à auditer, 7+ duplications de `SEVERITY_RANK`.
319
- - **Tests :** 96 tests existants mais happy-path-heavy ; aucune couverture des edge cases FP listés ci-dessus.
320
-
321
- **Recommandation pratique :** sans les patches H1-H4, le rapport doit être traité comme une **liste de départ pour triage manuel**, pas comme un inventaire de vulnérabilités exploitable. Le tier `exact` reste fiable. Tout le reste demande des yeux humains sur la description CVE avant action.
322
-
323
- ---
324
-
325
- ## Annexe — Références fichier:ligne
326
-
327
- **Faux positifs :**
328
- - `lib/maven-version.js:25-94, 101-124, 131-146`
329
- - `lib/cve-match.js:9, 99-109, 151-162, 164-173, 175-219, 191-198`
330
- - `lib/cpe.js:25, 36-68, 81-100, 161-184, 194-245, 266, 269-272`
331
- - `lib/osv.js:25-44, 60-66, 100-102, 188, 218-236, 242-265, 273`
332
- - `lib/nvd.js:28-45, 64-71, 145, 186-203`
333
- - `lib/retire.js:24-45, 77-81, 148-193`
334
- - `lib/outdated.js:22-37, 46-52, 79-86, 168-187, 212-254`
335
- - `lib/cve-download.js:82-89, 163-164, 274-301`
336
- - `lib/transitive.js:51-101, 111-158, 204-235, 273-320`
337
- - `lib/core.js:9-37, 49-149, 219-285`
338
- - `lib/npm/{parse.js:76-291, collect.js:25-224}`
339
- - `lib/cve-report.js:48-247, 340, 404-493, 519-521, 559, 633-1455`
340
- - `fad-checker.js:25-150, 202, 259, 551, 628-665`
341
- - `data/{cpe-coord-map.json, known-obsolete.json:92-96}`
342
-
343
- **Tests manquants :** `test/{cpe,cve-match,maven-version}.test.js` — ajouter edge cases `update`, no-bounds, substring vendor leak, garbage version.