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 +17 -14
- package/lib/cve-match.js +9 -5
- package/package.json +1 -1
- package/test/cve-match.test.js +13 -0
- package/CRITICAL-REVIEW.md +0 -343
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
|
-
//
|
|
545
|
-
//
|
|
546
|
-
|
|
547
|
-
const
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
const
|
|
552
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
563
|
-
console.log(chalk.bold.cyan(`\n 2. CVE in dev dependencies (${
|
|
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
|
-
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
//
|
|
173
|
-
|
|
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.
|
|
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>",
|
package/test/cve-match.test.js
CHANGED
|
@@ -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.
|
package/CRITICAL-REVIEW.md
DELETED
|
@@ -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.
|