fad-checker 2.4.1 → 2.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,44 @@ This project adheres to [Semantic Versioning](https://semver.org/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ### Fixed
9
+ - **CVE-index recall: real-world pre-2023 records were dropped (incl. Log4Shell).**
10
+ `isMavenRelevant()` only accepted CVE 5.x records carrying machine-readable Maven
11
+ metadata (`packageName`/`collectionURL`/`versionType:"maven"`) or an EXACT-match
12
+ known vendor — but CNAs publish legal-entity vendors ("Apache Software Foundation")
13
+ and display products ("Apache Log4j2"), so **CVE-2021-44228 was absent from the
14
+ index** and an offline scan of `log4j-core:2.14.0` reported no Log4Shell. The filter
15
+ now tokenises vendors, strips leading vendor words from products, and consults the
16
+ curated `data/cpe-coord-map.json` — which also **backfills `packageName`** on
17
+ product-only records so they match at tier-1. `versions[].changes[]` timelines
18
+ (how 44228 encodes its affected windows) are expanded into plain affected windows,
19
+ placeholder bounds (`lessThan:"log4j-core*"`, `"*"`, `"unspecified"`) no longer
20
+ poison comparisons (fail-closed preserved), and `fixVersion` picks the **highest**
21
+ version-like upper bound instead of the first (multi-branch advisories suggested a
22
+ downgrade). Rebuilt index: **6 589 → 15 236 CVE (+131 %)**. Regression-tested against
23
+ the real cvelistV5 record (`test/fixtures/cve-samples/cve-2021-44228-real.json`).
24
+ - **OSV offline: warmed cache older than the 12 h TTL was silently discarded.**
25
+ `--offline` now bypasses the OSV cache TTL (same rule as the NVD cache): on an
26
+ air-gapped box the warmed cache is the only source, and expiring it reported
27
+ "0 OSV vulns" for every ecosystem. Online behaviour is unchanged.
28
+ - **Go: `replace` directives are now applied** (module→module replaces rewrite the
29
+ scanned coordinate — a `replace` downgrade was invisible; directory replaces are
30
+ dropped with a chapter-0 `local-replace` warning), and **pre-1.17 modules merge the
31
+ `go.sum` graph** (their `go.mod` lists direct deps only — transitives were skipped).
32
+ - **PyPI: `pip-compile --generate-hashes` output parsed correctly.** The trailing
33
+ `\` of hash-pinned lines (and inline ` --hash=…` options) made every dep of such a
34
+ requirements file silently skipped. `uv.lock` no longer inventories the project's
35
+ own virtual/editable package. Same-name deps pinned to different versions across
36
+ files now ALL land in `versions[]` (every distinct version scanned, as Maven does).
37
+ - **NuGet: `Directory.Packages.props` is resolved by walking UP the tree** (MSBuild
38
+ semantics — nearest wins). Before, only the csproj's own directory was searched, so
39
+ a root-level CPM solution collected **zero** deps. Also: `VersionOverride` support,
40
+ exact-range pins `[1.2.3]` accepted as concrete, distinct resolved versions across
41
+ projects/TFMs all scanned, and **paged registration indexes** (Newtonsoft.Json-class
42
+ packages) have the needed pages fetched instead of returning empty findings.
43
+ - **Registry caches (go/pypi/nuget): offline runs no longer re-stamp cache freshness**
44
+ (a stale cache would then look fresh to the next online run and skip its refetch).
45
+
8
46
  ### Changed
9
47
  - **Report chapters reorganised into a two-level hierarchy.** Related chapters are now
10
48
  grouped under six **root chapters**, each whose header carries a breakdown count:
@@ -18,6 +56,20 @@ This project adheres to [Semantic Versioning](https://semver.org/).
18
56
  (roots + indented sub-chapters).
19
57
 
20
58
  ### Added
59
+ - **Certificate & key-material scanner (report chapter 2.4).** A new standalone scanner
60
+ (`lib/certs/`, on by default, `--no-certs` to disable) walks the source tree for
61
+ committed cryptographic material and surfaces it in a dedicated report chapter, the
62
+ JSON export (`certificates` array + `summary.certificates`/`certPrivateKeys`) and SARIF
63
+ (`FAD-*` rules). It detects **X.509 certificates** (PEM/DER, parsed with Node's built-in
64
+ `crypto.X509Certificate`) and flags **expired**, **expiring** (within `--cert-expiry-days`,
65
+ default 90), **weak key** (RSA<2048 / weak EC curve), **weak signature** (MD5/SHA1) and
66
+ **self-signed**; **private & public keys** — every key explicitly labelled **private**
67
+ (a committed secret → critical) or **public** (low) — across PEM (PKCS#1/8/SEC1),
68
+ **OpenSSH of every algorithm** (RSA/DSA/ECDSA/Ed25519 incl. FIDO `-sk`), PuTTY `.ppk`,
69
+ PGP and one-line SSH (`*.pub`, `authorized_keys`, `known_hosts`); and **keystores**
70
+ (JKS/JCEKS by magic byte, PKCS#12 by extension). Detection is by extension **and**
71
+ conventional SSH filename. **100% offline** — no network, no decryption — inventory-only
72
+ (does not affect the `--fail-on` gate).
21
73
  - **Scan-provenance manifest + Methodology chapter (audit reproducibility).** Every
22
74
  report now carries a provenance manifest — tool version, run mode (offline/online),
23
75
  the findings-affecting configuration, and the **freshness of every data source**
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  > **F**abulous **A**utonomous **D**ependency **C**hecker<br>
9
9
  > AKA **F**uckin' **A**utonomous **D**ependency **C**hecker<br>
10
10
 
11
- `fad-checker` audits **Maven · Gradle · npm · Yarn · pnpm · Composer · PyPI · NuGet · Go · Ruby**, vendored JavaScript and committed native binaries in any source tree — multi-module, monorepo, polyglot — and produces a self-contained **HTML + Word report** (CVE prioritised by EPSS + CISA KEV, EOL, obsolete, outdated, licenses) plus **CycloneDX SBOM / CSAF VEX / SARIF / JSON** exports. **No build tools, no Docker, no network needed** — it reads lockfiles and manifests straight off disk.
11
+ `fad-checker` audits **Maven · Gradle · npm · Yarn · pnpm · Composer · PyPI · NuGet · Go · Ruby**, vendored JavaScript, committed native binaries and cryptographic material (certificates & private/public keys) in any source tree — multi-module, monorepo, polyglot — and produces a self-contained **HTML + Word report** (CVE prioritised by EPSS + CISA KEV, EOL, obsolete, outdated, licenses) plus **CycloneDX SBOM / CSAF VEX / SARIF / JSON** exports. **No build tools, no Docker, no network needed** — it reads lockfiles and manifests straight off disk.
12
12
 
13
13
  🌐 **[Project site & docs →](https://9pings.github.io/fad-checker/)**
14
14
 
@@ -20,6 +20,7 @@
20
20
  ## Features
21
21
 
22
22
  - **10 ecosystems in one pass** — Maven, **Gradle**, npm/Yarn/pnpm, Composer (PHP), PyPI, NuGet, Go, Ruby; plus **vendored JS** (retire.js), committed **native binaries** (`.dll`/`.exe`/`.so`/`.dylib`, identified by checksum via deps.dev + CIRCL) and **embedded JARs** (fat-jars/war/ear, unzipped in-memory).
23
+ - **Crypto material** — committed **certificates** (X.509, PEM/DER), **private &amp; public keys** (PEM / OpenSSH every algorithm / PuTTY / PGP / one-line SSH) and **keystores** (JKS/JCEKS/PKCS#12). Each key is labelled **private** (a committed secret → critical) or **public**; certs are checked for **expiry, weak key (RSA<2048), weak signature (MD5/SHA1) and self-signed** — all parsed offline with the built-in X.509 parser, no network. `--no-certs` to disable.
23
24
  - **No build tools** — reads `pom.xml`, `build.gradle(.kts)`/`gradle.lockfile`/`libs.versions.toml`, `package-lock`/`yarn.lock`/`pnpm-lock`, `composer.lock`, `poetry`/`Pipfile`/`uv`/`pdm` locks, `packages.lock.json`/`*.csproj`, `go.mod`, `Gemfile.lock` directly. No `mvn`/`gradle`/`npm install`/`pip`/`dotnet restore`/`go build`/`bundle`, no `node_modules/`. → [how it stays build-free](docs/COMPARISON.md#how-its-autonomous-no-build-tools)
24
25
  - **CVE, merged & prioritised** — CVEProject + OSV.dev + NVD, CPE/version cross-checked to cut false positives, ranked **CISA KEV → EPSS → CVSS**.
25
26
  - **Per-module Maven version mediation** — recovers vulnerable transitive versions that a global `<dependencyManagement>` pin hides in another module (lifted Snyk-corroborated coverage **156 → 181** on a real 25-module reactor, finding CVEs a single Snyk scan missed).
@@ -62,7 +63,7 @@ The report is organised into **root chapters** (each grouping related sub-chapte
62
63
  | **0. Warnings** *(top)* | local heuristics | Missing lockfiles, unresolved Maven versions (BOM-managed), private libs not on Maven Central |
63
64
  | **Δ. Changes since baseline** *(top, with `--baseline`)* | diff vs prior JSON | New / fixed / unchanged findings per category + the list of **new production CVEs** — for repeat audits and `--fail-on-new` CI gating |
64
65
  | **1. CVE** *(X direct, Y indirect, Z dev)* | CVEProject + OSV.dev + NVD + CPE | **1.1 Production** — public CVE / GHSA in prod deps, per ecosystem, per manifest, **prioritised** by CISA KEV + EPSS + CVSS · **1.2 Vendored JS vulns** ([retire.js](https://retirejs.github.io/)) · **1.3 Dev** (`test`/`provided`, `dev`/`optional`/`peer`) · **1.4 Likely false positives** (CPE-filtered) |
65
- | **2. Unmanaged / unversioned components** | deps.dev + CIRCL (by checksum), retire.js | **2.1 Embedded binaries** — CVEs in libs shipped inside committed `.jar`/`.war`/`.ear` (fat-jars, shaded uber-jars) · **2.2 Native binaries** (`.dll`/`.exe`/`.so`/`.dylib`) identified by hash, flagged should-be-managed / name≠checksum / unknown / malicious · **2.3 Vendored JavaScript** inventory (jQuery, Bootstrap, …) vulnerable *or not* |
66
+ | **2. Unmanaged / unversioned components** | deps.dev + CIRCL (by checksum), retire.js, built-in X.509 | **2.1 Embedded binaries** — CVEs in libs shipped inside committed `.jar`/`.war`/`.ear` (fat-jars, shaded uber-jars) · **2.2 Native binaries** (`.dll`/`.exe`/`.so`/`.dylib`) identified by hash, flagged should-be-managed / name≠checksum / unknown / malicious · **2.3 Vendored JavaScript** inventory (jQuery, Bootstrap, …) vulnerable *or not* · **2.4 Certificates &amp; key material** — committed certs (expiry / weak key / weak signature / self-signed), **private vs public keys** (PEM/OpenSSH/PuTTY/PGP/SSH) and keystores, all parsed offline |
66
67
  | **3. Maintenance / lifecycle** *(X EOL, Y obsolete, Z outdated)* | endoflife.date · curated + registry flags · Maven Central / npm / Packagist / PyPI / NuGet | **3.1 End-of-Life** frameworks · **3.2 Obsolete / deprecated / abandoned / yanked** · **3.3 Outdated** (newer version available, with release dates) |
67
68
  | **4. Licenses** *(opt-in: `--licenses`)* | registry metadata + Maven POMs → SPDX policy | Each dep's license normalised to SPDX and classified; copyleft (GPL/AGPL/LGPL/MPL), proprietary and unknown flagged for review |
68
69
  | **5. Fix Recommendations** | computed | Per-ecosystem pin recipes: Maven `<dependencyManagement>`, Gradle `constraints { }`, npm `overrides`, yarn `resolutions`, `composer require`, `pip install`, `dotnet add package` |
@@ -0,0 +1,190 @@
1
+ # Revue de fiabilité — fad-checker v2.4.1 — 2026-07-02
2
+
3
+ > Revue approfondie axée **bugs de correction** (faux négatifs/positifs) et **mises à jour à faire**,
4
+ > avec focus demandé sur **Go, Python (PyPI) et C# (NuGet)**. Chaque bug listé ci-dessous a été
5
+ > **confirmé par reproduction** (test ou exécution réelle), pas seulement par lecture de code.
6
+ > Les correctifs ont été appliqués dans la foulée (commits séparés par sujet) avec un test de
7
+ > régression chacun. Suite de tests : **569 → 587, toutes vertes**.
8
+
9
+ ---
10
+
11
+ ## 1. Verdict global
12
+
13
+ L'architecture est saine (codecs isolés, matching multi-sources avec dédup, gate pure, caches
14
+ offline-aware) et la couverture de test est réelle. **Mais la revue a trouvé un défaut critique
15
+ en production** : l'index CVE local ne contenait pas Log4Shell — un audit air-gapped d'un projet
16
+ Java livrait un rapport propre sur `log4j-core:2.14.0`. Ce défaut était détectable : le test
17
+ `cli-safety` échouait déjà (568/569) au début de cette revue. Les trois écosystèmes sur lesquels
18
+ tu avais des doutes (Go/PyPI/NuGet) avaient chacun **au moins un trou de collecte réel** en
19
+ configuration entreprise courante (CPM .NET, pip-compile hashé, `replace` Go). Tous corrigés.
20
+
21
+ **Après correctifs : l'outil est fiable pour un usage d'audit professionnel**, avec les limites
22
+ résiduelles documentées en §4 (aucune n'est silencieuse : elles sur-rapportent ou sont hors
23
+ périmètre annoncé).
24
+
25
+ ---
26
+
27
+ ## 2. Bugs confirmés et corrigés
28
+
29
+ | # | Gravité | Composant | Bug | Effet en audit |
30
+ |---|---------|-----------|-----|----------------|
31
+ | B1 | **CRITIQUE** | `lib/cve-download.js` | Index CVE : les enregistrements pré-2023 sans métadonnées Maven machine-réadables étaient exclus — **Log4Shell absent de l'index** | Faux négatif majeur en mode offline/air-gapped (l'argument de vente n°1 de l'outil) |
32
+ | B2 | **HAUTE** | `lib/osv.js` | Cache OSV : TTL 12 h appliqué **même en `--offline`** | Machine air-gapped scannée >12 h après warm → « 0 vulns OSV » silencieux, tous écosystèmes |
33
+ | B3 | **HAUTE** | `lib/codecs/nuget.codec.js` | `Directory.Packages.props` cherché uniquement dans le dossier du `.csproj` (jamais en remontant) | Solution .NET en Central Package Management (le standard moderne) → **0 dep collectée** |
34
+ | B4 | **HAUTE** | `lib/codecs/pypi/parse.js` | `pkg==1.0 \` (continuation `pip-compile --generate-hashes`) et ` --hash=…` inline rejetés par `isPinned()` | Fichier requirements hash-pinné (pratique des shops sécurisés…) → **0 dep collectée** |
35
+ | B5 | MOYENNE+ | `lib/codecs/go/parse.js` | Directives `replace` ignorées | Un `replace` qui downgrade/redirige un module → version effective non scannée (FN) ; version remplacée scannée à tort (FP) |
36
+ | B6 | MOYENNE | `lib/codecs/go.codec.js` | `go.sum` jamais consulté dès que `go.mod` a des `require` | Module `go < 1.17` (liste seulement les directs) → **transitifs invisibles** |
37
+ | B7 | MOYENNE | `lib/codecs/nuget/registry.js` | Index de registration **paginé** (paquets populaires : Newtonsoft.Json, Serilog…) : pages `@id` jamais fetchées | Deprecation + outdated silencieusement vides pour les paquets les plus courants |
38
+ | B8 | MOYENNE | `pypi.codec.js` / `nuget.codec.js` | Même paquet pinné à des versions différentes dans 2 fichiers/projets → seule une version scannée (`out.set` écrase) | FN si la version la plus vieille est la vulnérable — violait la convention Maven « every distinct version is scanned » |
39
+ | B9 | MOYENNE | `lib/maven-version.js` | Bornes placeholder (`lessThan:"log4j-core*"`, `"*"`, `"unspecified"`) comparées comme des versions Maven | Comparaison garbage → ranges jamais matchés (FN) ; fail-open possible sur `{version:"*", lessThan:"*"}` |
40
+ | B10 | BASSE+ | `lib/codecs/nuget/parse.js` | Pin exact NuGet `[1.2.3]` rejeté comme « range » ; `VersionOverride` (CPM) ignoré | Deps skippées ou scannées à la mauvaise version |
41
+ | B11 | BASSE | `lib/cve-download.js` | `fixVersion` = **premier** `lessThan` rencontré | Advisory multi-branche (struts 2.3.32 / 2.5.10.1) → suggestion de « fix » plus vieille que la version courante |
42
+ | B12 | BASSE | `lib/codecs/pypi/parse.js` | `uv.lock` : le paquet racine du projet (`source.virtual`/`editable`) inventorié comme dep | Bruit ; collision possible avec un paquet PyPI homonyme |
43
+ | B13 | BASSE | registres go/pypi/nuget | Run offline ré-écrivait `meta.fetchedAt` du cache | Cache périmé considéré frais au run online suivant → refresh sauté |
44
+
45
+ ### B1 en détail (le plus important)
46
+
47
+ Le vrai enregistrement cvelistV5 de **CVE-2021-44228** n'a **ni** `packageName`, **ni**
48
+ `collectionURL`, **ni** `versionType:"maven"` ; son vendor est `"Apache Software Foundation"`
49
+ (entité légale, pas le token `apache` attendu en match exact), son produit `"Apache Log4j2"`
50
+ (le heuristique produit était ancré `^log4j`), sa borne `lessThan` est le placeholder
51
+ `"log4j-core*"` et ses fenêtres affectées sont encodées en `versions[].changes[]` (timeline
52
+ CVE 5.x) — quatre caractéristiques que le pipeline ignorait toutes. La fixture de test
53
+ existante (`cve-with-packagename.json`) était une version **synthétique idéalisée** du même CVE
54
+ avec un `packageName` propre : le test passait, le réel échouait.
55
+
56
+ **Correctif** (4 volets) :
57
+ 1. `isMavenRelevant()` tokenise le vendor (`"apache software foundation"` → `apache` ✓) et
58
+ teste le produit aussi après suppression du mot vendor (`"apache log4j2"` → `log4j2`) ;
59
+ 2. la map curée `data/cpe-coord-map.json` est consultée au build de l'index et **backfille
60
+ `packageName`** sur les enregistrements product-only → match **tier-1 exact** (entrée
61
+ `apache:log4j2` ajoutée) ;
62
+ 3. `changes[]` est expansé en fenêtres affected simples — `[2.0-beta9,2.3.1) ∪ [2.4,2.12.2) ∪
63
+ [2.13.0,2.15.0)` — donc **2.14.0 matche et les backports corrigés 2.12.2/2.3.1 ne matchent
64
+ pas** ;
65
+ 4. `isVersionAffected()` ignore les bornes non-versions (fail-closed préservé : un spec fait
66
+ uniquement de placeholders ne matche jamais).
67
+
68
+ **Mesure** : index reconstruit **6 589 → 15 236 CVE (+131 %)** ; `log4j-core` passe de 4 entrées
69
+ (toutes 2025-2026) à 12 (dont 44228/45046/44832/45105, 2019-17571…). Le scan de la fixture
70
+ polyglot remonte désormais 3 CRITICAL au lieu de 0, et `--fail-on high` sort en exit 1.
71
+ Test de régression sur **l'enregistrement réel** committé en fixture.
72
+
73
+ > ⚠️ **Action requise sur tes machines d'audit** : l'index en cache (`~/.fad-checker/cve-data/`)
74
+ > reste amputé tant qu'il n'est pas reconstruit avec le code corrigé (fait sur cette machine :
75
+ > rebuild du 2026-07-02). **Re-exporte aussi les archives `--export-cache`** utilisées pour les
76
+ > audits air-gapped — celles générées avant ce fix embarquent l'index défectueux.
77
+
78
+ ---
79
+
80
+ ## 3. Fiabilité par écosystème (état après correctifs)
81
+
82
+ | Écosystème | Verdict | Notes |
83
+ |---|---|---|
84
+ | **Maven / Gradle** | ✅ Solide | Résolution transitive offline + overlay par module : différenciant réel. Rappel CVE-index legacy dépend désormais de la map curée (§4.9) — OSV reste le filet principal online |
85
+ | **Go** | ✅ Bon | `replace` appliqués, go.sum mergé pré-1.17, encodage casse proxy correct. Reste : stdlib non scannée (§4.1) |
86
+ | **PyPI** | ✅ Bon | PEP 503 propre partout, hash-pins parsés, multi-versions scannées. Matching de versions délégué à OSV côté serveur (PEP 440 correct par construction) |
87
+ | **NuGet** | ✅ Bon | CPM walk-up, `VersionOverride`, pins exacts, registrations paginées. Reste : `Directory.Build.props` (§4.5) |
88
+ | **npm/yarn/pnpm** | ✅ (non ré-audité en profondeur cette passe) | Lane la plus mature (retire.js + registry + OSV) |
89
+ | **Composer / Ruby** | ⚠️ Non ré-audités en profondeur cette passe | Même architecture que pypi/nuget — les patterns corrigés (multi-versions, fetchedAt offline) méritent une passe de vérification |
90
+
91
+ ---
92
+
93
+ ## 4. Limites restantes (non corrigées — connues et assumées)
94
+
95
+ Aucune n'est un faux négatif *silencieux* ; elles sur-rapportent, dégradent une lane secondaire,
96
+ ou sont hors périmètre annoncé. Par priorité décroissante :
97
+
98
+ 1. **Go stdlib non scannée** : la directive `go 1.x`/`toolchain` n'émet pas de dep `stdlib`
99
+ (OSV la couvre, ecosystem `Go`, package `stdlib`). Un CVE net/http Go n'apparaîtra pas.
100
+ *Complément recommandé : `govulncheck` sur les projets Go, ou émettre un record `stdlib`.*
101
+ 2. **OSV NuGet et la casse** : OSV matche les noms de paquets de façon sensible à la casse ;
102
+ un lockfile écrivant `newtonsoft.json` (rare — la casse canonique est la norme) pourrait
103
+ rater l'advisory. Canonicalisation via le registry envisageable.
104
+ 3. **uv.lock : pas de classification dev** — tout est marqué prod (sur-rapporte, direction
105
+ sûre pour un audit). Poetry : les groupes custom non-dev sont classés dev (sous-gate
106
+ potentiel pour `--fail-on` sur ces groupes).
107
+ 4. **PEP 735 `[dependency-groups]`** (pyproject) non parsé (fallback best-effort uniquement).
108
+ 5. **NuGet** : `Directory.Build.props` (items partagés), `GlobalPackageReference`, attribut
109
+ `Update`, et `developmentDependency` de packages.config non pris en compte.
110
+ 6. **CVSS v4** : score non calculé depuis le vecteur (v3.0/3.1 seulement) ; la sévérité retombe
111
+ sur `database_specific.severity` (GHSA la fournit presque toujours) — impact faible.
112
+ 7. **Comparateurs `cmp()` naïfs** dans les registries go/pypi/nuget (lane « outdated »
113
+ uniquement — jamais utilisés pour le matching de vulnérabilités) : pré-releases mal
114
+ ordonnées possibles dans la colonne outdated.
115
+ 8. **Tier-2 `byProduct`** : la clé reste le produit brut du CNA (`"apache log4j2"`), donc ce
116
+ tier ne matche que si produit == artifactId. Le vrai chemin pour les CVE legacy est le
117
+ backfill par map curée (B1) : **la qualité du rappel legacy est proportionnelle à la
118
+ curation de `data/cpe-coord-map.json`** — l'enrichir en continu (candidats : jenkins,
119
+ elasticsearch, kafka, activemq, camel, cxf, solr, weblogic/websphere…).
120
+ 9. **Test `cli-safety` dépendant du cache HOME** : il scanne avec le vrai `~/.fad-checker` —
121
+ c'est CE test qui a détecté l'index amputé (précieux), mais il échouera sur une machine au
122
+ cache froid. Assumé ou à isoler (fixture d'index dédiée) — au choix.
123
+ 10. **`known-obsolete.json` / `popular-packages.json`** : listes curées à la main — prévoir un
124
+ rafraîchissement périodique (les cibles typosquat vieillissent vite).
125
+
126
+ ---
127
+
128
+ ## 5. Mises à jour à faire (état au 2026-07-02)
129
+
130
+ ### Dépendances de l'outil (`npm outdated`)
131
+ | Paquet | Actuel | Dispo | Action |
132
+ |---|---|---|---|
133
+ | retire | 5.4.2 | **5.4.3** | patch sûr, à prendre (scanner JS vendored) |
134
+ | commander | 14.0.1 | 14.0.3 / 15.0.0 | prendre 14.0.3 ; major 15 à évaluer |
135
+ | js-yaml | 4.1.1 | 4.3.0 / 5.2.1 | prendre 4.3.0 ; major 5 à évaluer |
136
+ | smol-toml | 1.6.1 | 1.7.0 | mineur sûr |
137
+ | rimraf | 6.0.1 | 6.1.3 | mineur sûr |
138
+ | chalk / p-limit | 4.1.2 / 3.1.0 | 5.x / 7.x | **ESM-only** → rester en 4.x/3.x tant que le build CJS+bun est la cible (choix actuel correct) |
139
+
140
+ Aucune CVE connue sur les versions pinnées (vérifié via la lane npm du repo lui-même).
141
+
142
+ ### APIs externes
143
+ - **endoflife.date** : l'ancien endpoint `/api/<slug>.json` répond encore (HTTP 200, vérifié
144
+ aujourd'hui) mais la v1 (`/api/v1/products/<slug>`) est la cible long-terme → prévoir la
145
+ migration du fetch dans `lib/outdated.js` (champ de réponse différent).
146
+ - NVD 2.0, OSV v1, EPSS, KEV, deps.dev v3, CIRCL, proxys de registries : inchangés, rien à faire.
147
+ - **CVEProject cvelistV5** : le format `.zip.zip` imbriqué est toujours servi (vérifié via le
148
+ rebuild d'aujourd'hui — 362 782 fichiers scannés).
149
+
150
+ ### Caches d'audit
151
+ - **Reconstruire l'index CVE** sur chaque machine (`--force` du download, ou attendre
152
+ l'expiration 24 h online). Fait sur cette machine.
153
+ - **Régénérer les archives `--export-cache`** destinées aux postes air-gapped (elles
154
+ contiennent l'index pré-fix).
155
+
156
+ ---
157
+
158
+ ## 6. Avis sur la pertinence de l'outil
159
+
160
+ **L'outil a une vraie raison d'être.** Sa niche — audit multi-écosystème **entièrement
161
+ offline/air-gapped** avec résolution transitive Maven réelle, exports normalisés
162
+ (SBOM/VEX/SARIF), gate CI et rapport livrable client — n'est couverte par aucun outil unique du
163
+ marché : OSV-Scanner et Trivy dégradent fortement hors-ligne (64/202 findings vs 181/202 mesurés
164
+ sur le projet de référence), Dependency-Check est Java-centrique et lent, Snyk exige le cloud.
165
+ Pour des audits chez des clients à contraintes réseau (banque, défense, industriel), c'est un
166
+ différenciant réel. Les plus : provenance/checksums des rapports (reproductibilité d'audit),
167
+ chapitre méthodologie, triage VEX/ignore, priorisation KEV/EPSS — c'est le bon jeu de features
168
+ pour du livrable d'audit professionnel.
169
+
170
+ **Deux réserves à garder en tête** :
171
+ 1. **La fiabilité repose sur des données warmées** — cette revue le prouve : un index mal
172
+ construit a produit des rapports faussement propres pendant ~2 semaines sans aucun signal
173
+ visible en ligne (OSV masquait le trou). Recommandation structurelle : ajouter un
174
+ **canari d'auto-validation** au build de l'index (ex. vérifier que CVE-2021-44228,
175
+ CVE-2017-5638 et 2-3 autres sentinelles sont présentes et matchables, sinon échec bruyant
176
+ du build). C'est le correctif systémique qui manque encore.
177
+ 2. **Le coût de maintenance est réel** : 10 codecs, ~15 sources de données, des formats de
178
+ lockfiles qui évoluent (uv, pnpm v9, PEP 735…). L'outil est bien architecturé pour ça
179
+ (ajouter un codec = un dossier), mais il faut budgéter une passe de vérification
180
+ trimestrielle du type de celle-ci.
181
+
182
+ En l'état, après cette passe : **adapté à un usage d'audit professionnel**, à condition de
183
+ reconstruire les caches (cf. §5) et d'ajouter le canari de l'index en prochaine itération.
184
+
185
+ ---
186
+
187
+ *Revue et correctifs : session Claude (Fable 5) du 2026-07-02. 13 bugs corrigés, 18 tests de
188
+ régression ajoutés, suite 587/587 verte. Le scanner de certificats (chapitre 2.4) présent en
189
+ non-commité dans l'arbre de travail n'a pas été audité en profondeur dans cette passe (ses 19
190
+ tests passent) et reste non-commité.*
@@ -6,7 +6,7 @@ _fad_check_complete() {
6
6
  COMPREPLY=()
7
7
  cur="${COMP_WORDS[COMP_CWORD]}"
8
8
  prev="${COMP_WORDS[COMP_CWORD-1]}"
9
- opts="--src --target --exclude --verbose --no-report --no-transitive --no-all-libs --no-osv --no-nvd --no-epss --no-kev --licenses --no-retire --retire-refresh --report-output --report-html --report-doc --report-sbom --report-csaf --report-json --report-sarif --fail-on --fail-on-new --baseline --no-checksums --ignore --vex --ignore-test --cve-refresh --cve-offline --snyk --transitive-depth --offline --ecosystem --no-maven --no-npm --no-yarn --no-nuget --no-composer --no-pypi --no-go --no-ruby --no-jars --no-js --set-nvd-key --show-config --completion --help --version -s -t -e -v"
9
+ opts="--src --target --exclude --verbose --no-report --no-transitive --no-all-libs --no-osv --no-nvd --no-epss --no-kev --licenses --no-retire --retire-refresh --report-output --report-html --report-doc --report-sbom --report-csaf --report-json --report-sarif --fail-on --fail-on-new --baseline --no-checksums --ignore --vex --ignore-test --cve-refresh --cve-offline --snyk --transitive-depth --offline --ecosystem --no-maven --no-npm --no-yarn --no-nuget --no-composer --no-pypi --no-go --no-ruby --no-jars --no-binaries --no-certs --cert-expiry-days --no-js --set-nvd-key --show-config --completion --help --version -s -t -e -v"
10
10
  case "$prev" in
11
11
  --src|-s|--target|-t|--report-output)
12
12
  COMPREPLY=( $(compgen -d -- "$cur") )
@@ -24,6 +24,9 @@ _fad_check() {
24
24
  '--report-sarif[write SARIF 2.1.0 log]::file:_files'
25
25
  '--no-report[write no output files (gate-only)]'
26
26
  '--no-jars[skip embedded .jar/.war/.ear scanning]'
27
+ '--no-binaries[skip committed native-binary scanning]'
28
+ '--no-certs[skip certificate / key-material scanning]'
29
+ '--cert-expiry-days[warn on certs expiring within N days (default 90)]:days:'
27
30
  '--no-go[skip the Go codec]'
28
31
  '--no-ruby[skip the Ruby codec]'
29
32
  '--fail-on[CI gate level]:level:(none low medium high critical kev)'
@@ -3,6 +3,7 @@
3
3
 
4
4
  "byVendorProduct": {
5
5
  "apache:log4j": ["log4j:log4j", "org.apache.logging.log4j:log4j-core", "org.apache.logging.log4j:log4j-api"],
6
+ "apache:log4j2": ["org.apache.logging.log4j:log4j-core"],
6
7
  "apache:log4j-core": ["org.apache.logging.log4j:log4j-core"],
7
8
  "apache:log4j-api": ["org.apache.logging.log4j:log4j-api"],
8
9
  "apache:struts": ["org.apache.struts:struts2-core"],
package/fad-checker.js CHANGED
@@ -301,6 +301,8 @@ program
301
301
  .option("--no-ruby", "skip the Ruby (Bundler) codec")
302
302
  .option("--no-binaries", "skip scanning committed native binaries (.dll/.exe/.so/.dylib)")
303
303
  .option("--no-jars", "skip scanning embedded .jar/.war/.ear binaries for Maven coordinates")
304
+ .option("--no-certs", "skip scanning committed certificates, private/public keys (PEM/SSH/PuTTY/PGP) and keystores")
305
+ .option("--cert-expiry-days <n>", "warn on certificates expiring within N days", "90")
304
306
  .option("--no-js", "alias: skip JS/npm/yarn manifests even if present (Maven-only)")
305
307
  .option("--repo <eco=url...>", "extra registry as <ecosystem>=<url> (e.g. npm=https://npm.acme/) tried before the public one. Repeatable. Supports https://user:pass@host/.")
306
308
  .option("--add-repo <eco>", "persist a registry: --add-repo <ecosystem> <name> <url> [--auth user:pass] [--token TOK]")
@@ -837,11 +839,14 @@ async function runReportFlow(resolved, ecoFlags = {}) {
837
839
  const willKev = !!options.kev;
838
840
  const willLicenses = !!options.licenses;
839
841
  const willRetire = !!options.retire;
842
+ // Committed crypto material (certs / keys / keystores) — local file scan, no network.
843
+ const willCerts = options.certs !== false && !!options.src;
844
+ const certExpiryDays = parseInt(options.certExpiryDays, 10) || 90;
840
845
  // Identify committed native binaries by checksum (deps.dev + CIRCL) when present.
841
846
  const willBinaryId = [...resolved.values()].some(d => d.provenance === "binary");
842
847
  // License detection piggybacks on the registry passes (same fetched metadata),
843
848
  // so it adds no progress step of its own.
844
- const totalSteps = [willBom, willTransitive, willOverlay, willCve, /*EOL*/ true, willOutdated, /*npm reg*/ true, ...otherRegistryIds.map(() => true), willOsv, willOsvDb, willNvd, willEpss, willKev, willRetire, willBinaryId].filter(Boolean).length;
849
+ const totalSteps = [willBom, willTransitive, willOverlay, willCve, /*EOL*/ true, willOutdated, /*npm reg*/ true, ...otherRegistryIds.map(() => true), willOsv, willOsvDb, willNvd, willEpss, willKev, willRetire, willCerts, willBinaryId].filter(Boolean).length;
845
850
  const progress = new ui.Progress(totalSteps);
846
851
 
847
852
  if (willBom) {
@@ -1107,6 +1112,21 @@ async function runReportFlow(resolved, ecoFlags = {}) {
1107
1112
  }
1108
1113
  }
1109
1114
 
1115
+ // 5b. Certificate / key-material scan — committed certs, private/public keys
1116
+ // (PEM, OpenSSH every algorithm, PuTTY, PGP, SSH one-liners) and keystores.
1117
+ // Pure local file walk: no network, so it runs the same online or offline.
1118
+ let certFindings = [];
1119
+ if (willCerts) {
1120
+ const st = progress.start("Certificates & keys");
1121
+ try {
1122
+ const { scanCertificates } = require("./lib/certs");
1123
+ certFindings = scanCertificates(options.src, { srcRoot: options.src, excludePath, defaultExcludes, expiryDays: certExpiryDays });
1124
+ const priv = certFindings.filter(c => c.kind === "private-key").length;
1125
+ const expired = certFindings.filter(c => c.issues.some(i => i.type === "cert-expired")).length;
1126
+ st.done(`${certFindings.length} item(s)${priv ? ` · ${priv} private key(s)` : ""}${expired ? ` · ${expired} expired` : ""}`);
1127
+ } catch (err) { st.fail(err.message); }
1128
+ }
1129
+
1110
1130
  // 6. Snyk (optional)
1111
1131
  let snykMatches = [];
1112
1132
  if (options.snyk) {
@@ -1235,6 +1255,18 @@ async function runReportFlow(resolved, ecoFlags = {}) {
1235
1255
  }
1236
1256
  }
1237
1257
 
1258
+ if (certFindings.length) {
1259
+ const privN = certFindings.filter(c => c.kind === "private-key").length;
1260
+ const expiredN = certFindings.filter(c => c.issues.some(i => i.type === "cert-expired")).length;
1261
+ heading("Certificates & keys", certFindings.length, [privN ? chalk.red(`${privN} private key`) : null, expiredN ? chalk.yellow(`${expiredN} expired`) : null].filter(Boolean).join(" "));
1262
+ for (const c of certFindings.slice(0, 10)) {
1263
+ const vis = c.keyVisibility ? chalk.dim(`[${c.keyVisibility}]`) : "";
1264
+ const label = c.kind === "certificate" ? `${c.subject || "?"}` : `${c.algorithm || ""} ${c.kind}`.trim();
1265
+ console.log(" " + sev(c.severity.toUpperCase())((c.severity || "?").padEnd(8)) + " " + chalk.white(path.basename(String(c.path))) + " " + vis + " " + chalk.dim(label));
1266
+ }
1267
+ if (certFindings.length > 10) console.log(chalk.dim(` …and ${certFindings.length - 10} more (see report ch.2.4)`));
1268
+ }
1269
+
1238
1270
  heading("EOL frameworks", eolResults.length);
1239
1271
  for (const e of eolResults.slice(0, 8)) console.log(" " + chalk.yellow(e.product.padEnd(18)) + " " + chalk.dim(`${coordOf(e.dep)}:${e.dep.version}`) + " " + chalk.dim(e.eol === true ? "EOL" : String(e.eol)) + definedInOf(e.dep));
1240
1272
  if (eolResults.length > 8) console.log(chalk.dim(` …and ${eolResults.length - 8} more`));
@@ -1379,7 +1411,7 @@ async function runReportFlow(resolved, ecoFlags = {}) {
1379
1411
  if (out.html || out.doc) {
1380
1412
  await ensureDir(out.html); await ensureDir(out.doc);
1381
1413
  const { htmlPath, docPath } = await writeReports({
1382
- cveMatches: prodMatches, devCveMatches: devMatches, embeddedMatches, retireMatches, vendoredJsInventory,
1414
+ cveMatches: prodMatches, devCveMatches: devMatches, embeddedMatches, retireMatches, vendoredJsInventory, certFindings,
1383
1415
  eolResults, obsoleteResults, outdatedResults, licenseResults, excludedDirs,
1384
1416
  resolvedDeps: resolved, projectInfo, warnings: reportWarnings, parsedManifests, diff,
1385
1417
  htmlPath: out.html, docPath: out.doc,
@@ -1410,7 +1442,7 @@ async function runReportFlow(resolved, ecoFlags = {}) {
1410
1442
  try {
1411
1443
  const { writeFindings } = require("./lib/json-export");
1412
1444
  await ensureDir(out.json);
1413
- writeFindings({ cveMatches, retireMatches, vendoredJsInventory, eolResults, obsoleteResults, outdatedResults, licenseResults, excludedDirs, resolvedDeps: resolved, projectInfo, toolVersion: pkg.version, typosquats, diff: jsonDiff }, out.json);
1445
+ writeFindings({ cveMatches, retireMatches, vendoredJsInventory, certFindings, eolResults, obsoleteResults, outdatedResults, licenseResults, excludedDirs, resolvedDeps: resolved, projectInfo, toolVersion: pkg.version, typosquats, diff: jsonDiff }, out.json);
1414
1446
  wrote.push(["Findings JSON", out.json]);
1415
1447
  } catch (err) { ui.warn(`JSON export failed: ${err.message}`); }
1416
1448
  }
@@ -1418,7 +1450,7 @@ async function runReportFlow(resolved, ecoFlags = {}) {
1418
1450
  try {
1419
1451
  const { writeSarif } = require("./lib/sarif-export");
1420
1452
  await ensureDir(out.sarif);
1421
- writeSarif(cveMatches.filter(m => !m.suppressed), out.sarif, { projectInfo, toolVersion: pkg.version });
1453
+ writeSarif(cveMatches.filter(m => !m.suppressed), out.sarif, { projectInfo, toolVersion: pkg.version, certFindings });
1422
1454
  wrote.push(["SARIF", out.sarif]);
1423
1455
  } catch (err) { ui.warn(`SARIF export failed: ${err.message}`); }
1424
1456
  }