fad-checker 2.0.0 → 2.0.1

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.
Files changed (93) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +58 -10
  3. package/data/eol-mapping.json +5 -5
  4. package/fad-checker.js +289 -203
  5. package/lib/codecs/npm/collect.js +26 -17
  6. package/lib/codecs/npm/parse.js +114 -10
  7. package/lib/codecs/npm/registry.js +4 -3
  8. package/lib/codecs/npm.codec.js +1 -1
  9. package/lib/codecs/nuget.codec.js +4 -2
  10. package/lib/codecs/pypi/parse.js +104 -12
  11. package/lib/codecs/pypi.codec.js +27 -6
  12. package/lib/core.js +35 -1
  13. package/lib/cve-match.js +15 -5
  14. package/lib/deps-descriptor.js +110 -0
  15. package/lib/nvd.js +4 -3
  16. package/lib/osv.js +7 -4
  17. package/lib/outdated.js +7 -2
  18. package/lib/retire.js +77 -13
  19. package/lib/transitive.js +3 -3
  20. package/lib/ui.js +87 -0
  21. package/package.json +3 -2
  22. package/CLAUDE.md +0 -147
  23. package/docs/ARCHITECTURE.md +0 -182
  24. package/docs/USAGE.md +0 -187
  25. package/docs/superpowers/plans/2026-05-29-codec-composer.md +0 -556
  26. package/docs/superpowers/plans/2026-05-29-codec-foundation.md +0 -851
  27. package/docs/superpowers/plans/2026-05-29-codec-nuget.md +0 -432
  28. package/docs/superpowers/plans/2026-05-29-codec-pypi.md +0 -450
  29. package/docs/superpowers/specs/2026-05-29-codecs-multi-ecosystem-design.md +0 -251
  30. package/test/cli-ecosystem.test.js +0 -30
  31. package/test/codec-edge-cases.test.js +0 -131
  32. package/test/codec-integration.test.js +0 -119
  33. package/test/codecs.test.js +0 -92
  34. package/test/composer.test.js +0 -71
  35. package/test/core.test.js +0 -153
  36. package/test/cpe.test.js +0 -166
  37. package/test/cve-download.test.js +0 -39
  38. package/test/cve-match.test.js +0 -217
  39. package/test/cve-report.test.js +0 -180
  40. package/test/dep-record.test.js +0 -31
  41. package/test/fixtures/complex-enterprise/api/pom.xml +0 -32
  42. package/test/fixtures/complex-enterprise/build/pom.xml +0 -46
  43. package/test/fixtures/complex-enterprise/dao/pom.xml +0 -37
  44. package/test/fixtures/complex-enterprise/pom.xml +0 -66
  45. package/test/fixtures/complex-enterprise/web/pom.xml +0 -77
  46. package/test/fixtures/csharp-config/packages.config +0 -4
  47. package/test/fixtures/csharp-csproj/Directory.Packages.props +0 -5
  48. package/test/fixtures/csharp-csproj/app.csproj +0 -7
  49. package/test/fixtures/csharp-lock/packages.lock.json +0 -6
  50. package/test/fixtures/cve-samples/cve-non-java.json +0 -19
  51. package/test/fixtures/cve-samples/cve-product-only.json +0 -31
  52. package/test/fixtures/cve-samples/cve-with-packagename.json +0 -37
  53. package/test/fixtures/cve-samples/nvd-log4shell.json +0 -40
  54. package/test/fixtures/cve-samples/nvd-npm-lodash.json +0 -22
  55. package/test/fixtures/monorepo-mixed/libs/common-bom/pom.xml +0 -26
  56. package/test/fixtures/monorepo-mixed/packages/cli/package.json +0 -14
  57. package/test/fixtures/monorepo-mixed/packages/cli/yarn.lock +0 -41
  58. package/test/fixtures/monorepo-mixed/packages/no-lock/package.json +0 -10
  59. package/test/fixtures/monorepo-mixed/packages/web-app/package-lock.json +0 -71
  60. package/test/fixtures/monorepo-mixed/packages/web-app/package.json +0 -17
  61. package/test/fixtures/monorepo-mixed/pom.xml +0 -29
  62. package/test/fixtures/monorepo-mixed/services/api/pom.xml +0 -27
  63. package/test/fixtures/monorepo-mixed/services/worker/pom.xml +0 -28
  64. package/test/fixtures/php-app/composer.json +0 -5
  65. package/test/fixtures/php-app/composer.lock +0 -10
  66. package/test/fixtures/polyglot/cs/packages.lock.json +0 -1
  67. package/test/fixtures/polyglot/js/package-lock.json +0 -1
  68. package/test/fixtures/polyglot/js/package.json +0 -1
  69. package/test/fixtures/polyglot/mvn/pom.xml +0 -7
  70. package/test/fixtures/polyglot/php/composer.lock +0 -1
  71. package/test/fixtures/polyglot/py/poetry.lock +0 -3
  72. package/test/fixtures/private-lib-detection/core/pom.xml +0 -26
  73. package/test/fixtures/private-lib-detection/plugin/pom.xml +0 -23
  74. package/test/fixtures/private-lib-detection/pom.xml +0 -35
  75. package/test/fixtures/python-pipenv/Pipfile.lock +0 -1
  76. package/test/fixtures/python-poetry/poetry.lock +0 -7
  77. package/test/fixtures/python-reqs/requirements.txt +0 -5
  78. package/test/fixtures/python-uv/uv.lock +0 -3
  79. package/test/fixtures/simple/app/pom.xml +0 -28
  80. package/test/fixtures/simple/lib/pom.xml +0 -18
  81. package/test/fixtures/simple/pom.xml +0 -24
  82. package/test/maven-repo.test.js +0 -111
  83. package/test/maven-version.test.js +0 -57
  84. package/test/monorepo.test.js +0 -132
  85. package/test/npm-registry.test.js +0 -64
  86. package/test/npm.test.js +0 -150
  87. package/test/nuget.test.js +0 -66
  88. package/test/osv.test.js +0 -62
  89. package/test/outdated.test.js +0 -101
  90. package/test/pypi.test.js +0 -72
  91. package/test/snyk.test.js +0 -64
  92. package/test/transitive.test.js +0 -305
  93. package/test/webjar.test.js +0 -33
