fad-checker 1.0.6 → 2.0.0
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 +47 -0
- package/CLAUDE.md +27 -9
- package/README.md +27 -13
- package/completions/fad-checker.bash +4 -1
- package/completions/fad-checker.zsh +9 -0
- package/data/eol-mapping.json +17 -0
- package/docs/ARCHITECTURE.md +35 -7
- package/docs/USAGE.md +15 -7
- package/docs/superpowers/plans/2026-05-29-codec-composer.md +556 -0
- package/docs/superpowers/plans/2026-05-29-codec-foundation.md +851 -0
- package/docs/superpowers/plans/2026-05-29-codec-nuget.md +432 -0
- package/docs/superpowers/plans/2026-05-29-codec-pypi.md +450 -0
- package/docs/superpowers/specs/2026-05-29-codecs-multi-ecosystem-design.md +251 -0
- package/fad-checker.js +108 -82
- package/lib/codecs/codec.interface.js +27 -0
- package/lib/codecs/composer/parse.js +59 -0
- package/lib/codecs/composer/registry.js +92 -0
- package/lib/codecs/composer.codec.js +93 -0
- package/lib/codecs/index.js +37 -0
- package/lib/codecs/maven.codec.js +71 -0
- package/lib/{npm → codecs/npm}/collect.js +32 -18
- package/lib/codecs/npm.codec.js +52 -0
- package/lib/codecs/nuget/parse.js +75 -0
- package/lib/codecs/nuget/registry.js +88 -0
- package/lib/codecs/nuget.codec.js +92 -0
- package/lib/codecs/pypi/parse.js +71 -0
- package/lib/codecs/pypi/registry.js +89 -0
- package/lib/codecs/pypi.codec.js +81 -0
- package/lib/codecs/recipes.js +108 -0
- package/lib/codecs/select.js +27 -0
- package/lib/codecs/yarn.codec.js +19 -0
- package/lib/cve-match.js +3 -6
- package/lib/cve-report.js +34 -70
- package/lib/dep-record.js +60 -0
- package/lib/osv.js +22 -14
- package/lib/outdated.js +13 -2
- package/package.json +3 -2
- package/test/cli-ecosystem.test.js +30 -0
- package/test/codec-edge-cases.test.js +131 -0
- package/test/codec-integration.test.js +119 -0
- package/test/codecs.test.js +92 -0
- package/test/composer.test.js +71 -0
- package/test/cve-report.test.js +9 -0
- package/test/dep-record.test.js +31 -0
- package/test/fixtures/csharp-config/packages.config +4 -0
- package/test/fixtures/csharp-csproj/Directory.Packages.props +5 -0
- package/test/fixtures/csharp-csproj/app.csproj +7 -0
- package/test/fixtures/csharp-lock/packages.lock.json +6 -0
- package/test/fixtures/monorepo-mixed/packages/no-lock/package.json +2 -1
- package/test/fixtures/php-app/composer.json +5 -0
- package/test/fixtures/php-app/composer.lock +10 -0
- package/test/fixtures/polyglot/cs/packages.lock.json +1 -0
- package/test/fixtures/polyglot/js/package-lock.json +1 -0
- package/test/fixtures/polyglot/js/package.json +1 -0
- package/test/fixtures/polyglot/mvn/pom.xml +7 -0
- package/test/fixtures/polyglot/php/composer.lock +1 -0
- package/test/fixtures/polyglot/py/poetry.lock +3 -0
- package/test/fixtures/python-pipenv/Pipfile.lock +1 -0
- package/test/fixtures/python-poetry/poetry.lock +7 -0
- package/test/fixtures/python-reqs/requirements.txt +5 -0
- package/test/fixtures/python-uv/uv.lock +3 -0
- package/test/monorepo.test.js +1 -1
- package/test/npm-registry.test.js +1 -1
- package/test/npm.test.js +8 -4
- package/test/nuget.test.js +66 -0
- package/test/osv.test.js +62 -0
- package/test/pypi.test.js +72 -0
- package/test/webjar.test.js +1 -1
- /package/lib/{npm → codecs/npm}/parse.js +0 -0
- /package/lib/{npm → codecs/npm}/registry.js +0 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Design — Codecs & support multi-écosystème (C#/.NET, PHP, Python)
|
|
2
|
+
|
|
3
|
+
**Date** : 2026-05-29
|
|
4
|
+
**Statut** : approuvé (brainstorming) — prêt pour le plan d'implémentation
|
|
5
|
+
|
|
6
|
+
## Objectif
|
|
7
|
+
|
|
8
|
+
1. Réorganiser tout le code spécifique à un écosystème (aujourd'hui Maven + npm/yarn,
|
|
9
|
+
éparpillé dans `lib/core.js`, `lib/npm/*`, `lib/outdated.js`, `lib/osv.js`,
|
|
10
|
+
`lib/cve-report.js`, `fad-checker.js`) derrière une **interface codec** uniforme.
|
|
11
|
+
2. Ajouter trois écosystèmes en **parité complète** (vuln, EOL, obsolete, outdated,
|
|
12
|
+
recette de fix dans le report) : **C#/.NET (nuget)**, **PHP (composer)**, **Python (pypi)**.
|
|
13
|
+
|
|
14
|
+
## Décisions cadrées (brainstorming)
|
|
15
|
+
|
|
16
|
+
- **Parité complète d'emblée** pour les 3 nouveaux écosystèmes.
|
|
17
|
+
- **Big-bang** : extraction complète maven/npm vers des codecs (pas de cohabitation
|
|
18
|
+
« ancien monde / nouveau monde »). Le big-bang **réorganise, il ne réécrit pas** la
|
|
19
|
+
logique métier éprouvée (résolution POM/BOM, walker Maven Central, parsers lockfiles).
|
|
20
|
+
- **depRecord généralisé** : schéma neutre `{ ecosystem, namespace, name, version,
|
|
21
|
+
versions[], coordKey, manifestPaths[], … }`.
|
|
22
|
+
- **Règle de résolution de manifeste uniforme** (changement de philosophie) :
|
|
23
|
+
lockfile présent → on le prend ; sinon **fallback** sur le manifeste lâche
|
|
24
|
+
(`composer.json` / `requirements.txt` / `*.csproj` / `package.json`) **avec warning**.
|
|
25
|
+
Versions épinglées scannées, ranges/floating skippées individuellement + warning.
|
|
26
|
+
- **TOML** : dépendance légère **`smol-toml`** (maintenue, zéro dép transitive, TOML 1.0).
|
|
27
|
+
|
|
28
|
+
### ⚠️ Changement de contrat assumé (npm)
|
|
29
|
+
|
|
30
|
+
Comportement actuel : un `package.json` sans `package-lock.json`/`yarn.lock` est
|
|
31
|
+
**entièrement skippé** (warning chapitre 0). Nouveau comportement : il est **parsé en
|
|
32
|
+
best-effort** (versions épinglées) avec warning `no-lockfile — résultats partiels`.
|
|
33
|
+
Le test existant `no-lockfile` est adapté en conséquence (vérifie désormais le
|
|
34
|
+
fallback+warning, plus le skip total).
|
|
35
|
+
|
|
36
|
+
## Architecture
|
|
37
|
+
|
|
38
|
+
### L'interface codec
|
|
39
|
+
|
|
40
|
+
`lib/codecs/codec.interface.js` documente le contrat (objet, pas de classe imposée) :
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
{
|
|
44
|
+
id, // "maven" | "npm" | "yarn" | "nuget" | "composer" | "pypi"
|
|
45
|
+
label, // "Maven", "npm", "NuGet", "Composer", "PyPI"
|
|
46
|
+
osvEcosystem, // "Maven" | "npm" | "NuGet" | "Packagist" | "PyPI" | null
|
|
47
|
+
|
|
48
|
+
manifestNames, // fichiers revendiqués par ce codec
|
|
49
|
+
detect(dir) -> bool, // ce repo contient-il mes manifestes ?
|
|
50
|
+
|
|
51
|
+
collect(dir, opts) -> { deps: Map<coordKey, depRecord>, warnings: [] },
|
|
52
|
+
|
|
53
|
+
coordKey(dep), // clé Map normalisée
|
|
54
|
+
formatCoord(dep), // affichage report
|
|
55
|
+
osvPackageName(dep), // nom pour OSV
|
|
56
|
+
|
|
57
|
+
checkRegistry(deps, opts) -> { outdated:[], deprecated:[] },
|
|
58
|
+
resolveEolProduct(dep) -> { product, label } | null,
|
|
59
|
+
|
|
60
|
+
recipe, // { label, pinSection, pinIntro, snippet(deps), directSection }
|
|
61
|
+
nativeScanners, // scanners au-delà du tronc commun OSV/NVD
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Le depRecord généralisé
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
{
|
|
69
|
+
ecosystem, // "maven"|"npm"|"yarn"|"nuget"|"composer"|"pypi"
|
|
70
|
+
namespace, // groupId (maven) / scope @org (npm) / vendor (composer) / "" sinon
|
|
71
|
+
name, // artifactId / package name
|
|
72
|
+
version, // version représentative (la plus haute) pour affichage/EOL/outdated
|
|
73
|
+
versions[], // toutes les versions concrètes distinctes (CVE/OSV itèrent dessus)
|
|
74
|
+
coordKey, // clé de la Map, ex "maven:org.apache:log4j", "pypi:requests"
|
|
75
|
+
scope, isDev,
|
|
76
|
+
manifestPaths[], // remplace pomPaths/manifestPaths
|
|
77
|
+
// transitif : via, viaPaths, depth
|
|
78
|
+
// lock-specific : lockType, resolved, integrity
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`coordKey` ne collisionne jamais entre écosystèmes (préfixe `ecosystem:`).
|
|
83
|
+
|
|
84
|
+
### Registre des codecs
|
|
85
|
+
|
|
86
|
+
`lib/codecs/index.js` :
|
|
87
|
+
- `getCodec(id)` → codec
|
|
88
|
+
- `allCodecs()` → tous les codecs enregistrés
|
|
89
|
+
- `detectCodecs(srcDir)` → codecs dont `detect()` est vrai
|
|
90
|
+
|
|
91
|
+
### Services partagés (agnostiques — plus aucun `if ecosystem === …`)
|
|
92
|
+
|
|
93
|
+
- `lib/osv.js` — querybatch groupé par `codec.osvEcosystem`, nom via `codec.osvPackageName`.
|
|
94
|
+
- `lib/nvd.js` — enrichment par CVE-id, totalement agnostique. **Activé pour tous les
|
|
95
|
+
écosystèmes** : dès qu'OSV remonte un `CVE-xxxx`, NVD/CPE s'appliquent.
|
|
96
|
+
- `lib/cpe.js` — refinement / filtrage faux positifs sur les matches, agnostique.
|
|
97
|
+
- Fetch `endoflife.date` — partagé ; le codec ne fournit que `{ product }`.
|
|
98
|
+
- Cache (`~/.fad-checker/`) — partagé.
|
|
99
|
+
|
|
100
|
+
### Scanners rattachés à un codec (`nativeScanners`)
|
|
101
|
+
|
|
102
|
+
- `maven` → CVE-index local cvelistV5 (`lib/cve-download.js` + tier matching `lib/cve-match.js`).
|
|
103
|
+
- `npm`/`yarn` → retire.js (`lib/retire.js`).
|
|
104
|
+
- `nuget` / `composer` / `pypi` → aucun (couverts par OSV + NVD).
|
|
105
|
+
|
|
106
|
+
### Flux d'orchestration (`fad-checker.js`)
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
1. detectCodecs(src) filtré par --ecosystem
|
|
110
|
+
2. resolved = Map() ; pour chaque codec actif : merge codec.collect(src,opts).deps
|
|
111
|
+
3. expansion transitive : déléguée au codec (maven → walker Maven Central ; autres → lock)
|
|
112
|
+
4. vuln :
|
|
113
|
+
a. OSV (tronc commun) groupé par codec.osvEcosystem
|
|
114
|
+
b. nativeScanners par codec (maven: CVE-index ; npm/yarn: retire.js)
|
|
115
|
+
c. NVD enrichment (agnostique, par CVE-id)
|
|
116
|
+
d. CPE refinement (agnostique)
|
|
117
|
+
5. EOL / obsolete / outdated : codec.resolveEolProduct + codec.checkRegistry
|
|
118
|
+
6. render : sections pilotées par les codecs présents
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Spécifications par codec
|
|
122
|
+
|
|
123
|
+
### maven (extrait)
|
|
124
|
+
|
|
125
|
+
`collect()` enveloppe `lib/core.js` (parse POM, résolution parent, merge multi-profils,
|
|
126
|
+
`<dependencyManagement>`, imports `scope=import`/BOM) **conservés tels quels** + walker
|
|
127
|
+
`lib/transitive.js`. WebJars : `webjarToNpm()` reste **dans le codec maven** (un WebJar
|
|
128
|
+
est un artifact Maven émettant une coord npm ; le codec maven route ces deps vers le
|
|
129
|
+
chemin npm pour EOL/registre).
|
|
130
|
+
|
|
131
|
+
| Aspect | Valeur |
|
|
132
|
+
|---|---|
|
|
133
|
+
| coordKey | `maven:groupId:artifactId` |
|
|
134
|
+
| osvEcosystem | `Maven` |
|
|
135
|
+
| nativeScanners | CVE-index cvelistV5 |
|
|
136
|
+
| registre | Maven Central Solr |
|
|
137
|
+
| obsolete | `data/known-obsolete.json` (curated) |
|
|
138
|
+
| EOL | `by_group_artifact` / `by_group_prefix` |
|
|
139
|
+
| recette | `<dependencyManagement>` + maj des deps directes |
|
|
140
|
+
|
|
141
|
+
### npm / yarn (extrait)
|
|
142
|
+
|
|
143
|
+
`collect()` enveloppe `lib/npm/parse.js` + `lib/npm/collect.js` (parsers
|
|
144
|
+
package-lock v1/2/3, yarn.lock v1) **conservés**. Deux codecs partageant `osvEcosystem=npm`
|
|
145
|
+
et le préfixe coordKey `npm:` ; `ecosystemType` distingue pour report/recette.
|
|
146
|
+
|
|
147
|
+
| Aspect | Valeur |
|
|
148
|
+
|---|---|
|
|
149
|
+
| coordKey | `npm:name` (ou `npm:@scope/name`) |
|
|
150
|
+
| osvEcosystem | `npm` |
|
|
151
|
+
| nativeScanners | retire.js |
|
|
152
|
+
| registre | npm registry (`deprecated` + `dist-tags.latest`) |
|
|
153
|
+
| EOL | `by_npm_name` / `by_npm_scope` |
|
|
154
|
+
| recette | `overrides` (npm) / `resolutions` (yarn) |
|
|
155
|
+
|
|
156
|
+
### nuget (C#/.NET) — nouveau
|
|
157
|
+
|
|
158
|
+
| Aspect | Valeur |
|
|
159
|
+
|---|---|
|
|
160
|
+
| manifestes | `packages.lock.json` (lock, résolu+transitif), `*.csproj` (`<PackageReference>`, XML via xml2js), `packages.config` (XML legacy) |
|
|
161
|
+
| CPM | `Directory.Packages.props` lu comme **table de versions** ; `PackageReference` sans `Version` résolu contre `<PackageVersion>`, sinon warning `unresolved-versions` |
|
|
162
|
+
| fallback | `.csproj` épinglé scanné ; floating (`1.*`, `[1.0,2.0)`) skip+warning |
|
|
163
|
+
| coordKey | `nuget:<name-lower>` (NuGet case-insensitive ; casse d'origine gardée pour l'affichage) |
|
|
164
|
+
| osvEcosystem | `NuGet` |
|
|
165
|
+
| registre | `api.nuget.org` registration index → version stable max + `deprecation.reasons`/`alternatePackage` |
|
|
166
|
+
| EOL | endoflife.date `dotnet` (target framework `net6.0`/`net48`…), `aspnet`, `entity-framework` via `by_nuget_name` |
|
|
167
|
+
| recette | maj `<PackageReference>` + note CPM `Directory.Packages.props` |
|
|
168
|
+
|
|
169
|
+
### composer (PHP) — nouveau
|
|
170
|
+
|
|
171
|
+
| Aspect | Valeur |
|
|
172
|
+
|---|---|
|
|
173
|
+
| manifestes | `composer.lock` (JSON, `packages[]` + `packages-dev[]`, concret+transitif) ; `composer.json` sans lock → fallback+warning |
|
|
174
|
+
| coordKey | `composer:vendor/package` (case-insensitive normalisé) |
|
|
175
|
+
| osvEcosystem | `Packagist` |
|
|
176
|
+
| registre | `repo.packagist.org/p2/{vendor}/{pkg}.json` → dernière version + champ **`abandoned`** (≈ deprecated, peut pointer un remplaçant) |
|
|
177
|
+
| EOL | endoflife.date `php`, `laravel`, `symfony`, `drupal` via `by_composer_name` |
|
|
178
|
+
| recette | `composer require vendor/pkg:^x` / bloc `composer.json` |
|
|
179
|
+
|
|
180
|
+
### pypi (Python) — nouveau
|
|
181
|
+
|
|
182
|
+
| Aspect | Valeur |
|
|
183
|
+
|---|---|
|
|
184
|
+
| manifestes | `poetry.lock` (TOML), `Pipfile.lock` (JSON), `uv.lock` / `pdm.lock` (TOML), `requirements.txt` (épinglé `==` seul) |
|
|
185
|
+
| fallback | `requirements.txt` à ranges → skip+warning ; `pyproject.toml`/`Pipfile` sans lock → fallback+warning |
|
|
186
|
+
| normalisation | PEP 503 : `Foo.Bar_baz` → `foo-bar-baz` pour clé/OSV/registre ; casse d'origine pour affichage |
|
|
187
|
+
| coordKey | `pypi:<name-pep503>` |
|
|
188
|
+
| osvEcosystem | `PyPI` |
|
|
189
|
+
| registre | `pypi.org/pypi/{name}/json` → `info.version` (latest) + détection `yanked` par version + classifier `Development Status :: 7 - Inactive` comme signal obsolete |
|
|
190
|
+
| EOL | endoflife.date `python`, `django`, `numpy`, `fastapi` via `by_pypi_name` |
|
|
191
|
+
| recette | `pip install 'pkg>=x'` / ligne `requirements.txt` / bloc selon le lock détecté |
|
|
192
|
+
|
|
193
|
+
## Données
|
|
194
|
+
|
|
195
|
+
- `data/eol-mapping.json` gagne `by_nuget_name`, `by_composer_name`, `by_pypi_name`.
|
|
196
|
+
Ce fichier ne contient **que l'association coord → nom de produit endoflife.date** ;
|
|
197
|
+
les dates de cycle/EOL sont **récupérées en ligne** (endoflife.date) et **mises en
|
|
198
|
+
cache 7 jours**. Le mapping peut donc démarrer **minimal** (dotnet/php/python + quelques
|
|
199
|
+
frameworks) et s'enrichir au fil de l'eau par simple édition JSON, sans toucher au code.
|
|
200
|
+
- `data/known-obsolete.json` reste Maven-only : les autres écosystèmes ont des champs
|
|
201
|
+
registre authoritatifs (`deprecation` NuGet, `abandoned` Packagist, `yanked` PyPI).
|
|
202
|
+
|
|
203
|
+
## CLI
|
|
204
|
+
|
|
205
|
+
- `--ecosystem` : enum → **liste** : `auto` (défaut, = `detect()` vrai) | `all` | liste
|
|
206
|
+
explicite `maven,nuget,pypi,…`.
|
|
207
|
+
- `--no-<id>` génériques : `--no-maven --no-npm --no-yarn --no-nuget --no-composer --no-pypi`.
|
|
208
|
+
- `--no-js` conservé comme **alias** de `--no-npm`+`--no-yarn` (rétro-compat).
|
|
209
|
+
- `--transitive` / `--transitive-depth` : s'appliquent au codec maven uniquement
|
|
210
|
+
(no-op documenté ailleurs ; les autres ont le transitif via lock).
|
|
211
|
+
- `--snyk` : inchangé (POM nettoyé maven).
|
|
212
|
+
|
|
213
|
+
## Report (`lib/cve-report.js`)
|
|
214
|
+
|
|
215
|
+
- Sections par écosystème (1.a, 1.b, …) **générées dynamiquement** selon les codecs
|
|
216
|
+
présents, ordre stable : `maven, npm, yarn, nuget, composer, pypi`.
|
|
217
|
+
- `RECIPE_SPECS` / `ECO_LABELS` / `ECO_MANIFEST_KIND` alimentés depuis `codec.label`,
|
|
218
|
+
`codec.recipe`, `codec.manifestNames` — fin des maps codées en dur.
|
|
219
|
+
- `formatCoord(dep)` délégué au codec — fin des `if (ecosystem === "npm")` dans le rendu.
|
|
220
|
+
|
|
221
|
+
## Tests
|
|
222
|
+
|
|
223
|
+
Objectif : les 96 tests existants restent verts + nouveaux tests.
|
|
224
|
+
|
|
225
|
+
- Fixtures : `csharp-app/` (.csproj + packages.lock.json + packages.config +
|
|
226
|
+
Directory.Packages.props), `php-app/` (composer.json + composer.lock),
|
|
227
|
+
`python-app/` (un répertoire par format : requirements.txt épinglé, poetry.lock,
|
|
228
|
+
Pipfile.lock, uv.lock).
|
|
229
|
+
- Un fichier de test par codec (`nuget.test.js`, `composer.test.js`, `pypi.test.js`) :
|
|
230
|
+
parsing, coordKey, normalisation de noms (PEP 503, case-insensitive NuGet),
|
|
231
|
+
fallback no-lockfile.
|
|
232
|
+
- Test de contrat paramétré sur `allCodecs()` : chaque codec implémente toute l'interface.
|
|
233
|
+
- Fixture `polyglot/` mêlant les 5 écosystèmes (ou extension de `monorepo-mixed`).
|
|
234
|
+
|
|
235
|
+
## Ordre d'exécution du big-bang (chaque étape laisse le pipeline fonctionnel)
|
|
236
|
+
|
|
237
|
+
1. `codec.interface.js` + `lib/codecs/index.js` (registre vide).
|
|
238
|
+
2. Généraliser le `depRecord` (`namespace`/`name`/`coordKey`/`manifestPaths`) — adapter
|
|
239
|
+
collecteurs + report + tests existants.
|
|
240
|
+
3. Extraire `maven` et `npm`/`yarn` dans des codecs (déplacement, pas réécriture) ;
|
|
241
|
+
brancher l'orchestrateur sur le registre. **→ les 96 tests repassent ici.**
|
|
242
|
+
4. Rendre `osv.js` / `nvd.js` / `cpe.js` / endoflife agnostiques (via codec).
|
|
243
|
+
5. Ajouter `composer` (lock JSON, le plus simple), puis `pypi`, puis `nuget`.
|
|
244
|
+
6. Report dynamique + CLI liste + fixtures + tests nouveaux.
|
|
245
|
+
|
|
246
|
+
## Hors périmètre (YAGNI)
|
|
247
|
+
|
|
248
|
+
- Pas de résolution de ranges/floating (cohérent lockfile-first ; ranges → warning).
|
|
249
|
+
- Pas de support yarn-berry (déjà `unsupported` aujourd'hui), pas de pnpm dans ce lot.
|
|
250
|
+
- Pas de scanner natif dédié pour nuget/composer/pypi (OSV + NVD suffisent à la parité).
|
|
251
|
+
- Pas de curation obsolete pour les nouveaux écosystèmes (champs registre authoritatifs).
|
package/fad-checker.js
CHANGED
|
@@ -187,8 +187,14 @@ program
|
|
|
187
187
|
.option("--no-retire", "skip retire.js vendored-JS scan")
|
|
188
188
|
.option("--retire-refresh", "ignore retire cache and re-scan")
|
|
189
189
|
.option("--transitive-depth <n>", "max transitive depth", "6")
|
|
190
|
-
.option("--ecosystem <
|
|
191
|
-
.option("--no-
|
|
190
|
+
.option("--ecosystem <list>", "codecs to run: auto|all|<comma list> e.g. maven,npm,nuget,composer,pypi (default: auto = detected)", "auto")
|
|
191
|
+
.option("--no-maven", "skip the Maven codec")
|
|
192
|
+
.option("--no-npm", "skip the npm codec")
|
|
193
|
+
.option("--no-yarn", "skip the Yarn codec")
|
|
194
|
+
.option("--no-nuget", "skip the NuGet (C#/.NET) codec")
|
|
195
|
+
.option("--no-composer", "skip the Composer (PHP) codec")
|
|
196
|
+
.option("--no-pypi", "skip the PyPI (Python) codec")
|
|
197
|
+
.option("--no-js", "alias: skip JS/npm/yarn manifests even if present (Maven-only)")
|
|
192
198
|
.option("--repo <url...>", "extra Maven repository URL(s) to try before Maven Central. Supports https://user:pass@host/path/. Repeatable.")
|
|
193
199
|
.option("--add-repo <name>", "persist a Maven repo: --add-repo <name> <url> [--auth user:pass]")
|
|
194
200
|
.option("--remove-repo <name>", "remove a persisted Maven repo by name")
|
|
@@ -242,45 +248,53 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
242
248
|
console.log(chalk.gray(`📦 Maven repos: ${mavenRepos.map(r => r.name).join(" → ")}`));
|
|
243
249
|
}
|
|
244
250
|
|
|
245
|
-
const pomFiles = core.findPomFiles(options.src);
|
|
246
|
-
const allPomMetadata = core.newMetadataStore();
|
|
247
|
-
const allPropsByPom = {};
|
|
248
251
|
let wrotePom = 0;
|
|
249
252
|
|
|
250
|
-
//
|
|
251
|
-
const {
|
|
253
|
+
// --- Codec detection + selection ---
|
|
254
|
+
const { detectCodecs, allCodecs, getCodec } = require("./lib/codecs");
|
|
255
|
+
const { resolveActiveCodecs } = require("./lib/codecs/select");
|
|
252
256
|
const eco = (options.ecosystem || "auto").toLowerCase();
|
|
253
|
-
const
|
|
254
|
-
const
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
+
const detected = (eco === "auto") ? detectCodecs(options.src).map(c => c.id) : allCodecs().map(c => c.id);
|
|
258
|
+
const noCodecs = ["maven", "npm", "yarn", "nuget", "composer", "pypi"].filter(id => options[id] === false);
|
|
259
|
+
const activeIds = resolveActiveCodecs(eco, detected, { noCodecs, noJs: !options.js });
|
|
260
|
+
const runMaven = activeIds.includes("maven");
|
|
261
|
+
const runNpm = activeIds.includes("npm") || activeIds.includes("yarn");
|
|
262
|
+
|
|
263
|
+
// --- Collect deps from every active codec into one Map (coordKeys never collide) ---
|
|
264
|
+
const resolved = new Map();
|
|
265
|
+
let mavenCtx = null;
|
|
266
|
+
const collectWarnings = [];
|
|
267
|
+
for (const id of activeIds) {
|
|
268
|
+
if (id === "yarn") continue; // the npm codec already collects yarn.lock
|
|
269
|
+
const codec = getCodec(id);
|
|
270
|
+
let res;
|
|
271
|
+
try {
|
|
272
|
+
res = await codec.collect(options.src, { ignoreTest: !!options.ignoreTest, deps2Exclude, verbose });
|
|
273
|
+
} catch (err) {
|
|
274
|
+
console.warn(chalk.red(`❌ ${id} collect failed:`), chalk.dim(err.message));
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
for (const [k, v] of res.deps) resolved.set(k, v);
|
|
278
|
+
if (res.warnings?.length) collectWarnings.push(...res.warnings);
|
|
279
|
+
if (id === "maven") mavenCtx = res._maven;
|
|
280
|
+
}
|
|
257
281
|
|
|
258
282
|
if (!readOnly) {
|
|
259
283
|
try { await rimraf(options.target); } catch (_) { /* fresh dir */ }
|
|
260
284
|
}
|
|
261
285
|
|
|
262
|
-
if (runMaven) console.log(chalk.blue(`🔍 Found ${pomFiles.length} pom.xml files`));
|
|
286
|
+
if (runMaven && mavenCtx) console.log(chalk.blue(`🔍 Found ${mavenCtx.pomFiles.length} pom.xml files`));
|
|
263
287
|
if (runNpm) console.log(chalk.blue(`🔍 JS manifests detected — npm/yarn pipeline enabled`));
|
|
264
288
|
console.log();
|
|
265
289
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
catch (err) { console.warn(chalk.red("❌ parse failed:"), chalk.gray(pom), chalk.dim(err.message)); }
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Phase 2: resolve inheritance & merge profile deps
|
|
274
|
-
for (const pom of Object.keys(allPomMetadata.byPath)) {
|
|
275
|
-
try { await core.getAllInheritedProps(pom, allPomMetadata, allPropsByPom); }
|
|
276
|
-
catch (err) { console.error(`❌ inheritance failed for ${pom}:`, err.message); }
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Phase 3: rewrite cleaned POMs (or simulate, in --test mode)
|
|
290
|
+
// Maven POM rewrite (cleanup feature). Parse + inheritance already happened
|
|
291
|
+
// inside the maven codec's collect(); we reuse its metadata store here.
|
|
292
|
+
if (runMaven && mavenCtx) {
|
|
293
|
+
const { store, propsByPom, pomFiles } = mavenCtx;
|
|
280
294
|
const rewriteOpts = { srcRoot: options.src, targetRoot: options.target, deps2Exclude, verbose, readOnly };
|
|
281
295
|
for (const pom of pomFiles) {
|
|
282
296
|
try {
|
|
283
|
-
if (await core.rewritePoms(pom,
|
|
297
|
+
if (await core.rewritePoms(pom, store, propsByPom, rewriteOpts)) wrotePom++;
|
|
284
298
|
} catch (err) {
|
|
285
299
|
console.error(chalk.red(`❌ rewrite failed for ${pom}:`), err.message);
|
|
286
300
|
}
|
|
@@ -289,7 +303,8 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
289
303
|
|
|
290
304
|
// ---------- Summary: parents missing / excluded ----------
|
|
291
305
|
let privateLibIds = [];
|
|
292
|
-
if (runMaven) {
|
|
306
|
+
if (runMaven && mavenCtx) {
|
|
307
|
+
const allPomMetadata = mavenCtx.store; // reuse the codec's parsed metadata
|
|
293
308
|
console.log(chalk.cyanBright("\n─────────────────────────────────────────────"));
|
|
294
309
|
console.log(chalk.cyanBright("📦 Résumé des POM analysés :"));
|
|
295
310
|
console.log(chalk.cyanBright("─────────────────────────────────────────────\n"));
|
|
@@ -350,7 +365,7 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
350
365
|
|
|
351
366
|
// ---------- Report flow (CVE / EOL / Obsolete) ----------
|
|
352
367
|
if (options.report) {
|
|
353
|
-
await runReportFlow(
|
|
368
|
+
await runReportFlow(resolved, { activeIds, runMaven, runNpm, privateLibIds, mavenRepos, collectWarnings });
|
|
354
369
|
} else if (!readOnly) {
|
|
355
370
|
const target = options.target;
|
|
356
371
|
console.log(chalk.gray(`💡 Pour lancer Snyk depuis ${target} :`));
|
|
@@ -358,45 +373,35 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
358
373
|
}
|
|
359
374
|
})();
|
|
360
375
|
|
|
361
|
-
async function runReportFlow(
|
|
362
|
-
const { runMaven = true, runNpm = false, privateLibIds = [], mavenRepos = [] } = ecoFlags;
|
|
363
|
-
const {
|
|
364
|
-
const { ensureCveIndex } = require("./lib/cve-download");
|
|
376
|
+
async function runReportFlow(resolved, ecoFlags = {}) {
|
|
377
|
+
const { activeIds = [], runMaven = true, runNpm = false, privateLibIds = [], mavenRepos = [], collectWarnings = [] } = ecoFlags;
|
|
378
|
+
const { expandWithTransitives } = require("./lib/cve-match");
|
|
365
379
|
const { writeReports, computeStats } = require("./lib/cve-report");
|
|
380
|
+
const { getCodec } = require("./lib/codecs");
|
|
366
381
|
const outdated = require("./lib/outdated");
|
|
367
382
|
const offline = !!options.offline;
|
|
368
383
|
if (offline) console.log(chalk.gray(" (--offline: cached data only, no network)"));
|
|
369
384
|
|
|
370
385
|
console.log(chalk.bold.cyan("\n📋 Rapport CVE / EOL / Obsolète\n") + chalk.gray("─────────────────────────────"));
|
|
371
386
|
|
|
372
|
-
//
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
387
|
+
// Deps were already collected per-codec by main(). Just report the counts.
|
|
388
|
+
const byEcoCount = {};
|
|
389
|
+
for (const d of resolved.values()) byEcoCount[d.ecosystem] = (byEcoCount[d.ecosystem] || 0) + 1;
|
|
390
|
+
if (runMaven) console.log(chalk.blue(`📚 ${byEcoCount.maven || 0} dépendances Maven directes (incl. parent POMs)`));
|
|
391
|
+
if (runNpm) console.log(chalk.blue(`📦 ${byEcoCount.npm || 0} dépendances npm/yarn`));
|
|
392
|
+
for (const [ecoId, n] of Object.entries(byEcoCount)) {
|
|
393
|
+
if (ecoId === "maven" || ecoId === "npm") continue;
|
|
394
|
+
const label = (require("./lib/codecs").getCodec(ecoId)?.label) || ecoId;
|
|
395
|
+
console.log(chalk.blue(`📦 ${n} dépendances ${label}`));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Warnings surfaced during collection (e.g. npm no-lockfile fallback).
|
|
399
|
+
const npmWarnings = collectWarnings || [];
|
|
384
400
|
let scanWarnings = [];
|
|
385
|
-
if (
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
deps2Exclude,
|
|
390
|
-
verbose,
|
|
391
|
-
});
|
|
392
|
-
for (const [k, v] of npmDeps) resolved.set(k, v);
|
|
393
|
-
console.log(chalk.blue(`📦 ${npmDeps.size} dépendances npm/yarn ajoutées (total: ${resolved.size})`));
|
|
394
|
-
npmWarnings = npmDeps.warnings || [];
|
|
395
|
-
if (npmWarnings.length) {
|
|
396
|
-
console.log(chalk.yellow(`⚠️ ${npmWarnings.length} JS manifest(s) sans lockfile — non scannés :`));
|
|
397
|
-
for (const w of npmWarnings) {
|
|
398
|
-
console.log(chalk.yellow(` • ${w.manifestPath} — ${w.message}`));
|
|
399
|
-
}
|
|
401
|
+
if (npmWarnings.length) {
|
|
402
|
+
console.log(chalk.yellow(`⚠️ ${npmWarnings.length} manifest warning(s) :`));
|
|
403
|
+
for (const w of npmWarnings) {
|
|
404
|
+
console.log(chalk.yellow(` • ${w.manifestPath} — ${w.message}`));
|
|
400
405
|
}
|
|
401
406
|
}
|
|
402
407
|
const directCount = resolved.size;
|
|
@@ -419,7 +424,7 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
|
|
|
419
424
|
}
|
|
420
425
|
}
|
|
421
426
|
|
|
422
|
-
if (options.transitive) {
|
|
427
|
+
if (options.transitive && runMaven) {
|
|
423
428
|
await expandWithTransitives(resolved, {
|
|
424
429
|
verbose,
|
|
425
430
|
offline,
|
|
@@ -430,20 +435,20 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
|
|
|
430
435
|
console.log(chalk.blue(`🌳 ${resolved.size - directCount} dépendances transitives ajoutées (total: ${resolved.size})`));
|
|
431
436
|
}
|
|
432
437
|
|
|
433
|
-
// 1. CVE
|
|
438
|
+
// 1. CVE — native scanner contributed by the maven codec (local cvelistV5 index).
|
|
434
439
|
let cveMatches = [];
|
|
435
440
|
let cveDataDate = null;
|
|
436
|
-
if (
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
verbose
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
441
|
+
if (runMaven) {
|
|
442
|
+
const sc = (getCodec("maven").nativeScanners || []).find(s => s.kind === "cve");
|
|
443
|
+
const indexExists = fs.existsSync(require("./lib/cve-download").CVE_INDEX_PATH);
|
|
444
|
+
if (sc && (!(options.cveOffline || offline) || indexExists)) {
|
|
445
|
+
try {
|
|
446
|
+
const r = await sc.scan(resolved, { cveRefresh: !!options.cveRefresh, cveOffline: !!options.cveOffline, offline, verbose });
|
|
447
|
+
cveMatches = r.matches || [];
|
|
448
|
+
cveDataDate = r.meta?.cveDataDate || null;
|
|
449
|
+
} catch (err) {
|
|
450
|
+
console.warn(chalk.yellow("⚠️ CVE scan skipped:"), err.message);
|
|
451
|
+
}
|
|
447
452
|
}
|
|
448
453
|
}
|
|
449
454
|
|
|
@@ -469,12 +474,26 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
|
|
|
469
474
|
// WebJars (Maven artifacts wrapping npm/bower libs), so it runs even in
|
|
470
475
|
// Maven-only mode. One fetch per package; no-ops when there are no targets.
|
|
471
476
|
try {
|
|
472
|
-
const { checkNpmRegistryDeps } = require("./lib/npm/registry");
|
|
477
|
+
const { checkNpmRegistryDeps } = require("./lib/codecs/npm/registry");
|
|
473
478
|
const npmReg = await checkNpmRegistryDeps(resolved, { verbose, offline, allLibs: options.allLibs });
|
|
474
479
|
obsoleteResults = obsoleteResults.concat(npmReg.deprecated);
|
|
475
480
|
outdatedResults = outdatedResults.concat(npmReg.outdated);
|
|
476
481
|
} catch (err) { console.warn(chalk.yellow("⚠️ npm registry check skipped:"), err.message); }
|
|
477
482
|
|
|
483
|
+
// 4b. Per-codec registry for ecosystems beyond maven/npm (composer/pypi/nuget).
|
|
484
|
+
// maven (Maven Central) + npm (registry) are already covered above; this loop
|
|
485
|
+
// drives each remaining active codec's own registry (Packagist abandoned, etc.).
|
|
486
|
+
for (const id of activeIds) {
|
|
487
|
+
if (id === "maven" || id === "npm" || id === "yarn") continue;
|
|
488
|
+
const codec = getCodec(id);
|
|
489
|
+
if (!codec?.checkRegistry) continue;
|
|
490
|
+
try {
|
|
491
|
+
const reg = await codec.checkRegistry(resolved, { verbose, offline, allLibs: options.allLibs });
|
|
492
|
+
obsoleteResults = obsoleteResults.concat(reg.deprecated || []);
|
|
493
|
+
outdatedResults = outdatedResults.concat(reg.outdated || []);
|
|
494
|
+
} catch (err) { console.warn(chalk.yellow(`⚠️ ${id} registry check skipped:`), err.message); }
|
|
495
|
+
}
|
|
496
|
+
|
|
478
497
|
// Cross-section dedup: drop entries from outdated that already appear in EOL/Obsolete
|
|
479
498
|
const eolKeys = new Set(eolResults.map(r => `${r.dep.groupId}:${r.dep.artifactId}`));
|
|
480
499
|
const obsKeys = new Set(obsoleteResults.map(r => `${r.dep.groupId}:${r.dep.artifactId}`));
|
|
@@ -519,16 +538,23 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
|
|
|
519
538
|
}
|
|
520
539
|
}
|
|
521
540
|
|
|
522
|
-
// 5. retire.js —
|
|
523
|
-
//
|
|
541
|
+
// 5. retire.js — native "vendored" scanner contributed by the npm codec. Scans
|
|
542
|
+
// vendored JS files (jquery copies, bootstrap, pdf.js, …) that live in the
|
|
543
|
+
// source tree without any lockfile to back them.
|
|
544
|
+
// Not gated by an active npm ecosystem: retire scans the source tree for
|
|
545
|
+
// vendored .js (which can live in a Maven project's resources too). The
|
|
546
|
+
// scanner is owned by the npm codec but runs whenever --retire is on.
|
|
524
547
|
let retireMatches = [];
|
|
525
548
|
if (options.retire) {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
549
|
+
const sc = (getCodec("npm").nativeScanners || []).find(s => s.kind === "vendored");
|
|
550
|
+
if (sc) {
|
|
551
|
+
try {
|
|
552
|
+
const r = await sc.scan(resolved, { src: options.src, verbose, retireRefresh: !!options.retireRefresh, offline });
|
|
553
|
+
retireMatches = r.matches || [];
|
|
554
|
+
console.log(chalk.blue(`🔎 retire.js: ${retireMatches.length} vendored-JS finding(s)`));
|
|
555
|
+
} catch (err) {
|
|
556
|
+
console.warn(chalk.yellow("⚠️ retire.js skipped:"), err.message);
|
|
557
|
+
}
|
|
532
558
|
}
|
|
533
559
|
}
|
|
534
560
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/codecs/codec.interface.js — contrat que tout codec doit respecter.
|
|
3
|
+
*
|
|
4
|
+
* Pas de classe imposée : un codec est un objet litéral exportant ces clés.
|
|
5
|
+
* Voir docs/superpowers/specs/2026-05-29-codecs-multi-ecosystem-design.md.
|
|
6
|
+
*/
|
|
7
|
+
const REQUIRED_KEYS = [
|
|
8
|
+
"id", "label", "osvEcosystem", "manifestNames",
|
|
9
|
+
"detect", "collect", "coordKey", "formatCoord", "osvPackageName",
|
|
10
|
+
"checkRegistry", "resolveEolProduct", "recipe", "nativeScanners",
|
|
11
|
+
];
|
|
12
|
+
const FUNCTION_KEYS = ["detect", "collect", "coordKey", "formatCoord", "osvPackageName", "checkRegistry", "resolveEolProduct"];
|
|
13
|
+
|
|
14
|
+
function assertCodecShape(codec) {
|
|
15
|
+
if (!codec || typeof codec !== "object") throw new Error("codec must be an object");
|
|
16
|
+
for (const k of REQUIRED_KEYS) {
|
|
17
|
+
if (!(k in codec)) throw new Error(`codec "${codec.id || "?"}" missing required key: ${k}`);
|
|
18
|
+
}
|
|
19
|
+
for (const k of FUNCTION_KEYS) {
|
|
20
|
+
if (typeof codec[k] !== "function") throw new Error(`codec "${codec.id}" key ${k} must be a function`);
|
|
21
|
+
}
|
|
22
|
+
if (!Array.isArray(codec.manifestNames)) throw new Error(`codec "${codec.id}" manifestNames must be an array`);
|
|
23
|
+
if (!Array.isArray(codec.nativeScanners)) throw new Error(`codec "${codec.id}" nativeScanners must be an array`);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { REQUIRED_KEYS, FUNCTION_KEYS, assertCodecShape };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/composer/parse.js — parse PHP Composer manifests.
|
|
3
|
+
*
|
|
4
|
+
* composer.lock — { packages: [{name, version}], "packages-dev": [...] }
|
|
5
|
+
* versions are concrete (resolved) — the ideal source.
|
|
6
|
+
* composer.json — { require: {name: range}, "require-dev": {...} }
|
|
7
|
+
* used only as a no-lock fallback (pinned exact versions).
|
|
8
|
+
*
|
|
9
|
+
* Composer names are "vendor/package", case-insensitive (lowercased by convention).
|
|
10
|
+
*/
|
|
11
|
+
const fs = require("fs");
|
|
12
|
+
|
|
13
|
+
// "1.2.3" | "v1.2.3" | "dev-main" | "1.0.x-dev". Strip a leading "v" for OSV/registry.
|
|
14
|
+
function normVersion(v) {
|
|
15
|
+
if (!v) return null;
|
|
16
|
+
return String(v).replace(/^v/, "");
|
|
17
|
+
}
|
|
18
|
+
function isConcrete(v) {
|
|
19
|
+
if (!v) return false;
|
|
20
|
+
return /^\d+(\.\d+)*([.\-+]\S+)?$/.test(String(v).replace(/^v/, ""));
|
|
21
|
+
}
|
|
22
|
+
function splitName(full) {
|
|
23
|
+
const i = full.indexOf("/");
|
|
24
|
+
if (i < 0) return { vendor: "", pkg: full };
|
|
25
|
+
return { vendor: full.slice(0, i), pkg: full.slice(i + 1) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseComposerLock(filePath) {
|
|
29
|
+
const json = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
30
|
+
const deps = [];
|
|
31
|
+
const push = (arr, scope) => {
|
|
32
|
+
for (const p of arr || []) {
|
|
33
|
+
if (!p.name) continue;
|
|
34
|
+
const { vendor, pkg } = splitName(p.name);
|
|
35
|
+
deps.push({ name: p.name, vendor, pkg, version: normVersion(p.version), scope, isDev: scope === "dev" });
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
push(json.packages, "prod");
|
|
39
|
+
push(json["packages-dev"], "dev");
|
|
40
|
+
return { manifestPath: filePath, manifestType: "composer.lock", deps };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseComposerJson(filePath) {
|
|
44
|
+
const json = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
45
|
+
const deps = [];
|
|
46
|
+
const push = (obj, scope) => {
|
|
47
|
+
for (const [name, version] of Object.entries(obj || {})) {
|
|
48
|
+
// Platform requirements aren't packages.
|
|
49
|
+
if (name === "php" || name.startsWith("ext-") || name.startsWith("lib-")) continue;
|
|
50
|
+
const { vendor, pkg } = splitName(name);
|
|
51
|
+
deps.push({ name, vendor, pkg, version: String(version), scope, isDev: scope === "dev" });
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
push(json.require, "prod");
|
|
55
|
+
push(json["require-dev"], "dev");
|
|
56
|
+
return { manifestPath: filePath, manifestType: "composer.json", deps };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { parseComposerLock, parseComposerJson, normVersion, isConcrete, splitName };
|