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 +52 -0
- package/README.md +3 -2
- package/REVIEW-2026-07-02.md +190 -0
- package/completions/fad-checker.bash +1 -1
- package/completions/fad-checker.zsh +3 -0
- package/data/cpe-coord-map.json +1 -0
- package/fad-checker.js +36 -4
- package/lib/certs/analyze.js +305 -0
- package/lib/certs/index.js +17 -0
- package/lib/certs/scan.js +67 -0
- package/lib/certs/sniff.js +29 -0
- package/lib/codecs/go/parse.js +50 -8
- package/lib/codecs/go/registry.js +3 -1
- package/lib/codecs/go.codec.js +22 -4
- package/lib/codecs/nuget/parse.js +21 -6
- package/lib/codecs/nuget/registry.js +30 -4
- package/lib/codecs/nuget.codec.js +37 -7
- package/lib/codecs/pypi/parse.js +11 -2
- package/lib/codecs/pypi/registry.js +3 -1
- package/lib/codecs/pypi.codec.js +3 -0
- package/lib/cve-download.js +102 -11
- package/lib/cve-report.js +56 -8
- package/lib/json-export.js +4 -1
- package/lib/maven-version.js +25 -16
- package/lib/osv.js +8 -4
- package/lib/provenance.js +2 -0
- package/lib/sarif-export.js +48 -2
- package/package.json +1 -1
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
|
|
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 & 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 & 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)'
|
package/data/cpe-coord-map.json
CHANGED
|
@@ -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
|
}
|