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,57 +0,0 @@
1
- const { test } = require("node:test");
2
- const assert = require("node:assert/strict");
3
- const { parseMavenVersion, compareMavenVersions, isVersionAffected, parseRange } = require("../lib/maven-version");
4
-
5
- test("parseMavenVersion returns segments", () => {
6
- const v = parseMavenVersion("2.14.0");
7
- assert.equal(v.original, "2.14.0");
8
- assert.deepEqual(v.segments.map(s => s.value), [2, 14, 0]);
9
- });
10
-
11
- test("compareMavenVersions basic numeric ordering", () => {
12
- assert.equal(compareMavenVersions("1.0.0", "1.0.0"), 0);
13
- assert.equal(compareMavenVersions("1.0.0", "1.0.1"), -1);
14
- assert.equal(compareMavenVersions("2.0.0", "1.9.9"), 1);
15
- assert.equal(compareMavenVersions("1.10.0", "1.9.0"), 1);
16
- });
17
-
18
- test("compareMavenVersions handles qualifiers", () => {
19
- assert.equal(compareMavenVersions("1.0.0-SNAPSHOT", "1.0.0"), -1);
20
- assert.equal(compareMavenVersions("1.0.0-rc1", "1.0.0"), -1);
21
- assert.equal(compareMavenVersions("1.0.0-alpha", "1.0.0-beta"), -1);
22
- assert.equal(compareMavenVersions("5.3.20.Final", "5.3.20"), 0);
23
- assert.equal(compareMavenVersions("5.3.20.RELEASE", "5.3.20.Final"), 0);
24
- });
25
-
26
- test("isVersionAffected respects [version, lessThan)", () => {
27
- const spec = { version: "2.0", status: "affected", lessThan: "2.15.0" };
28
- assert.equal(isVersionAffected("2.14.0", spec), true);
29
- assert.equal(isVersionAffected("2.15.0", spec), false, "lessThan is exclusive");
30
- assert.equal(isVersionAffected("1.5.0", spec), false, "below lower bound");
31
- });
32
-
33
- test("isVersionAffected with lessThanOrEqual is inclusive", () => {
34
- const spec = { version: "1.0", status: "affected", lessThanOrEqual: "1.5.0" };
35
- assert.equal(isVersionAffected("1.5.0", spec), true);
36
- assert.equal(isVersionAffected("1.5.1", spec), false);
37
- });
38
-
39
- test("isVersionAffected returns false when status != affected", () => {
40
- const spec = { version: "1.0", lessThan: "2.0", status: "unaffected" };
41
- assert.equal(isVersionAffected("1.5", spec), false);
42
- });
43
-
44
- test("isVersionAffected fail-closed when spec has no version bounds (H1)", () => {
45
- // CVEProject sometimes emits {status:"affected"} stubs with no version
46
- // fields. The matcher must NOT fall through to `return true` — that was
47
- // the H1 cascade.
48
- assert.equal(isVersionAffected("2.14.0", { status: "affected" }), false);
49
- assert.equal(isVersionAffected("2.14.0", {}), false);
50
- assert.equal(isVersionAffected("0.0.1", { status: "affected" }), false);
51
- });
52
-
53
- test("parseRange handles Maven range syntax", () => {
54
- assert.deepEqual(parseRange("1.2.3"), { exact: "1.2.3" });
55
- assert.deepEqual(parseRange("[1.0,2.0)"), { lower: "1.0", lowerInclusive: true, upper: "2.0", upperInclusive: false });
56
- assert.deepEqual(parseRange("(,1.5]"), { lower: null, lowerInclusive: false, upper: "1.5", upperInclusive: true });
57
- });
@@ -1,132 +0,0 @@
1
- /**
2
- * End-to-end test against fixtures/monorepo-mixed:
3
- * - 1 parent pom + 1 BOM module + 2 Maven submodules
4
- * - 1 npm package (package-lock v3) with prod/dev/peer deps
5
- * - 1 yarn-v1 package with prod/dev deps + a private @acme/* dep
6
- *
7
- * Drives the same code paths as `fad-checker --report` minus the network calls.
8
- */
9
- const { test } = require("node:test");
10
- const assert = require("node:assert/strict");
11
- const path = require("path");
12
- const core = require("../lib/core");
13
- const { collectResolvedDeps } = require("../lib/cve-match");
14
- const { collectNpmDeps, hasJsManifests } = require("../lib/codecs/npm/collect");
15
-
16
- const FIX = path.join(__dirname, "fixtures", "monorepo-mixed");
17
-
18
- async function loadMavenTree(src) {
19
- const pomFiles = core.findPomFiles(src);
20
- const meta = core.newMetadataStore();
21
- const propsByPom = {};
22
- for (const pom of pomFiles) await core.parsePom(pom, meta);
23
- for (const pom of Object.keys(meta.byPath)) {
24
- await core.getAllInheritedProps(pom, meta, propsByPom);
25
- }
26
- return { meta, propsByPom, pomFiles };
27
- }
28
-
29
- test("hasJsManifests detects the JS packages under the monorepo", () => {
30
- assert.equal(hasJsManifests(FIX), true);
31
- });
32
-
33
- test("Maven side: parses parent + BOM + submodules", async () => {
34
- const { meta, pomFiles } = await loadMavenTree(FIX);
35
- assert.equal(pomFiles.length, 4); // root + bom + api + worker
36
- assert.ok(meta.byId["com.acme:monorepo-parent"]);
37
- assert.ok(meta.byId["com.acme:common-bom"]);
38
- assert.ok(meta.byId["com.acme:api"]);
39
- assert.ok(meta.byId["com.acme:worker"]);
40
- });
41
-
42
- test("Maven side: BOM-imported versions surface in api's resolved deps", async () => {
43
- const { meta, propsByPom } = await loadMavenTree(FIX);
44
- const resolved = collectResolvedDeps(meta, propsByPom, {});
45
- const databind = resolved.get("com.fasterxml.jackson.core:jackson-databind");
46
- assert.ok(databind, "jackson-databind should be resolved via BOM");
47
- // Version arrives through ${jackson.version} property defined on the root parent
48
- assert.equal(databind.version, "2.9.10");
49
- assert.equal(databind.ecosystem, "maven");
50
- });
51
-
52
- test("Maven side: exclude regex strips com.acme.private but keeps public deps", async () => {
53
- const { meta, propsByPom } = await loadMavenTree(FIX);
54
- const resolved = collectResolvedDeps(meta, propsByPom, {
55
- deps2Exclude: /^com\.acme\.private/,
56
- });
57
- assert.equal(resolved.has("com.acme.private:internal-auth"), false);
58
- assert.ok(resolved.has("com.fasterxml.jackson.core:jackson-databind"));
59
- });
60
-
61
- test("Maven side: --ignore-test drops junit test scope", async () => {
62
- const { meta, propsByPom } = await loadMavenTree(FIX);
63
- const withTest = collectResolvedDeps(meta, propsByPom, {});
64
- const noTest = collectResolvedDeps(meta, propsByPom, { ignoreTest: true });
65
- assert.ok(withTest.has("junit:junit"));
66
- assert.equal(noTest.has("junit:junit"), false);
67
- });
68
-
69
- test("JS side: both packages discovered, namespaced keys, no Maven collision", () => {
70
- const npm = collectNpmDeps(FIX, {});
71
- assert.ok(npm.has("npm:axios"));
72
- assert.ok(npm.has("npm:chalk"));
73
- assert.ok(npm.has("npm:@acme/private-utils"));
74
- // No accidental collision with Maven g:a keys
75
- for (const key of npm.keys()) assert.ok(key.startsWith("npm:"), `${key} should be namespaced`);
76
- });
77
-
78
- test("Combined: merged Map preserves both ecosystems and is queryable as one", async () => {
79
- const { meta, propsByPom } = await loadMavenTree(FIX);
80
- const combined = collectResolvedDeps(meta, propsByPom, {});
81
- const npm = collectNpmDeps(FIX, {});
82
- for (const [k, v] of npm) combined.set(k, v);
83
-
84
- // Maven and npm coords coexist
85
- assert.ok(combined.get("com.fasterxml.jackson.core:jackson-databind"));
86
- assert.ok(combined.get("npm:axios"));
87
- // Distinct ecosystems
88
- assert.equal(combined.get("com.fasterxml.jackson.core:jackson-databind").ecosystem, "maven");
89
- assert.equal(combined.get("npm:axios").ecosystem, "npm");
90
- // Total deps from both sides
91
- assert.ok(combined.size >= 10, `expected ≥10 combined deps, got ${combined.size}`);
92
- });
93
-
94
- test("Combined: shared exclusion regex applies per-ecosystem appropriately", async () => {
95
- // Maven private prefix
96
- const { meta, propsByPom } = await loadMavenTree(FIX);
97
- const mvn = collectResolvedDeps(meta, propsByPom, { deps2Exclude: /^com\.acme\.private/ });
98
- assert.equal(mvn.has("com.acme.private:internal-auth"), false);
99
-
100
- // npm uses a different convention (@scope) — own regex matches it
101
- const npm = collectNpmDeps(FIX, { deps2Exclude: /^@acme\// });
102
- assert.equal(npm.has("npm:@acme/private-utils"), false);
103
- });
104
-
105
- test("Combined: a sample CVE match flow runs without throwing on a mixed Map", async () => {
106
- const { matchDepsAgainstCves } = require("../lib/cve-match");
107
- const { meta, propsByPom } = await loadMavenTree(FIX);
108
- const combined = collectResolvedDeps(meta, propsByPom, {});
109
- const npm = collectNpmDeps(FIX, {});
110
- for (const [k, v] of npm) combined.set(k, v);
111
-
112
- // Tiny stub CVE index. Only Maven-relevant — npm should be silently skipped by matchDepsAgainstCves.
113
- const idx = {
114
- byPackageName: {
115
- "org.apache.logging.log4j:log4j-core": [{
116
- id: "CVE-2021-44228",
117
- severity: "CRITICAL",
118
- score: 10.0,
119
- description: "Log4Shell",
120
- fixVersion: "2.15.0",
121
- ranges: [{ status: "affected", version: "2.0.0", lessThan: "2.15.0", versionType: "maven" }],
122
- vendor: "apache",
123
- product: "log4j-core",
124
- }],
125
- },
126
- byProduct: {},
127
- };
128
- const matches = matchDepsAgainstCves(combined, idx);
129
- assert.ok(matches.length >= 1);
130
- const log4shell = matches.find(m => m.cve.id === "CVE-2021-44228");
131
- assert.ok(log4shell, "log4-core 2.14.0 should match CVE-2021-44228");
132
- });
@@ -1,64 +0,0 @@
1
- const { test } = require("node:test");
2
- const assert = require("node:assert/strict");
3
- const { packumentToFindings } = require("../lib/codecs/npm/registry");
4
-
5
- const dep = (name, version) => ({ ecosystem: "npm", groupId: "", artifactId: name, version });
6
-
7
- test("packumentToFindings flags a deprecated resolved version", () => {
8
- const packument = {
9
- name: "request",
10
- "dist-tags": { latest: "2.88.2" },
11
- versions: {
12
- "2.88.2": { deprecated: "request has been deprecated, see https://github.com/request/request/issues/3142" },
13
- },
14
- time: { "2.88.2": "2020-02-11T00:00:00.000Z" },
15
- };
16
- const { deprecated, outdated } = packumentToFindings(packument, dep("request", "2.88.2"));
17
- assert.ok(deprecated, "should return a deprecated finding");
18
- assert.match(deprecated.reason, /has been deprecated/);
19
- assert.equal(deprecated.source, "npm");
20
- assert.equal(deprecated.dep.artifactId, "request");
21
- // Latest === current, so not outdated.
22
- assert.equal(outdated, null);
23
- });
24
-
25
- test("packumentToFindings extracts a replacement URL from the deprecation message", () => {
26
- const packument = {
27
- "dist-tags": { latest: "1.0.0" },
28
- versions: { "1.0.0": { deprecated: "use the foo package instead, see https://example.com/why" } },
29
- };
30
- const { deprecated } = packumentToFindings(packument, dep("bar", "1.0.0"));
31
- assert.equal(deprecated.replacement, "https://example.com/why");
32
- });
33
-
34
- test("packumentToFindings reports outdated when latest is newer", () => {
35
- const packument = {
36
- "dist-tags": { latest: "3.7.1" },
37
- versions: { "3.6.0": {} },
38
- time: { "3.7.1": "2023-08-28T00:00:00.000Z" },
39
- };
40
- const { deprecated, outdated } = packumentToFindings(packument, dep("jquery", "3.6.0"));
41
- assert.equal(deprecated, null, "not deprecated");
42
- assert.ok(outdated, "should be outdated");
43
- assert.equal(outdated.latest, "3.7.1");
44
- assert.equal(outdated.releaseDate, "2023-08-28");
45
- });
46
-
47
- test("packumentToFindings returns nothing for an up-to-date, non-deprecated dep", () => {
48
- const packument = {
49
- "dist-tags": { latest: "4.18.2" },
50
- versions: { "4.18.2": {} },
51
- };
52
- const { deprecated, outdated } = packumentToFindings(packument, dep("express", "4.18.2"));
53
- assert.equal(deprecated, null);
54
- assert.equal(outdated, null);
55
- });
56
-
57
- test("packumentToFindings tolerates a missing version entry", () => {
58
- // Resolved version not present in the registry (e.g. unpublished) — must not throw.
59
- const packument = { "dist-tags": { latest: "2.0.0" }, versions: { "2.0.0": {} } };
60
- const { deprecated, outdated } = packumentToFindings(packument, dep("ghost", "1.5.0"));
61
- assert.equal(deprecated, null);
62
- assert.ok(outdated, "1.5.0 < 2.0.0 so still outdated");
63
- assert.equal(outdated.latest, "2.0.0");
64
- });
package/test/npm.test.js DELETED
@@ -1,150 +0,0 @@
1
- const { test } = require("node:test");
2
- const assert = require("node:assert/strict");
3
- const path = require("path");
4
- const {
5
- parsePackageJson,
6
- parsePackageLock,
7
- parseYarnLockV1,
8
- findJsManifests,
9
- } = require("../lib/codecs/npm/parse");
10
- const { collectNpmDeps } = require("../lib/codecs/npm/collect");
11
-
12
- const FIX = path.join(__dirname, "fixtures", "monorepo-mixed");
13
-
14
- test("parsePackageJson extracts dependencies + dev + peer scopes", () => {
15
- const res = parsePackageJson(path.join(FIX, "packages", "web-app", "package.json"));
16
- assert.equal(res.packageName, "@acme/web-app");
17
- assert.equal(res.packageVersion, "1.0.0");
18
- const byName = Object.fromEntries(res.deps.map(d => [d.name, d]));
19
- assert.equal(byName.axios.scope, "prod");
20
- assert.equal(byName.jest.scope, "dev");
21
- assert.equal(byName.react.scope, "peer");
22
- });
23
-
24
- test("parsePackageLock v3 enumerates direct + transitive deps with correct scopes", () => {
25
- const res = parsePackageLock(path.join(FIX, "packages", "web-app", "package-lock.json"));
26
- assert.equal(res.lockfileVersion, 3);
27
- const byName = Object.fromEntries(res.deps.map(d => [d.name, d]));
28
- // direct prod
29
- assert.equal(byName.axios.scope, "prod");
30
- assert.equal(byName.axios.version, "0.21.0");
31
- // transitive (depth via nested node_modules) — flat in v3, but here axios's "follow-redirects" lives at the top, depth 0
32
- assert.equal(byName["follow-redirects"].version, "1.13.0");
33
- // dev
34
- assert.equal(byName.jest.scope, "dev");
35
- assert.equal(byName.eslint.scope, "dev");
36
- // peer
37
- assert.equal(byName.react.scope, "peer");
38
- });
39
-
40
- test("parseYarnLockV1 picks up versions and dedupes by (name, version)", () => {
41
- const res = parseYarnLockV1(path.join(FIX, "packages", "cli", "yarn.lock"));
42
- assert.equal(res.lockfileVersion, 1);
43
- const byName = Object.fromEntries(res.deps.map(d => [d.name, d]));
44
- assert.equal(byName.chalk.version, "4.1.2");
45
- assert.equal(byName["ansi-styles"].version, "4.3.0");
46
- assert.equal(byName["@acme/private-utils"].version, "1.0.0");
47
- assert.equal(byName.mocha.version, "9.0.0");
48
- });
49
-
50
- test("findJsManifests discovers both packages, skips node_modules and Maven dirs", () => {
51
- const groups = findJsManifests(FIX);
52
- const dirs = groups.map(g => path.relative(FIX, g.dir)).sort();
53
- assert.ok(dirs.includes(path.join("packages", "web-app")));
54
- assert.ok(dirs.includes(path.join("packages", "cli")));
55
- // No Maven dir should appear
56
- assert.ok(!dirs.some(d => d.startsWith("services" + path.sep)));
57
- });
58
-
59
- test("collectNpmDeps merges both packages and namespaces keys with 'npm:'", () => {
60
- const map = collectNpmDeps(FIX, { verbose: false });
61
- assert.ok(map.has("npm:axios"));
62
- assert.ok(map.has("npm:lodash"));
63
- assert.ok(map.has("npm:chalk"));
64
- assert.ok(map.has("npm:@acme/private-utils"));
65
- const axios = map.get("npm:axios");
66
- assert.equal(axios.ecosystem, "npm");
67
- assert.equal(axios.artifactId, "axios");
68
- assert.equal(axios.groupId, "");
69
- assert.equal(axios.version, "0.21.0");
70
- assert.equal(axios.scope, "prod");
71
- });
72
-
73
- test("collectNpmDeps --ignore-test skips devDependencies (jest, eslint, mocha)", () => {
74
- const map = collectNpmDeps(FIX, { ignoreTest: true, verbose: false });
75
- assert.equal(map.has("npm:jest"), false);
76
- assert.equal(map.has("npm:eslint"), false);
77
- assert.equal(map.has("npm:mocha"), false);
78
- // prod deps still present
79
- assert.ok(map.has("npm:axios"));
80
- assert.ok(map.has("npm:chalk"));
81
- });
82
-
83
- test("collectNpmDeps deps2Exclude regex strips @acme/private-utils", () => {
84
- const map = collectNpmDeps(FIX, { deps2Exclude: /^@acme\//, verbose: false });
85
- assert.equal(map.has("npm:@acme/private-utils"), false);
86
- // public deps still present
87
- assert.ok(map.has("npm:axios"));
88
- assert.ok(map.has("npm:chalk"));
89
- });
90
-
91
- test("collectNpmDeps prefers lockfile (resolved) version over package.json range", () => {
92
- const map = collectNpmDeps(FIX, { verbose: false });
93
- // In web-app, package.json pins "0.21.0" and lockfile says "0.21.0" — verify
94
- // concrete resolved version is the one kept.
95
- const axios = map.get("npm:axios");
96
- assert.equal(/^\d/.test(axios.version), true);
97
- });
98
-
99
- test("collectNpmDeps skips package.json when a lockfile is present in the same dir", () => {
100
- // web-app has both package.json and package-lock.json. Only the lockfile
101
- // should source dep entries; the package.json (which carries ranges like
102
- // "^1.0.0" we can't query OSV with) must be ignored as a source.
103
- const map = collectNpmDeps(FIX, { verbose: false });
104
- const axios = map.get("npm:axios");
105
- assert.ok(axios.lockType?.startsWith("package-lock-v"), `axios should come from lockfile, got lockType=${axios.lockType}`);
106
- // And the only manifestPaths recorded for axios should be the lockfile
107
- for (const p of axios.manifestPaths) {
108
- assert.ok(p.endsWith("package-lock.json") || p.endsWith("yarn.lock"),
109
- `expected only lockfile manifestPaths for axios, got ${p}`);
110
- }
111
- });
112
-
113
- test("collectNpmDeps no-lockfile fallback: warns + scans pinned, skips ranges", () => {
114
- const map = collectNpmDeps(FIX, { verbose: false });
115
- assert.ok(Array.isArray(map.warnings), "warnings array should be present");
116
- const w = map.warnings.find(x => x.type === "no-lockfile" && x.manifestPath.includes("no-lock"));
117
- assert.ok(w, "expected a no-lockfile warning for packages/no-lock/package.json");
118
- assert.match(w.message, /best-effort/, "warning should mention best-effort/partial results");
119
- // Range-only deps must NOT leak into the Map (can't query OSV with "^1.0.0").
120
- assert.equal(map.has("npm:left-pad"), false, "left-pad ^1.0.0 must not be collected (unresolved range)");
121
- // Pinned exact versions ARE now collected best-effort (changed contract).
122
- assert.ok(map.has("npm:semver"), "semver 7.5.0 (pinned) should be collected from no-lock package.json");
123
- assert.equal(map.get("npm:semver").version, "7.5.0");
124
- });
125
-
126
- test("parsePackageLock tags flattened-transitive entries as scope='transitive'", () => {
127
- // follow-redirects is pulled in by axios but is not in the root's direct
128
- // dependency lists — even though npm v3 places it at depth 0 in node_modules,
129
- // it must be reported as transitive.
130
- const res = parsePackageLock(path.join(FIX, "packages", "web-app", "package-lock.json"));
131
- const byName = Object.fromEntries(res.deps.map(d => [d.name, d]));
132
- assert.equal(byName["follow-redirects"].scope, "transitive");
133
- assert.equal(byName.qs.scope, "transitive");
134
- // direct deps stay direct
135
- assert.equal(byName.axios.scope, "prod");
136
- assert.equal(byName.express.scope, "prod");
137
- });
138
-
139
- test("collectNpmDeps sets ecosystemType=npm for package-lock and yarn for yarn.lock", () => {
140
- const map = collectNpmDeps(FIX, { verbose: false });
141
- assert.equal(map.get("npm:axios").ecosystemType, "npm"); // from package-lock.json
142
- assert.equal(map.get("npm:chalk").ecosystemType, "yarn"); // from yarn.lock
143
- });
144
-
145
- test("collectNpmDeps captures peerDependencies (react@17 in web-app)", () => {
146
- const map = collectNpmDeps(FIX, { verbose: false });
147
- const react = map.get("npm:react");
148
- assert.ok(react, "react peer dep should be present");
149
- assert.equal(react.scope, "peer");
150
- });
@@ -1,66 +0,0 @@
1
- const test = require("node:test");
2
- const assert = require("node:assert");
3
- const path = require("path");
4
- const { parsePackagesLockJson, parseCsproj, parsePackagesConfig, parseDirectoryPackagesProps } = require("../lib/codecs/nuget/parse");
5
- const F = n => path.join(__dirname, "fixtures", n);
6
-
7
- test("parsePackagesLockJson reads resolved versions + Direct/Transitive scope", async () => {
8
- const r = await parsePackagesLockJson(F("csharp-lock/packages.lock.json"));
9
- const m = Object.fromEntries(r.deps.map(d => [d.name, d]));
10
- assert.strictEqual(m["Newtonsoft.Json"].version, "13.0.1");
11
- assert.strictEqual(m["System.Buffers"].scope, "transitive");
12
- });
13
-
14
- test("parseDirectoryPackagesProps returns a name→version map (CPM)", async () => {
15
- const m = await parseDirectoryPackagesProps(F("csharp-csproj/Directory.Packages.props"));
16
- assert.strictEqual(m["managed"], "6.0.0"); // keyed lowercase
17
- });
18
-
19
- test("parseCsproj: pinned scanned, floating skipped, CPM resolved against props", async () => {
20
- const cpm = await parseDirectoryPackagesProps(F("csharp-csproj/Directory.Packages.props"));
21
- const r = await parseCsproj(F("csharp-csproj/app.csproj"), cpm);
22
- const m = Object.fromEntries(r.deps.map(d => [d.name, d.version]));
23
- assert.strictEqual(m["Newtonsoft.Json"], "13.0.1");
24
- assert.strictEqual(m["Managed"], "6.0.0"); // resolved via CPM
25
- assert.ok(!("Floating" in m)); // "1.*" skipped
26
- assert.strictEqual(r.skipped, 1);
27
- });
28
-
29
- test("parsePackagesConfig reads legacy id/version", async () => {
30
- const r = await parsePackagesConfig(F("csharp-config/packages.config"));
31
- assert.strictEqual(r.deps.find(d => d.name === "EntityFramework").version, "6.4.4");
32
- });
33
-
34
- const { nugetRegistrationToFindings } = require("../lib/codecs/nuget/registry");
35
- test("nugetRegistrationToFindings extracts latest stable + deprecation for version", () => {
36
- const reg = { items: [ { items: [
37
- { catalogEntry: { version: "13.0.1", deprecation: { reasons: ["Legacy"], alternatePackage: { id: "NewPkg" } } } },
38
- { catalogEntry: { version: "13.0.3" } },
39
- { catalogEntry: { version: "14.0.0-preview" } },
40
- ] } ] };
41
- const f = nugetRegistrationToFindings(reg, { version: "13.0.1" });
42
- assert.strictEqual(f.outdated.latest, "13.0.3");
43
- assert.deepStrictEqual(f.deprecated, { reason: "Legacy", replacement: "NewPkg" });
44
- const f2 = nugetRegistrationToFindings(reg, { version: "13.0.3" });
45
- assert.strictEqual(f2.deprecated, null);
46
- assert.strictEqual(f2.outdated, null);
47
- });
48
-
49
- const nuget = require("../lib/codecs/nuget.codec");
50
- const { assertCodecShape } = require("../lib/codecs/codec.interface");
51
- test("nuget codec: shape, detect, collect lockfile, case-insensitive key", async () => {
52
- assertCodecShape(nuget);
53
- assert.strictEqual(nuget.detect(F("csharp-lock")), true);
54
- const { deps } = await nuget.collect(F("csharp-lock"), {});
55
- const j = deps.get("nuget:newtonsoft.json"); // key lowercased
56
- assert.ok(j);
57
- assert.strictEqual(j.name, "Newtonsoft.Json"); // display keeps original case
58
- assert.strictEqual(nuget.osvPackageName(j), "Newtonsoft.Json");
59
- });
60
- test("nuget codec: csproj uses CPM + skips floating with warning", async () => {
61
- const { deps, warnings } = await nuget.collect(F("csharp-csproj"), {});
62
- assert.ok(deps.has("nuget:newtonsoft.json"));
63
- assert.ok(deps.has("nuget:managed")); // resolved via Directory.Packages.props
64
- assert.ok(!deps.has("nuget:floating"));
65
- assert.ok(warnings.find(w => w.type === "no-lockfile"));
66
- });
package/test/osv.test.js DELETED
@@ -1,62 +0,0 @@
1
- const test = require("node:test");
2
- const assert = require("node:assert");
3
- const { osvEcosystemFor, osvPkgName } = require("../lib/osv");
4
-
5
- test("osvEcosystemFor maps codec ids to OSV ecosystem names", () => {
6
- assert.strictEqual(osvEcosystemFor({ ecosystem: "maven" }), "Maven");
7
- assert.strictEqual(osvEcosystemFor({ ecosystem: "npm" }), "npm");
8
- assert.strictEqual(osvEcosystemFor({ ecosystem: "yarn" }), "npm");
9
- assert.strictEqual(osvEcosystemFor({ ecosystem: "nuget" }), "NuGet");
10
- assert.strictEqual(osvEcosystemFor({ ecosystem: "composer" }), "Packagist");
11
- assert.strictEqual(osvEcosystemFor({ ecosystem: "pypi" }), "PyPI");
12
- });
13
-
14
- test("osvPkgName delegates to codec for maven (g:a) and npm (bare name)", () => {
15
- assert.strictEqual(osvPkgName({ ecosystem: "maven", namespace: "org.apache", name: "log4j", groupId: "org.apache", artifactId: "log4j" }), "org.apache:log4j");
16
- assert.strictEqual(osvPkgName({ ecosystem: "npm", namespace: "", name: "lodash", artifactId: "lodash" }), "lodash");
17
- });
18
-
19
- const { queryOsvForDeps, OSV_CACHE_DIR } = require("../lib/osv");
20
- const { makeDepRecord } = require("../lib/dep-record");
21
- const fs = require("fs");
22
-
23
- // Regression: queryOsvForDeps must send the codec's package name + ecosystem to
24
- // OSV for EVERY ecosystem. A prior bug cherry-picked only groupId/artifactId when
25
- // rebuilding per-version deps, so composer/pypi/nuget queried name=undefined.
26
- test("queryOsvForDeps sends correct package name + ecosystem per codec (mock fetcher)", async () => {
27
- // Purge sentinel cache entries so the mock fetcher is always exercised
28
- // (queryBatch writes a per-dep cache after a live batch — would self-poison).
29
- try {
30
- for (const f of fs.readdirSync(OSV_CACHE_DIR)) {
31
- if (f.includes("9.9.9-fadtest")) fs.unlinkSync(require("path").join(OSV_CACHE_DIR, f));
32
- }
33
- } catch { /* dir may not exist yet */ }
34
- const captured = [];
35
- const fetcher = async (url, opts) => {
36
- if (url.includes("/querybatch")) {
37
- const body = JSON.parse(opts.body);
38
- captured.push(...body.queries);
39
- return { ok: true, json: async () => ({ results: body.queries.map(() => ({ vulns: [] })) }) };
40
- }
41
- return { ok: true, json: async () => ({}) };
42
- };
43
- const deps = new Map();
44
- for (const [eco, ns, name, ver, expName, expEco] of [
45
- ["pypi", "", "django", "9.9.9-fadtest", "django", "PyPI"],
46
- ["composer", "guzzlehttp", "guzzle", "9.9.9-fadtest", "guzzlehttp/guzzle", "Packagist"],
47
- ["nuget", "", "Newtonsoft.Json", "9.9.9-fadtest", "Newtonsoft.Json", "NuGet"],
48
- ["maven", "org.apache", "log4j-core", "9.9.9-fadtest", "org.apache:log4j-core", "Maven"],
49
- ["npm", "", "lodash", "9.9.9-fadtest", "lodash", "npm"],
50
- ]) {
51
- const r = makeDepRecord({ ecosystem: eco, namespace: ns, name, version: ver, manifestPath: "x" });
52
- r._exp = { name: expName, eco: expEco };
53
- deps.set(r.coordKey, r);
54
- }
55
- await queryOsvForDeps(deps, { fetcher });
56
- for (const d of deps.values()) {
57
- const q = captured.find(c => c.package.name === d._exp.name);
58
- assert.ok(q, `expected an OSV query with package.name="${d._exp.name}" (${d.ecosystem})`);
59
- assert.strictEqual(q.package.ecosystem, d._exp.eco, `ecosystem for ${d._exp.name}`);
60
- assert.strictEqual(q.version, d.version);
61
- }
62
- });
@@ -1,101 +0,0 @@
1
- const { test } = require("node:test");
2
- const assert = require("node:assert/strict");
3
- const { checkObsoleteDeps, checkObsolete, findEolProduct, KNOWN_OBSOLETE } = require("../lib/outdated");
4
-
5
- test("known-obsolete.json contains the obvious historical hazards", () => {
6
- assert.ok(KNOWN_OBSOLETE["log4j:log4j"], "log4j 1.x must be flagged");
7
- assert.ok(KNOWN_OBSOLETE["commons-logging:commons-logging"]);
8
- assert.ok(KNOWN_OBSOLETE["org.codehaus.jackson:jackson-databind"] || KNOWN_OBSOLETE["org.codehaus.jackson:jackson-mapper-asl"]);
9
- });
10
-
11
- test("checkObsoleteDeps flags log4j 1.x and jackson 1.x", () => {
12
- const deps = new Map([
13
- ["log4j:log4j", { groupId: "log4j", artifactId: "log4j", version: "1.2.17" }],
14
- ["org.codehaus.jackson:jackson-mapper-asl", { groupId: "org.codehaus.jackson", artifactId: "jackson-mapper-asl", version: "1.9.13" }],
15
- ["com.fasterxml.jackson.core:jackson-databind", { groupId: "com.fasterxml.jackson.core", artifactId: "jackson-databind", version: "2.16.0" }],
16
- ]);
17
- const out = checkObsoleteDeps(deps);
18
- const ids = out.map(o => `${o.dep.groupId}:${o.dep.artifactId}`);
19
- assert.ok(ids.includes("log4j:log4j"));
20
- assert.ok(ids.includes("org.codehaus.jackson:jackson-mapper-asl"));
21
- assert.ok(!ids.includes("com.fasterxml.jackson.core:jackson-databind"));
22
- const log4j = out.find(o => o.dep.artifactId === "log4j");
23
- assert.equal(log4j.severity, "CRITICAL");
24
- });
25
-
26
- test("checkObsoleteDeps deduplicates by g:a", () => {
27
- const deps = new Map([
28
- ["log4j:log4j", { groupId: "log4j", artifactId: "log4j", version: "1.2.17" }],
29
- ]);
30
- // Call twice — should still report once
31
- const out1 = checkObsoleteDeps(deps);
32
- const out2 = checkObsoleteDeps(deps);
33
- assert.equal(out1.length, 1);
34
- assert.equal(out2.length, 1);
35
- });
36
-
37
- test("checkObsolete single-dep returns details or null", () => {
38
- const o = checkObsolete({ groupId: "log4j", artifactId: "log4j", version: "1.2" });
39
- assert.ok(o);
40
- assert.equal(o.severity, "CRITICAL");
41
- assert.equal(checkObsolete({ groupId: "com.fasterxml.jackson.core", artifactId: "jackson-databind" }), null);
42
- });
43
-
44
- test("findEolProduct matches Spring Boot by exact coord and by prefix", () => {
45
- const sb = findEolProduct({ groupId: "org.springframework.boot", artifactId: "spring-boot-starter-parent" });
46
- assert.equal(sb.product, "spring-boot");
47
-
48
- const sbcustom = findEolProduct({ groupId: "org.springframework.boot", artifactId: "spring-boot-starter-anything" });
49
- assert.equal(sbcustom.product, "spring-boot", "prefix-only mapping must still match");
50
- });
51
-
52
- test("findEolProduct picks longest prefix match", () => {
53
- const sec = findEolProduct({ groupId: "org.springframework.security", artifactId: "made-up" });
54
- assert.equal(sec.product, "spring-framework");
55
- assert.equal(sec.label, "Spring Security");
56
- });
57
-
58
- test("findEolProduct maps the npm 'angular' package to AngularJS 1.x", () => {
59
- // The literal npm package named "angular" IS AngularJS (1.x), EOL since 2022.
60
- const a = findEolProduct({ ecosystem: "npm", groupId: "", artifactId: "angular" });
61
- assert.equal(a.product, "angularjs");
62
- assert.equal(a.label, "AngularJS");
63
- });
64
-
65
- test("findEolProduct maps @angular/* scoped packages to modern Angular", () => {
66
- const core = findEolProduct({ ecosystem: "npm", groupId: "", artifactId: "@angular/core" });
67
- assert.equal(core.product, "angular");
68
- const router = findEolProduct({ ecosystem: "npm", groupId: "", artifactId: "@angular/router" });
69
- assert.equal(router.product, "angular");
70
- });
71
-
72
- test("findEolProduct maps react / react-dom / jquery / vue / bootstrap", () => {
73
- assert.equal(findEolProduct({ ecosystem: "npm", artifactId: "react" }).product, "react");
74
- assert.equal(findEolProduct({ ecosystem: "npm", artifactId: "react-dom" }).product, "react");
75
- assert.equal(findEolProduct({ ecosystem: "npm", artifactId: "jquery" }).product, "jquery");
76
- assert.equal(findEolProduct({ ecosystem: "npm", artifactId: "vue" }).product, "vue");
77
- assert.equal(findEolProduct({ ecosystem: "npm", artifactId: "bootstrap" }).product, "bootstrap");
78
- });
79
-
80
- test("findEolProduct returns null for an unmapped npm package", () => {
81
- assert.equal(findEolProduct({ ecosystem: "npm", artifactId: "left-pad" }), null);
82
- // A Maven groupId must never leak into the npm lookup.
83
- assert.equal(findEolProduct({ ecosystem: "npm", artifactId: "org.springframework" }), null);
84
- });
85
-
86
- test("findEolProduct maps WebJars (client-side JS shipped as Maven artifacts)", () => {
87
- // org.webjars:angularjs:1.8.3 — AngularJS 1.x, EOL since 2021.
88
- const ajs = findEolProduct({ groupId: "org.webjars", artifactId: "angularjs", version: "1.8.3" });
89
- assert.ok(ajs, "org.webjars:angularjs must map");
90
- assert.equal(ajs.product, "angularjs");
91
-
92
- assert.equal(findEolProduct({ groupId: "org.webjars", artifactId: "jquery" }).product, "jquery");
93
- assert.equal(findEolProduct({ groupId: "org.webjars", artifactId: "bootstrap" }).product, "bootstrap");
94
- // org.webjars.npm mirrors npm names; scope slash is encoded as "__".
95
- assert.equal(findEolProduct({ groupId: "org.webjars.npm", artifactId: "vue" }).product, "vue");
96
- assert.equal(findEolProduct({ groupId: "org.webjars.npm", artifactId: "angular__core" }).product, "angular");
97
- });
98
-
99
- test("findEolProduct returns null for an unmapped WebJar artifact", () => {
100
- assert.equal(findEolProduct({ groupId: "org.webjars", artifactId: "datatables" }), null);
101
- });