@@ -1,251 +0,0 @@
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).
@@ -1,30 +0,0 @@
1
- const test = require("node:test");
2
- const assert = require("node:assert");
3
- const { resolveActiveCodecs } = require("../lib/codecs/select");
4
-
5
- test("resolveActiveCodecs parses comma list and 'all'", () => {
6
- const all = ["maven", "npm", "nuget", "composer", "pypi"];
7
- assert.deepStrictEqual(resolveActiveCodecs("maven,pypi", all, {}), ["maven", "pypi"]);
8
- assert.deepStrictEqual(resolveActiveCodecs("all", all, {}), all);
9
- assert.deepStrictEqual(resolveActiveCodecs("auto", all, {}), all);
10
- });
11
-
12
- test("resolveActiveCodecs honors --no-<id> flags", () => {
13
- const all = ["maven", "npm", "nuget"];
14
- assert.deepStrictEqual(resolveActiveCodecs("all", all, { noCodecs: ["npm"] }), ["maven", "nuget"]);
15
- });
16
-
17
- test("--no-js aliases to npm+yarn", () => {
18
- const all = ["maven", "npm", "yarn", "nuget"];
19
- assert.deepStrictEqual(resolveActiveCodecs("all", all, { noJs: true }), ["maven", "nuget"]);
20
- });
21
-
22
- test("legacy 'both' maps to maven+npm/yarn only", () => {
23
- const all = ["maven", "npm", "yarn", "nuget", "pypi"];
24
- assert.deepStrictEqual(resolveActiveCodecs("both", all, {}), ["maven", "npm", "yarn"]);
25
- });
26
-
27
- test("explicit list intersects with available (auto-detect)", () => {
28
- const detected = ["maven"]; // only maven detected on this tree
29
- assert.deepStrictEqual(resolveActiveCodecs("maven,npm", detected, {}), ["maven"]);
30
- });
@@ -1,131 +0,0 @@
1
- /**
2
- * Advanced edge-case + robustness tests for the composer/pypi/nuget codecs.
3
- * Uses temp dirs so we can throw malformed inputs at the collectors.
4
- */
5
- const test = require("node:test");
6
- const assert = require("node:assert");
7
- const fs = require("fs");
8
- const os = require("os");
9
- const path = require("path");
10
-
11
- const composer = require("../lib/codecs/composer.codec");
12
- const pypi = require("../lib/codecs/pypi.codec");
13
- const nuget = require("../lib/codecs/nuget.codec");
14
- const { parseRequirementsTxt, pep503 } = require("../lib/codecs/pypi/parse");
15
-
16
- function tmp(prefix, files) {
17
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
18
- for (const [name, content] of Object.entries(files)) {
19
- const fp = path.join(dir, name);
20
- fs.mkdirSync(path.dirname(fp), { recursive: true });
21
- fs.writeFileSync(fp, content);
22
- }
23
- return dir;
24
- }
25
-
26
- /* ---------------- composer ---------------- */
27
-
28
- test("composer: ignoreTest drops dev packages; deps2Exclude filters by name", async () => {
29
- const dir = tmp("comp-edge-", {
30
- "composer.lock": JSON.stringify({
31
- packages: [{ name: "guzzlehttp/guzzle", version: "7.4.5" }, { name: "acme/private", version: "1.0.0" }],
32
- "packages-dev": [{ name: "phpunit/phpunit", version: "10.0.0" }],
33
- }),
34
- });
35
- const noDev = await composer.collect(dir, { ignoreTest: true });
36
- assert.ok(!noDev.deps.has("composer:phpunit/phpunit"), "dev dep dropped with ignoreTest");
37
- assert.ok(noDev.deps.has("composer:guzzlehttp/guzzle"));
38
- const excl = await composer.collect(dir, { deps2Exclude: /^acme\// });
39
- assert.ok(!excl.deps.has("composer:acme/private"), "excluded by regex");
40
- });
41
-
42
- test("composer: malformed composer.lock does not throw, emits parse-error warning", async () => {
43
- const dir = tmp("comp-bad-", { "composer.lock": "{ not valid json ]" });
44
- const { deps, warnings } = await composer.collect(dir, {});
45
- assert.strictEqual(deps.size, 0);
46
- assert.ok(warnings.find(w => w.type === "parse-error"));
47
- });
48
-
49
- /* ---------------- pypi ---------------- */
50
-
51
- test("pypi: requirements.txt handles extras, env markers, -r/-e/comments", () => {
52
- const dir = tmp("py-req-", {
53
- "requirements.txt": [
54
- "# header",
55
- "requests[security]==2.31.0", // extras → still pinned
56
- 'django==4.2 ; python_version >= "3.8"', // env marker dropped
57
- "flask>=2.0", // range → skip
58
- "-r other.txt", // include → skip
59
- "-e .", // editable → skip
60
- "",
61
- ].join("\n"),
62
- });
63
- const r = parseRequirementsTxt(path.join(dir, "requirements.txt"));
64
- const names = r.deps.map(d => d.name).sort();
65
- assert.deepStrictEqual(names, ["django", "requests"]);
66
- assert.strictEqual(r.deps.find(d => d.name === "requests").version, "2.31.0");
67
- assert.strictEqual(r.skipped, 1); // only flask>=2.0 counts as a skipped requirement spec
68
- });
69
-
70
- test("pypi: pep503 collapses mixed separators and casing", () => {
71
- assert.strictEqual(pep503("Jinja2"), "jinja2");
72
- assert.strictEqual(pep503("ruamel.yaml"), "ruamel-yaml");
73
- assert.strictEqual(pep503("backports.zoneinfo"), "backports-zoneinfo");
74
- });
75
-
76
- test("pypi: lockfile wins over requirements.txt in the same directory", async () => {
77
- const dir = tmp("py-prec-", {
78
- "poetry.lock": '[[package]]\nname = "requests"\nversion = "2.31.0"\n',
79
- "requirements.txt": "requests==1.0.0\n",
80
- });
81
- const { deps } = await pypi.collect(dir, {});
82
- assert.strictEqual(deps.get("pypi:requests").version, "2.31.0", "poetry.lock should win over requirements.txt");
83
- });
84
-
85
- test("pypi: malformed poetry.lock does not throw", async () => {
86
- const dir = tmp("py-bad-", { "poetry.lock": "this is = not [ valid toml" });
87
- const { deps, warnings } = await pypi.collect(dir, {});
88
- assert.strictEqual(deps.size, 0);
89
- assert.ok(warnings.find(w => w.type === "parse-error"));
90
- });
91
-
92
- /* ---------------- nuget ---------------- */
93
-
94
- test("nuget: packages.lock.json dedups the same id across target frameworks", async () => {
95
- const dir = tmp("nuget-multi-tfm-", {
96
- "packages.lock.json": JSON.stringify({
97
- version: 1,
98
- dependencies: {
99
- "net6.0": { "Newtonsoft.Json": { type: "Direct", resolved: "13.0.1" } },
100
- "net8.0": { "Newtonsoft.Json": { type: "Direct", resolved: "13.0.1" } },
101
- },
102
- }),
103
- });
104
- const { deps } = await nuget.collect(dir, {});
105
- assert.strictEqual(deps.size, 1, "same id+version across TFMs collapses to one entry");
106
- assert.ok(deps.has("nuget:newtonsoft.json"));
107
- });
108
-
109
- test("nuget: Version as a child element is read", async () => {
110
- const dir = tmp("nuget-child-", {
111
- "app.csproj": `<Project Sdk="Microsoft.NET.Sdk"><ItemGroup><PackageReference Include="Serilog"><Version>2.12.0</Version></PackageReference></ItemGroup></Project>`,
112
- });
113
- const { deps } = await nuget.collect(dir, {});
114
- assert.strictEqual(deps.get("nuget:serilog").version, "2.12.0");
115
- });
116
-
117
- test("nuget: packages.config collected via codec", async () => {
118
- const dir = tmp("nuget-cfg-", {
119
- "packages.config": `<?xml version="1.0"?><packages><package id="EntityFramework" version="6.4.4" /></packages>`,
120
- });
121
- const { deps } = await nuget.collect(dir, {});
122
- assert.ok(deps.has("nuget:entityframework"));
123
- assert.strictEqual(deps.get("nuget:entityframework").name, "EntityFramework");
124
- });
125
-
126
- test("nuget: malformed packages.lock.json does not throw", async () => {
127
- const dir = tmp("nuget-bad-", { "packages.lock.json": "{ broken" });
128
- const { deps, warnings } = await nuget.collect(dir, {});
129
- assert.strictEqual(deps.size, 0);
130
- assert.ok(warnings.find(w => w.type === "parse-error"));
131
- });
@@ -1,119 +0,0 @@
1
- /**
2
- * Cross-ecosystem integration tests — exercise all codecs together the way the
3
- * orchestrator does: detect → collect → merge into one Map → shared services.
4
- */
5
- const test = require("node:test");
6
- const assert = require("node:assert");
7
- const path = require("path");
8
- const { allCodecs, getCodec, detectCodecs } = require("../lib/codecs");
9
- const { assertCodecShape } = require("../lib/codecs/codec.interface");
10
- const { osvEcosystemFor, osvPkgName } = require("../lib/osv");
11
- const { findEolProduct } = require("../lib/outdated");
12
- const { generateHtmlReport } = require("../lib/cve-report");
13
-
14
- const POLY = path.join(__dirname, "fixtures", "polyglot");
15
-
16
- // Collect every active codec into one Map, exactly like fad-checker.js main().
17
- async function collectAll(dir) {
18
- const resolved = new Map();
19
- const warnings = [];
20
- for (const id of detectCodecs(dir).map(c => c.id)) {
21
- if (id === "yarn") continue;
22
- const { deps, warnings: w } = await getCodec(id).collect(dir, {});
23
- for (const [k, v] of deps) resolved.set(k, v);
24
- if (w) warnings.push(...w);
25
- }
26
- return { resolved, warnings };
27
- }
28
-
29
- test("every registered codec satisfies the interface contract", () => {
30
- for (const c of allCodecs()) assertCodecShape(c);
31
- });
32
-
33
- test("detectCodecs finds all five ecosystems in the polyglot tree", () => {
34
- const ids = detectCodecs(POLY).map(c => c.id).sort();
35
- assert.deepStrictEqual(ids, ["composer", "maven", "npm", "nuget", "pypi"]);
36
- });
37
-
38
- test("collecting all codecs merges into one Map with NO coordKey collisions", async () => {
39
- const { resolved } = await collectAll(POLY);
40
- // Every key unique (Map guarantees) AND every record's coordKey === its key.
41
- for (const [k, d] of resolved) {
42
- assert.strictEqual(d.coordKey, k, `coordKey mismatch for ${k}`);
43
- assert.ok(d.ecosystem, `missing ecosystem for ${k}`);
44
- assert.ok(d.name, `missing name for ${k}`);
45
- }
46
- // All five ecosystems represented.
47
- const ecos = new Set([...resolved.values()].map(d => d.ecosystem));
48
- for (const e of ["maven", "npm", "composer", "pypi", "nuget"]) assert.ok(ecos.has(e), `missing ${e} deps`);
49
- // Spot-check one coord per ecosystem.
50
- assert.ok(resolved.has("org.apache.logging.log4j:log4j-core"));
51
- assert.ok(resolved.has("npm:lodash"));
52
- assert.ok(resolved.has("composer:guzzlehttp/guzzle"));
53
- assert.ok(resolved.has("pypi:requests"));
54
- assert.ok(resolved.has("nuget:newtonsoft.json"));
55
- });
56
-
57
- test("coordKeys from different ecosystems never collide even for same bare name", () => {
58
- const { coordKeyFor } = require("../lib/dep-record");
59
- const keys = [
60
- coordKeyFor("maven", "org.x", "thing"),
61
- coordKeyFor("npm", "", "thing"),
62
- coordKeyFor("composer", "vendor", "thing"),
63
- coordKeyFor("pypi", "", "thing"),
64
- coordKeyFor("nuget", "", "thing"),
65
- ];
66
- assert.strictEqual(new Set(keys).size, keys.length, `collision among ${keys.join(", ")}`);
67
- });
68
-
69
- test("osv ecosystem + package name are correct for every codec", () => {
70
- const cases = [
71
- { dep: { ecosystem: "maven", namespace: "org.apache", name: "log4j-core", groupId: "org.apache", artifactId: "log4j-core" }, eco: "Maven", pkg: "org.apache:log4j-core" },
72
- { dep: { ecosystem: "npm", namespace: "", name: "lodash", artifactId: "lodash" }, eco: "npm", pkg: "lodash" },
73
- { dep: { ecosystem: "composer", namespace: "guzzlehttp", name: "guzzle" }, eco: "Packagist", pkg: "guzzlehttp/guzzle" },
74
- { dep: { ecosystem: "pypi", namespace: "", name: "requests" }, eco: "PyPI", pkg: "requests" },
75
- { dep: { ecosystem: "nuget", namespace: "", name: "Newtonsoft.Json" }, eco: "NuGet", pkg: "Newtonsoft.Json" },
76
- ];
77
- for (const c of cases) {
78
- assert.strictEqual(osvEcosystemFor(c.dep), c.eco, `osv eco for ${c.dep.ecosystem}`);
79
- assert.strictEqual(osvPkgName(c.dep), c.pkg, `osv pkg for ${c.dep.ecosystem}`);
80
- }
81
- });
82
-
83
- test("findEolProduct dispatches per ecosystem to the right product", () => {
84
- assert.strictEqual(findEolProduct({ ecosystem: "maven", groupId: "org.apache.logging.log4j", artifactId: "log4j-core", namespace: "org.apache.logging.log4j", name: "log4j-core" })?.product, "log4j");
85
- assert.strictEqual(findEolProduct({ ecosystem: "npm", artifactId: "angular", name: "angular" })?.product, "angularjs");
86
- assert.strictEqual(findEolProduct({ ecosystem: "composer", namespace: "symfony", name: "console" })?.product, "symfony");
87
- assert.strictEqual(findEolProduct({ ecosystem: "pypi", name: "django" })?.product, "django");
88
- assert.strictEqual(findEolProduct({ ecosystem: "nuget", name: "Microsoft.EntityFrameworkCore" })?.product, "efcore");
89
- // unknown → null
90
- assert.strictEqual(findEolProduct({ ecosystem: "pypi", name: "totally-unknown-pkg" }), null);
91
- });
92
-
93
- test("generateHtmlReport renders findings from every ecosystem without throwing", async () => {
94
- const { resolved } = await collectAll(POLY);
95
- const deps = [...resolved.values()];
96
- const mk = d => ({ dep: d, cve: { id: `CVE-TEST-${d.ecosystem}`, severity: "HIGH", score: 7.5, description: `vuln in ${d.name}` }, source: "osv", confidence: "exact" });
97
- const html = generateHtmlReport({
98
- cveMatches: deps.map(mk),
99
- eolResults: [], obsoleteResults: [], outdatedResults: [],
100
- resolvedDeps: resolved,
101
- projectInfo: { name: "polyglot", generatedAt: "2026-05-29", toolVersion: "test" },
102
- });
103
- assert.ok(html.startsWith("<!doctype html>"));
104
- // Each ecosystem's section label should appear.
105
- for (const label of ["Maven", "npm", "Composer", "PyPI", "NuGet"]) {
106
- assert.ok(html.includes(label), `report should mention ${label}`);
107
- }
108
- // Ecosystem-tagged coordinates render (non-maven get a prefix).
109
- assert.ok(html.includes("composer:") || html.includes("guzzlehttp/guzzle"));
110
- });
111
-
112
- test("every codec recipe renders a non-empty snippet from a sample finding", () => {
113
- const sample = [{ groupId: "g", artifactId: "pkg", fixVersion: "1.2.3" }];
114
- for (const c of allCodecs()) {
115
- const out = c.recipe.snippet(sample);
116
- assert.strictEqual(typeof out, "string");
117
- assert.ok(out.length > 0, `${c.id} recipe snippet empty`);
118
- }
119
- });
@@ -1,92 +0,0 @@
1
- const test = require("node:test");
2
- const assert = require("node:assert");
3
- const path = require("path");
4
- const { assertCodecShape } = require("../lib/codecs/codec.interface");
5
-
6
- const COMPLETE_STUB = {
7
- id: "x", label: "X", osvEcosystem: "npm",
8
- manifestNames: ["x.json"],
9
- detect: () => false,
10
- collect: async () => ({ deps: new Map(), warnings: [] }),
11
- coordKey: d => `x:${d.name}`,
12
- formatCoord: d => d.name,
13
- osvPackageName: d => d.name,
14
- checkRegistry: async () => ({ outdated: [], deprecated: [] }),
15
- resolveEolProduct: () => null,
16
- recipe: { label: "X", pinSection: "", pinIntro: () => "", snippet: () => "", directSection: "" },
17
- nativeScanners: [],
18
- };
19
-
20
- test("assertCodecShape accepts a complete codec stub", () => {
21
- assert.doesNotThrow(() => assertCodecShape(COMPLETE_STUB));
22
- });
23
-
24
- test("assertCodecShape rejects a codec missing a required method", () => {
25
- assert.throws(() => assertCodecShape({ id: "y" }), /missing|y/i);
26
- });
27
-
28
- const maven = require("../lib/codecs/maven.codec");
29
-
30
- test("maven codec detects the simple fixture and collects deps with bare g:a coordKeys", async () => {
31
- const dir = path.join(__dirname, "fixtures", "simple");
32
- assert.strictEqual(maven.detect(dir), true);
33
- const { deps } = await maven.collect(dir, {});
34
- assert.ok(deps.size > 0);
35
- for (const [k, d] of deps) {
36
- assert.strictEqual(d.ecosystem, "maven");
37
- assert.strictEqual(d.coordKey, k);
38
- assert.ok(!k.startsWith("npm:") && !k.startsWith("nuget:"), `maven key ${k} must stay bare`);
39
- }
40
- });
41
-
42
- const npm = require("../lib/codecs/npm.codec");
43
-
44
- test("npm codec collects from monorepo-mixed with npm: coordKeys", async () => {
45
- const dir = path.join(__dirname, "fixtures", "monorepo-mixed");
46
- assert.strictEqual(npm.detect(dir), true);
47
- const { deps } = await npm.collect(dir, {});
48
- assert.ok(deps.size > 0);
49
- for (const [k, d] of deps) {
50
- assert.ok(k.startsWith("npm:"), `key ${k} should be npm-namespaced`);
51
- assert.strictEqual(d.ecosystem, "npm");
52
- assert.strictEqual(d.coordKey, k);
53
- }
54
- });
55
-
56
- test("yarn codec is a no-op collector (npm codec does the JS scan)", async () => {
57
- const yarn = require("../lib/codecs/yarn.codec");
58
- assert.strictEqual(yarn.id, "yarn");
59
- const { deps } = await yarn.collect("/whatever", {});
60
- assert.strictEqual(deps.size, 0);
61
- });
62
-
63
- const { getCodec, allCodecs, detectCodecs } = require("../lib/codecs");
64
-
65
- test("registry exposes maven/npm/yarn and validates their shape", () => {
66
- const ids = allCodecs().map(c => c.id).sort();
67
- assert.deepStrictEqual(ids, ["composer", "maven", "npm", "nuget", "pypi", "yarn"]);
68
- for (const c of allCodecs()) assertCodecShape(c);
69
- assert.strictEqual(getCodec("maven").id, "maven");
70
- assert.strictEqual(getCodec("nope"), null);
71
- });
72
-
73
- test("detectCodecs finds maven+npm on monorepo-mixed, not yarn duplicate", () => {
74
- const dir = path.join(__dirname, "fixtures", "monorepo-mixed");
75
- const detected = detectCodecs(dir).map(c => c.id);
76
- assert.ok(detected.includes("maven"));
77
- assert.ok(detected.includes("npm"));
78
- assert.ok(!detected.includes("yarn"));
79
- });
80
-
81
- const recipes = require("../lib/codecs/recipes");
82
-
83
- test("each codec recipe exposes label + snippet function", () => {
84
- for (const c of allCodecs()) {
85
- assert.strictEqual(typeof c.recipe.label, "string");
86
- assert.strictEqual(typeof c.recipe.snippet, "function");
87
- }
88
- // snippet output matches the legacy format
89
- const xml = recipes.maven.snippet([{ groupId: "g", artifactId: "a", fixVersion: "1.0" }]);
90
- assert.match(xml, /<dependencyManagement>/);
91
- assert.match(xml, /<artifactId>a<\/artifactId>/);
92
- });