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
package/test/pypi.test.js DELETED
@@ -1,72 +0,0 @@
1
- const test = require("node:test");
2
- const assert = require("node:assert");
3
- const path = require("path");
4
- const { pep503, parsePoetryLock, parsePipfileLock, parseUvLock, parseRequirementsTxt } = require("../lib/codecs/pypi/parse");
5
-
6
- const F = n => path.join(__dirname, "fixtures", n);
7
-
8
- test("pep503 normalizes names (lowercase, collapse separators to -)", () => {
9
- assert.strictEqual(pep503("Flask-SQLAlchemy"), "flask-sqlalchemy");
10
- assert.strictEqual(pep503("zope.interface"), "zope-interface");
11
- assert.strictEqual(pep503("My__Pkg"), "my-pkg");
12
- });
13
-
14
- test("parsePoetryLock returns PEP503 names + versions", () => {
15
- const r = parsePoetryLock(F("python-poetry/poetry.lock"));
16
- const m = Object.fromEntries(r.deps.map(d => [d.name, d.version]));
17
- assert.strictEqual(m["requests"], "2.31.0");
18
- assert.strictEqual(m["flask-sqlalchemy"], "3.0.5"); // normalized
19
- });
20
-
21
- test("parsePipfileLock splits default/develop, strips ==", () => {
22
- const r = parsePipfileLock(F("python-pipenv/Pipfile.lock"));
23
- const m = Object.fromEntries(r.deps.map(d => [d.name, d]));
24
- assert.strictEqual(m["django"].version, "4.2.0");
25
- assert.strictEqual(m["django"].scope, "prod");
26
- assert.strictEqual(m["pytest"].scope, "dev");
27
- });
28
-
29
- test("parseUvLock reads [[package]]", () => {
30
- const r = parseUvLock(F("python-uv/uv.lock"));
31
- assert.strictEqual(r.deps.find(d => d.name === "numpy").version, "1.26.0");
32
- });
33
-
34
- test("parseRequirementsTxt keeps == pins, skips ranges/flags/comments", () => {
35
- const r = parseRequirementsTxt(F("python-reqs/requirements.txt"));
36
- const names = r.deps.map(d => d.name).sort();
37
- assert.deepStrictEqual(names, ["fastapi", "urllib3"]);
38
- assert.strictEqual(r.skipped, 1); // flask>=2.0
39
- });
40
-
41
- const { pypiToFindings } = require("../lib/codecs/pypi/registry");
42
- test("pypiToFindings extracts latest, yanked-for-version, inactive classifier", () => {
43
- const data = {
44
- info: { version: "2.1.0", classifiers: ["Development Status :: 7 - Inactive"] },
45
- releases: { "2.0.4": [{ yanked: true, yanked_reason: "security" }], "2.1.0": [{ yanked: false }] },
46
- };
47
- const f = pypiToFindings(data, { version: "2.0.4" });
48
- assert.strictEqual(f.outdated.latest, "2.1.0");
49
- assert.strictEqual(f.yanked.reason, "security");
50
- assert.strictEqual(f.inactive, true);
51
- const f2 = pypiToFindings(data, { version: "2.1.0" });
52
- assert.strictEqual(f2.yanked, null);
53
- assert.strictEqual(f2.outdated, null);
54
- });
55
-
56
- const pypi = require("../lib/codecs/pypi.codec");
57
- const { assertCodecShape } = require("../lib/codecs/codec.interface");
58
- test("pypi codec: shape, detect, collect, coordKey pypi:<name>", async () => {
59
- assertCodecShape(pypi);
60
- assert.strictEqual(pypi.detect(F("python-poetry")), true);
61
- const { deps } = await pypi.collect(F("python-poetry"), {});
62
- const r = deps.get("pypi:requests");
63
- assert.ok(r);
64
- assert.strictEqual(r.ecosystem, "pypi");
65
- assert.strictEqual(pypi.osvPackageName(r), "requests");
66
- });
67
- test("pypi collect: requirements.txt fallback warns + scans pins only", async () => {
68
- const { deps, warnings } = await pypi.collect(F("python-reqs"), {});
69
- assert.ok(deps.has("pypi:fastapi"));
70
- assert.ok(!deps.has("pypi:flask"));
71
- assert.ok(warnings.find(w => w.type === "no-lockfile"));
72
- });
package/test/snyk.test.js DELETED
@@ -1,64 +0,0 @@
1
- const { test } = require("node:test");
2
- const assert = require("node:assert/strict");
3
- const { parseSnykResults, parseSnykStdout, mergeWithFadResults } = require("../lib/snyk");
4
-
5
- const snykSample = {
6
- vulnerabilities: [
7
- {
8
- id: "SNYK-JAVA-LOG4J-2314720",
9
- packageName: "org.apache.logging.log4j:log4j-core",
10
- version: "2.14.0",
11
- severity: "critical",
12
- cvssScore: 10,
13
- title: "Remote Code Execution",
14
- fixedIn: ["2.15.0", "2.16.0"],
15
- identifiers: { CVE: ["CVE-2021-44228"] },
16
- },
17
- {
18
- id: "SNYK-JAVA-JACKSON-2421244",
19
- packageName: "com.fasterxml.jackson.core:jackson-databind",
20
- version: "2.13.0",
21
- severity: "high",
22
- cvssScore: 7.5,
23
- title: "Denial of Service",
24
- fixedIn: ["2.13.5"],
25
- identifiers: { CVE: ["CVE-2022-42003"] },
26
- },
27
- ],
28
- };
29
-
30
- test("parseSnykStdout accepts a JSON object or array", () => {
31
- assert.deepEqual(parseSnykStdout(""), []);
32
- const arr = parseSnykStdout(JSON.stringify([snykSample]));
33
- assert.equal(arr.length, 1);
34
- const obj = parseSnykStdout(JSON.stringify(snykSample));
35
- assert.equal(obj.length, 1);
36
- });
37
-
38
- test("parseSnykResults normalises to fad-checker match shape", () => {
39
- const out = parseSnykResults(snykSample);
40
- assert.equal(out.length, 2);
41
- assert.equal(out[0].dep.groupId, "org.apache.logging.log4j");
42
- assert.equal(out[0].dep.artifactId, "log4j-core");
43
- assert.equal(out[0].cve.id, "CVE-2021-44228");
44
- assert.equal(out[0].cve.severity, "CRITICAL");
45
- assert.equal(out[0].cve.fixVersion, "2.15.0");
46
- assert.equal(out[0].source, "snyk");
47
- });
48
-
49
- test("mergeWithFadResults tags overlap as 'both' and keeps Snyk-only as 'snyk'", () => {
50
- const fadMatches = [
51
- {
52
- dep: { groupId: "org.apache.logging.log4j", artifactId: "log4j-core", version: "2.14.0" },
53
- cve: { id: "CVE-2021-44228", severity: "CRITICAL" },
54
- confidence: "exact",
55
- },
56
- ];
57
- const snykMatches = parseSnykResults(snykSample);
58
- const merged = mergeWithFadResults(fadMatches, snykMatches);
59
- assert.equal(merged.length, 2);
60
- const log4j = merged.find(m => m.cve.id === "CVE-2021-44228");
61
- assert.equal(log4j.source, "both");
62
- const jackson = merged.find(m => m.cve.id === "CVE-2022-42003");
63
- assert.equal(jackson.source, "snyk");
64
- });
@@ -1,305 +0,0 @@
1
- const { test } = require("node:test");
2
- const assert = require("node:assert/strict");
3
- const fs = require("fs");
4
- const os = require("os");
5
- const path = require("path");
6
- const { parsePomXml, resolveProps, buildMgmt, SCOPE_MATRIX, resolveTransitiveDeps } = require("../lib/transitive");
7
-
8
- // Per-test cache dir so we don't poison ~/.fad-checker/poms-cache during tests
9
- function freshCache() {
10
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fad-checker-pom-cache-"));
11
- return dir;
12
- }
13
-
14
- // ---------- POM fixtures expressed as XML strings ----------
15
-
16
- const POM_A = `<?xml version="1.0"?>
17
- <project>
18
- <modelVersion>4.0.0</modelVersion>
19
- <groupId>com.example</groupId>
20
- <artifactId>a</artifactId>
21
- <version>1.0.0</version>
22
- <dependencies>
23
- <dependency>
24
- <groupId>com.example</groupId>
25
- <artifactId>b</artifactId>
26
- <version>2.0.0</version>
27
- </dependency>
28
- <dependency>
29
- <groupId>com.example</groupId>
30
- <artifactId>test-only</artifactId>
31
- <version>3.0.0</version>
32
- <scope>test</scope>
33
- </dependency>
34
- <dependency>
35
- <groupId>com.example</groupId>
36
- <artifactId>optional-lib</artifactId>
37
- <version>4.0.0</version>
38
- <optional>true</optional>
39
- </dependency>
40
- </dependencies>
41
- </project>`;
42
-
43
- const POM_B = `<?xml version="1.0"?>
44
- <project>
45
- <modelVersion>4.0.0</modelVersion>
46
- <groupId>com.example</groupId>
47
- <artifactId>b</artifactId>
48
- <version>2.0.0</version>
49
- <dependencies>
50
- <dependency>
51
- <groupId>com.example</groupId>
52
- <artifactId>c</artifactId>
53
- <version>5.0.0</version>
54
- </dependency>
55
- </dependencies>
56
- </project>`;
57
-
58
- const POM_C = `<?xml version="1.0"?>
59
- <project>
60
- <modelVersion>4.0.0</modelVersion>
61
- <groupId>com.example</groupId>
62
- <artifactId>c</artifactId>
63
- <version>5.0.0</version>
64
- </project>`;
65
-
66
- // Build an in-memory fetcher
67
- function fakeFetcher(responses) {
68
- return async (url) => {
69
- if (responses[url]) {
70
- return { ok: true, status: 200, text: async () => responses[url] };
71
- }
72
- return { ok: false, status: 404, text: async () => "" };
73
- };
74
- }
75
-
76
- const MC = "https://repo1.maven.org/maven2";
77
-
78
- // ---------- Tests ----------
79
-
80
- test("parsePomXml extracts coords, deps, scope, optional, exclusions", async () => {
81
- const xml = `<?xml version="1.0"?>
82
- <project>
83
- <groupId>g</groupId><artifactId>a</artifactId><version>1</version>
84
- <dependencies>
85
- <dependency>
86
- <groupId>org.foo</groupId><artifactId>bar</artifactId><version>2.0</version>
87
- <scope>runtime</scope><optional>true</optional>
88
- <exclusions>
89
- <exclusion><groupId>x</groupId><artifactId>y</artifactId></exclusion>
90
- </exclusions>
91
- </dependency>
92
- </dependencies>
93
- </project>`;
94
- const pom = await parsePomXml(xml);
95
- assert.equal(pom.groupId, "g");
96
- assert.equal(pom.deps[0].scope, "runtime");
97
- assert.equal(pom.deps[0].optional, true);
98
- assert.deepEqual(pom.deps[0].exclusions, [{ groupId: "x", artifactId: "y" }]);
99
- });
100
-
101
- test("parsePomXml handles parent + inherits groupId/version from it", async () => {
102
- const xml = `<?xml version="1.0"?>
103
- <project>
104
- <parent><groupId>g</groupId><artifactId>p</artifactId><version>3.0</version></parent>
105
- <artifactId>child</artifactId>
106
- </project>`;
107
- const pom = await parsePomXml(xml);
108
- assert.equal(pom.groupId, "g");
109
- assert.equal(pom.version, "3.0");
110
- assert.equal(pom.parent.artifactId, "p");
111
- });
112
-
113
- test("resolveProps substitutes ${prop} including project.* builtins", () => {
114
- const props = { "spring.version": "5.3.20", "log4j": "2.20.0" };
115
- const builtins = { "project.version": "1.0" };
116
- assert.equal(resolveProps("${spring.version}", props, builtins), "5.3.20");
117
- assert.equal(resolveProps("${project.version}", props, builtins), "1.0");
118
- assert.equal(resolveProps("v=${log4j}", props, builtins), "v=2.20.0");
119
- assert.equal(resolveProps("${unknown}", props, builtins), "${unknown}");
120
- });
121
-
122
- test("resolveProps follows chained property references", () => {
123
- const props = { a: "${b}", b: "${c}", c: "final" };
124
- assert.equal(resolveProps("${a}", props, {}), "final");
125
- });
126
-
127
- test("SCOPE_MATRIX implements Maven's scope-propagation rules", () => {
128
- // compile direct + compile transitive → compile
129
- assert.equal(SCOPE_MATRIX.compile.compile, "compile");
130
- // compile + runtime → runtime
131
- assert.equal(SCOPE_MATRIX.compile.runtime, "runtime");
132
- // compile + test → not propagated
133
- assert.equal(SCOPE_MATRIX.compile.test, null);
134
- // test + compile → test
135
- assert.equal(SCOPE_MATRIX.test.compile, "test");
136
- // provided + runtime → provided
137
- assert.equal(SCOPE_MATRIX.provided.runtime, "provided");
138
- });
139
-
140
- test("buildMgmt indexes depMgmt by g:a", () => {
141
- const m = buildMgmt([
142
- { groupId: "g", artifactId: "a", version: "1.0" },
143
- { groupId: "h", artifactId: "b", version: "2.0" },
144
- ]);
145
- assert.equal(m.size, 2);
146
- assert.equal(m.get("g:a").version, "1.0");
147
- });
148
-
149
- test("resolveTransitiveDeps walks A→B→C, skips test + optional", async () => {
150
- const responses = {
151
- [`${MC}/com/example/a/1.0.0/a-1.0.0.pom`]: POM_A,
152
- [`${MC}/com/example/b/2.0.0/b-2.0.0.pom`]: POM_B,
153
- [`${MC}/com/example/c/5.0.0/c-5.0.0.pom`]: POM_C,
154
- };
155
- const transitives = await resolveTransitiveDeps(
156
- [{ groupId: "com.example", artifactId: "a", version: "1.0.0", scope: "compile" }],
157
- { fetcher: fakeFetcher(responses), verbose: false, cacheDir: freshCache() }
158
- );
159
- // b is the direct dep of a → it IS a transitive (we excluded `a` itself
160
- // from the result earlier). c is b's transitive.
161
- const keys = [...transitives.keys()];
162
- assert.ok(keys.includes("com.example:b"), `expected b in transitives, got ${keys.join(",")}`);
163
- assert.ok(keys.includes("com.example:c"), `expected c in transitives, got ${keys.join(",")}`);
164
- // test scope must NOT propagate
165
- assert.ok(!keys.includes("com.example:test-only"), "test-scope dep should not propagate");
166
- // optional must NOT propagate
167
- assert.ok(!keys.includes("com.example:optional-lib"), "optional dep should not propagate");
168
- });
169
-
170
- test("resolveTransitiveDeps applies root depMgmt version overrides", async () => {
171
- const POM_X = `<?xml version="1.0"?>
172
- <project>
173
- <groupId>g</groupId><artifactId>x</artifactId><version>1.0</version>
174
- <dependencies>
175
- <dependency>
176
- <groupId>g</groupId><artifactId>y</artifactId><version>5.0</version>
177
- </dependency>
178
- </dependencies>
179
- </project>`;
180
- const POM_Y_5 = `<?xml version="1.0"?>
181
- <project><groupId>g</groupId><artifactId>y</artifactId><version>5.0</version></project>`;
182
- const POM_Y_OVERRIDE = `<?xml version="1.0"?>
183
- <project><groupId>g</groupId><artifactId>y</artifactId><version>9.9</version></project>`;
184
- const responses = {
185
- [`${MC}/g/x/1.0/x-1.0.pom`]: POM_X,
186
- [`${MC}/g/y/5.0/y-5.0.pom`]: POM_Y_5,
187
- [`${MC}/g/y/9.9/y-9.9.pom`]: POM_Y_OVERRIDE,
188
- };
189
- const rootMgmt = new Map([["g:y", { version: "9.9" }]]);
190
- const transitives = await resolveTransitiveDeps(
191
- [{ groupId: "g", artifactId: "x", version: "1.0", scope: "compile" }],
192
- { fetcher: fakeFetcher(responses), rootDepMgmt: rootMgmt, verbose: false, cacheDir: freshCache() }
193
- );
194
- const y = transitives.get("g:y");
195
- assert.ok(y, "y should be resolved");
196
- assert.equal(y.version, "9.9", "root depMgmt should override transitive declared version");
197
- });
198
-
199
- test("resolveTransitiveDeps honours <exclusion> blocks", async () => {
200
- const POM_ROOT = `<?xml version="1.0"?>
201
- <project>
202
- <groupId>g</groupId><artifactId>root</artifactId><version>1</version>
203
- <dependencies>
204
- <dependency>
205
- <groupId>g</groupId><artifactId>mid</artifactId><version>1</version>
206
- <exclusions>
207
- <exclusion><groupId>g</groupId><artifactId>leaf</artifactId></exclusion>
208
- </exclusions>
209
- </dependency>
210
- </dependencies>
211
- </project>`;
212
- const POM_MID = `<?xml version="1.0"?>
213
- <project>
214
- <groupId>g</groupId><artifactId>mid</artifactId><version>1</version>
215
- <dependencies>
216
- <dependency><groupId>g</groupId><artifactId>leaf</artifactId><version>1</version></dependency>
217
- <dependency><groupId>g</groupId><artifactId>kept</artifactId><version>1</version></dependency>
218
- </dependencies>
219
- </project>`;
220
- const POM_LEAF = `<?xml version="1.0"?>
221
- <project><groupId>g</groupId><artifactId>leaf</artifactId><version>1</version></project>`;
222
- const POM_KEPT = `<?xml version="1.0"?>
223
- <project><groupId>g</groupId><artifactId>kept</artifactId><version>1</version></project>`;
224
- const responses = {
225
- [`${MC}/g/root/1/root-1.pom`]: POM_ROOT,
226
- [`${MC}/g/mid/1/mid-1.pom`]: POM_MID,
227
- [`${MC}/g/leaf/1/leaf-1.pom`]: POM_LEAF,
228
- [`${MC}/g/kept/1/kept-1.pom`]: POM_KEPT,
229
- };
230
- const transitives = await resolveTransitiveDeps(
231
- [{ groupId: "g", artifactId: "root", version: "1", scope: "compile", exclusions: [{ groupId: "g", artifactId: "leaf" }] }],
232
- { fetcher: fakeFetcher(responses), verbose: false, cacheDir: freshCache() }
233
- );
234
- const keys = [...transitives.keys()];
235
- assert.ok(keys.includes("g:mid"), "mid should be there");
236
- assert.ok(keys.includes("g:kept"), "kept should be there");
237
- assert.ok(!keys.includes("g:leaf"), `leaf should be excluded, got ${keys.join(",")}`);
238
- });
239
-
240
- test("resolveTransitiveDeps stops at maxDepth", async () => {
241
- const POM_R = `<?xml version="1.0"?><project>
242
- <groupId>g</groupId><artifactId>r</artifactId><version>1</version>
243
- <dependencies><dependency><groupId>g</groupId><artifactId>l1</artifactId><version>1</version></dependency></dependencies>
244
- </project>`;
245
- const POM_L1 = `<?xml version="1.0"?><project>
246
- <groupId>g</groupId><artifactId>l1</artifactId><version>1</version>
247
- <dependencies><dependency><groupId>g</groupId><artifactId>l2</artifactId><version>1</version></dependency></dependencies>
248
- </project>`;
249
- const POM_L2 = `<?xml version="1.0"?><project>
250
- <groupId>g</groupId><artifactId>l2</artifactId><version>1</version>
251
- <dependencies><dependency><groupId>g</groupId><artifactId>l3</artifactId><version>1</version></dependency></dependencies>
252
- </project>`;
253
- const POM_L3 = `<?xml version="1.0"?><project><groupId>g</groupId><artifactId>l3</artifactId><version>1</version></project>`;
254
- const responses = {
255
- [`${MC}/g/r/1/r-1.pom`]: POM_R,
256
- [`${MC}/g/l1/1/l1-1.pom`]: POM_L1,
257
- [`${MC}/g/l2/1/l2-1.pom`]: POM_L2,
258
- [`${MC}/g/l3/1/l3-1.pom`]: POM_L3,
259
- };
260
- const transitives = await resolveTransitiveDeps(
261
- [{ groupId: "g", artifactId: "r", version: "1", scope: "compile" }],
262
- { fetcher: fakeFetcher(responses), maxDepth: 2, cacheDir: freshCache() }
263
- );
264
- const keys = [...transitives.keys()];
265
- assert.ok(keys.includes("g:l1"));
266
- assert.ok(keys.includes("g:l2"));
267
- assert.ok(!keys.includes("g:l3"), `l3 is at depth 3, should not be present at maxDepth=2; got ${keys.join(",")}`);
268
- });
269
-
270
- test("resolveTransitiveDeps nearest-wins on version conflict", async () => {
271
- // root → a@1 → c@5
272
- // root → c@9 (declared directly under root via b path)
273
- // Result: c@9 wins because it's at depth 1 vs c@5 at depth 2
274
- // But here we're testing the BFS, not the root-direct resolution. Setup:
275
- // root has two deps: x@1 (which has y@5), and y@9 directly.
276
- const POM_ROOT2 = `<?xml version="1.0"?><project>
277
- <groupId>g</groupId><artifactId>root</artifactId><version>1</version>
278
- <dependencies>
279
- <dependency><groupId>g</groupId><artifactId>x</artifactId><version>1</version></dependency>
280
- <dependency><groupId>g</groupId><artifactId>y</artifactId><version>9</version></dependency>
281
- </dependencies>
282
- </project>`;
283
- const POM_X2 = `<?xml version="1.0"?><project>
284
- <groupId>g</groupId><artifactId>x</artifactId><version>1</version>
285
- <dependencies>
286
- <dependency><groupId>g</groupId><artifactId>y</artifactId><version>5</version></dependency>
287
- </dependencies>
288
- </project>`;
289
- const POM_Y_5 = `<?xml version="1.0"?><project><groupId>g</groupId><artifactId>y</artifactId><version>5</version></project>`;
290
- const POM_Y_9 = `<?xml version="1.0"?><project><groupId>g</groupId><artifactId>y</artifactId><version>9</version></project>`;
291
- const responses = {
292
- [`${MC}/g/root/1/root-1.pom`]: POM_ROOT2,
293
- [`${MC}/g/x/1/x-1.pom`]: POM_X2,
294
- [`${MC}/g/y/5/y-5.pom`]: POM_Y_5,
295
- [`${MC}/g/y/9/y-9.pom`]: POM_Y_9,
296
- };
297
- const transitives = await resolveTransitiveDeps([
298
- { groupId: "g", artifactId: "root", version: "1", scope: "compile" },
299
- ], { fetcher: fakeFetcher(responses), verbose: false, cacheDir: freshCache() });
300
- // When the caller (cve-match) passes root's depMgmt with y@9, that pins y.
301
- // Without depMgmt, BFS visits y@9 directly under root first, then sees y again
302
- // from x, and skips it (nearest-wins).
303
- const y = transitives.get("g:y");
304
- assert.ok(y, "y should be in transitives");
305
- });
@@ -1,33 +0,0 @@
1
- const { test } = require("node:test");
2
- const assert = require("node:assert/strict");
3
- const { webjarToNpm } = require("../lib/codecs/npm/collect");
4
-
5
- test("webjarToNpm derives the npm name from org.webjars.npm (deterministic mirror)", () => {
6
- assert.deepEqual(
7
- webjarToNpm({ groupId: "org.webjars.npm", artifactId: "react", version: "18.2.0" }),
8
- { name: "react", version: "18.2.0" },
9
- );
10
- // Scoped packages encode "@scope/name" as "scope__name".
11
- assert.deepEqual(
12
- webjarToNpm({ groupId: "org.webjars.npm", artifactId: "angular__core", version: "17.2.3" }),
13
- { name: "@angular/core", version: "17.2.3" },
14
- );
15
- });
16
-
17
- test("webjarToNpm passes classic org.webjars artifactIds through as-is", () => {
18
- // Classic WebJars are hand-curated; the artifactId is the JS lib name.
19
- assert.deepEqual(
20
- webjarToNpm({ groupId: "org.webjars", artifactId: "angularjs", version: "1.8.3" }),
21
- { name: "angularjs", version: "1.8.3" },
22
- );
23
- assert.equal(webjarToNpm({ groupId: "org.webjars", artifactId: "jquery", version: "3.7.1" }).name, "jquery");
24
- });
25
-
26
- test("webjarToNpm handles bower webjars too", () => {
27
- assert.equal(webjarToNpm({ groupId: "org.webjars.bowergithub.foo", artifactId: "bar", version: "1.0.0" }).name, "bar");
28
- });
29
-
30
- test("webjarToNpm returns null for non-webjar coordinates", () => {
31
- assert.equal(webjarToNpm({ groupId: "org.springframework", artifactId: "spring-core", version: "6.0.0" }), null);
32
- assert.equal(webjarToNpm({ ecosystem: "npm", groupId: "", artifactId: "react", version: "18.0.0" }), null);
33
- });