fad-checker 1.0.6 → 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 (79) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +77 -15
  3. package/completions/fad-checker.bash +4 -1
  4. package/completions/fad-checker.zsh +9 -0
  5. package/data/eol-mapping.json +17 -0
  6. package/fad-checker.js +353 -241
  7. package/lib/codecs/codec.interface.js +27 -0
  8. package/lib/codecs/composer/parse.js +59 -0
  9. package/lib/codecs/composer/registry.js +92 -0
  10. package/lib/codecs/composer.codec.js +93 -0
  11. package/lib/codecs/index.js +37 -0
  12. package/lib/codecs/maven.codec.js +71 -0
  13. package/lib/{npm → codecs/npm}/collect.js +58 -35
  14. package/lib/{npm → codecs/npm}/parse.js +114 -10
  15. package/lib/{npm → codecs/npm}/registry.js +4 -3
  16. package/lib/codecs/npm.codec.js +52 -0
  17. package/lib/codecs/nuget/parse.js +75 -0
  18. package/lib/codecs/nuget/registry.js +88 -0
  19. package/lib/codecs/nuget.codec.js +94 -0
  20. package/lib/codecs/pypi/parse.js +163 -0
  21. package/lib/codecs/pypi/registry.js +89 -0
  22. package/lib/codecs/pypi.codec.js +102 -0
  23. package/lib/codecs/recipes.js +108 -0
  24. package/lib/codecs/select.js +27 -0
  25. package/lib/codecs/yarn.codec.js +19 -0
  26. package/lib/core.js +35 -1
  27. package/lib/cve-match.js +18 -11
  28. package/lib/cve-report.js +34 -70
  29. package/lib/dep-record.js +60 -0
  30. package/lib/deps-descriptor.js +110 -0
  31. package/lib/nvd.js +4 -3
  32. package/lib/osv.js +29 -18
  33. package/lib/outdated.js +20 -4
  34. package/lib/retire.js +77 -13
  35. package/lib/transitive.js +3 -3
  36. package/lib/ui.js +87 -0
  37. package/package.json +4 -2
  38. package/CLAUDE.md +0 -129
  39. package/docs/ARCHITECTURE.md +0 -154
  40. package/docs/USAGE.md +0 -179
  41. package/test/core.test.js +0 -153
  42. package/test/cpe.test.js +0 -166
  43. package/test/cve-download.test.js +0 -39
  44. package/test/cve-match.test.js +0 -217
  45. package/test/cve-report.test.js +0 -171
  46. package/test/fixtures/complex-enterprise/api/pom.xml +0 -32
  47. package/test/fixtures/complex-enterprise/build/pom.xml +0 -46
  48. package/test/fixtures/complex-enterprise/dao/pom.xml +0 -37
  49. package/test/fixtures/complex-enterprise/pom.xml +0 -66
  50. package/test/fixtures/complex-enterprise/web/pom.xml +0 -77
  51. package/test/fixtures/cve-samples/cve-non-java.json +0 -19
  52. package/test/fixtures/cve-samples/cve-product-only.json +0 -31
  53. package/test/fixtures/cve-samples/cve-with-packagename.json +0 -37
  54. package/test/fixtures/cve-samples/nvd-log4shell.json +0 -40
  55. package/test/fixtures/cve-samples/nvd-npm-lodash.json +0 -22
  56. package/test/fixtures/monorepo-mixed/libs/common-bom/pom.xml +0 -26
  57. package/test/fixtures/monorepo-mixed/packages/cli/package.json +0 -14
  58. package/test/fixtures/monorepo-mixed/packages/cli/yarn.lock +0 -41
  59. package/test/fixtures/monorepo-mixed/packages/no-lock/package.json +0 -9
  60. package/test/fixtures/monorepo-mixed/packages/web-app/package-lock.json +0 -71
  61. package/test/fixtures/monorepo-mixed/packages/web-app/package.json +0 -17
  62. package/test/fixtures/monorepo-mixed/pom.xml +0 -29
  63. package/test/fixtures/monorepo-mixed/services/api/pom.xml +0 -27
  64. package/test/fixtures/monorepo-mixed/services/worker/pom.xml +0 -28
  65. package/test/fixtures/private-lib-detection/core/pom.xml +0 -26
  66. package/test/fixtures/private-lib-detection/plugin/pom.xml +0 -23
  67. package/test/fixtures/private-lib-detection/pom.xml +0 -35
  68. package/test/fixtures/simple/app/pom.xml +0 -28
  69. package/test/fixtures/simple/lib/pom.xml +0 -18
  70. package/test/fixtures/simple/pom.xml +0 -24
  71. package/test/maven-repo.test.js +0 -111
  72. package/test/maven-version.test.js +0 -57
  73. package/test/monorepo.test.js +0 -132
  74. package/test/npm-registry.test.js +0 -64
  75. package/test/npm.test.js +0 -146
  76. package/test/outdated.test.js +0 -101
  77. package/test/snyk.test.js +0 -64
  78. package/test/transitive.test.js +0 -305
  79. package/test/webjar.test.js +0 -33
package/test/cpe.test.js DELETED
@@ -1,166 +0,0 @@
1
- const { test } = require("node:test");
2
- const assert = require("node:assert/strict");
3
- const fs = require("fs");
4
- const path = require("path");
5
- const {
6
- parseCpe23,
7
- matchVersionRange,
8
- cpeMatchesDep,
9
- evaluateCveForDep,
10
- refineMatchesWithCpe,
11
- } = require("../lib/cpe");
12
-
13
- const FIX = path.join(__dirname, "fixtures", "cve-samples");
14
-
15
- test("parseCpe23 splits all 13 fields and handles escaping", () => {
16
- const c = parseCpe23("cpe:2.3:a:apache:log4j:2.14.0:*:*:*:*:*:*:*");
17
- assert.equal(c.part, "a");
18
- assert.equal(c.vendor, "apache");
19
- assert.equal(c.product, "log4j");
20
- assert.equal(c.version, "2.14.0");
21
- });
22
-
23
- test("parseCpe23 returns null for malformed URI", () => {
24
- assert.equal(parseCpe23("not a cpe"), null);
25
- assert.equal(parseCpe23("cpe:2.3:a:vendor"), null);
26
- assert.equal(parseCpe23(42), null);
27
- });
28
-
29
- test("parseCpe23 handles backslash-escaped colons (e.g. eclipse:vert\\.x)", () => {
30
- const c = parseCpe23("cpe:2.3:a:vendor:weird\\:name:1.0:*:*:*:*:*:*:*");
31
- assert.equal(c.product, "weird:name");
32
- });
33
-
34
- test("matchVersionRange honours versionStartIncluding + versionEndExcluding", () => {
35
- const m = {
36
- criteria: "cpe:2.3:a:apache:log4j:*:*:*:*:*:*:*:*",
37
- vulnerable: true,
38
- versionStartIncluding: "2.0.0",
39
- versionEndExcluding: "2.15.0",
40
- };
41
- assert.equal(matchVersionRange("2.14.1", m), true);
42
- assert.equal(matchVersionRange("2.0.0", m), true); // lower-inclusive
43
- assert.equal(matchVersionRange("2.15.0", m), false); // upper-exclusive
44
- assert.equal(matchVersionRange("1.9.0", m), false);
45
- assert.equal(matchVersionRange("2.17.0", m), false);
46
- });
47
-
48
- test("matchVersionRange honours hard-pinned criteria version", () => {
49
- const m = { criteria: "cpe:2.3:a:apache:log4j:2.14.0:*:*:*:*:*:*:*", vulnerable: true };
50
- assert.equal(matchVersionRange("2.14.0", m), true);
51
- assert.equal(matchVersionRange("2.14.1", m), false);
52
- });
53
-
54
- test("matchVersionRange folds CPE update qualifier into the version pin (H5)", () => {
55
- // CPE 2.3 ":1.0.0:rc1:" describes the 1.0.0-rc1 pre-release. A release dep
56
- // at 1.0.0 must NOT match — that was the H5 cascade.
57
- const beta = { criteria: "cpe:2.3:a:apache:foo:1.0.0:beta1:*:*:*:*:*:*", vulnerable: true };
58
- assert.equal(matchVersionRange("1.0.0", beta), false);
59
- assert.equal(matchVersionRange("1.0.0-beta1", beta), true);
60
-
61
- const rc = { criteria: "cpe:2.3:a:apache:foo:1.0.0:rc1:*:*:*:*:*:*", vulnerable: true };
62
- assert.equal(matchVersionRange("1.0.0", rc), false);
63
- assert.equal(matchVersionRange("1.0.0-rc1", rc), true);
64
- });
65
-
66
- test("cpeMatchesDep rejects short-token vendor leak (M4 mirror of H2)", () => {
67
- const cpe = parseCpe23("cpe:2.3:a:a:log4j-core:*:*:*:*:*:*:*:*");
68
- const dep = { groupId: "com.unrelated.log4j-core", artifactId: "log4j-core", ecosystem: "maven" };
69
- assert.equal(cpeMatchesDep(cpe, dep), false);
70
- });
71
-
72
- test("matchVersionRange returns true for unknown dep version (conservative)", () => {
73
- const m = { criteria: "cpe:2.3:a:apache:log4j:*:*:*:*:*:*:*:*", vulnerable: true, versionEndExcluding: "2.15.0" };
74
- assert.equal(matchVersionRange(null, m), true);
75
- });
76
-
77
- test("cpeMatchesDep — curated map exact maven coord", () => {
78
- const cpe = parseCpe23("cpe:2.3:a:apache:log4j:*:*:*:*:*:*:*:*");
79
- const dep = { groupId: "org.apache.logging.log4j", artifactId: "log4j-core", ecosystem: "maven" };
80
- assert.equal(cpeMatchesDep(cpe, dep), true);
81
- });
82
-
83
- test("cpeMatchesDep — heuristic vendor token in groupId", () => {
84
- const cpe = parseCpe23("cpe:2.3:a:apache:commons-totally-novel:*:*:*:*:*:*:*:*");
85
- const dep = { groupId: "org.apache.commons", artifactId: "commons-totally-novel", ecosystem: "maven" };
86
- assert.equal(cpeMatchesDep(cpe, dep), true);
87
- });
88
-
89
- test("cpeMatchesDep — npm bare name", () => {
90
- const cpe = parseCpe23("cpe:2.3:a:lodash:lodash:*:*:*:*:*:node.js:*:*");
91
- const dep = { groupId: "", artifactId: "lodash", ecosystem: "npm" };
92
- assert.equal(cpeMatchesDep(cpe, dep), true);
93
- });
94
-
95
- test("cpeMatchesDep — npm scoped package via vendor split", () => {
96
- const cpe = parseCpe23("cpe:2.3:a:scope:pkg:*:*:*:*:*:*:*:*");
97
- const dep = { groupId: "", artifactId: "@scope/pkg", ecosystem: "npm" };
98
- assert.equal(cpeMatchesDep(cpe, dep), true);
99
- });
100
-
101
- test("cpeMatchesDep — wrong artifact does not match", () => {
102
- const cpe = parseCpe23("cpe:2.3:a:apache:log4j:*:*:*:*:*:*:*:*");
103
- const dep = { groupId: "org.apache.commons", artifactId: "commons-io", ecosystem: "maven" };
104
- assert.equal(cpeMatchesDep(cpe, dep), false);
105
- });
106
-
107
- test("evaluateCveForDep — Log4Shell NVD record matches vulnerable log4j-core", () => {
108
- const cve = JSON.parse(fs.readFileSync(path.join(FIX, "nvd-log4shell.json"), "utf8"));
109
- const dep = { groupId: "org.apache.logging.log4j", artifactId: "log4j-core", version: "2.14.0", ecosystem: "maven" };
110
- const { affected, confidence } = evaluateCveForDep(cve, dep);
111
- assert.equal(affected, true);
112
- assert.equal(confidence, "exact");
113
- });
114
-
115
- test("evaluateCveForDep — patched log4j is not affected (version out of range)", () => {
116
- const cve = JSON.parse(fs.readFileSync(path.join(FIX, "nvd-log4shell.json"), "utf8"));
117
- const dep = { groupId: "org.apache.logging.log4j", artifactId: "log4j-core", version: "2.17.1", ecosystem: "maven" };
118
- const { affected } = evaluateCveForDep(cve, dep);
119
- assert.equal(affected, false);
120
- });
121
-
122
- test("evaluateCveForDep — unrelated maven dep is not affected", () => {
123
- const cve = JSON.parse(fs.readFileSync(path.join(FIX, "nvd-log4shell.json"), "utf8"));
124
- const dep = { groupId: "com.google.guava", artifactId: "guava", version: "31.0", ecosystem: "maven" };
125
- const { affected } = evaluateCveForDep(cve, dep);
126
- assert.equal(affected, false);
127
- });
128
-
129
- test("evaluateCveForDep — npm lodash matches the lodash CVE", () => {
130
- const cve = JSON.parse(fs.readFileSync(path.join(FIX, "nvd-npm-lodash.json"), "utf8"));
131
- const dep = { groupId: "", artifactId: "lodash", version: "4.17.10", ecosystem: "npm" };
132
- const { affected, confidence } = evaluateCveForDep(cve, dep);
133
- assert.equal(affected, true);
134
- assert.equal(confidence, "exact");
135
- });
136
-
137
- test("evaluateCveForDep — patched npm lodash is not affected", () => {
138
- const cve = JSON.parse(fs.readFileSync(path.join(FIX, "nvd-npm-lodash.json"), "utf8"));
139
- const dep = { groupId: "", artifactId: "lodash", version: "4.17.20", ecosystem: "npm" };
140
- const { affected } = evaluateCveForDep(cve, dep);
141
- assert.equal(affected, false);
142
- });
143
-
144
- test("refineMatchesWithCpe upgrades possible→exact when curated map confirms", () => {
145
- const cve = JSON.parse(fs.readFileSync(path.join(FIX, "nvd-log4shell.json"), "utf8"));
146
- const matches = [{
147
- dep: { groupId: "org.apache.logging.log4j", artifactId: "log4j-core", version: "2.14.0", ecosystem: "maven" },
148
- cve: { id: cve.id, configurations: cve.configurations, severity: "HIGH" },
149
- confidence: "possible",
150
- }];
151
- refineMatchesWithCpe(matches);
152
- assert.equal(matches[0].confidence, "exact");
153
- assert.equal(matches[0].cpeConfidence, "exact");
154
- assert.equal(matches[0].cpeFiltered, undefined);
155
- });
156
-
157
- test("refineMatchesWithCpe flags out-of-range version as likely FP", () => {
158
- const cve = JSON.parse(fs.readFileSync(path.join(FIX, "nvd-log4shell.json"), "utf8"));
159
- const matches = [{
160
- dep: { groupId: "org.apache.logging.log4j", artifactId: "log4j-core", version: "2.17.1", ecosystem: "maven" },
161
- cve: { id: cve.id, configurations: cve.configurations, severity: "HIGH" },
162
- confidence: "probable",
163
- }];
164
- refineMatchesWithCpe(matches);
165
- assert.equal(matches[0].cpeFiltered, true);
166
- });
@@ -1,39 +0,0 @@
1
- const { test } = require("node:test");
2
- const assert = require("node:assert/strict");
3
- const fs = require("fs");
4
- const path = require("path");
5
- const { extractMavenRelevantCve, isMavenRelevant } = require("../lib/cve-download");
6
-
7
- const SAMPLES = path.join(__dirname, "fixtures", "cve-samples");
8
-
9
- test("extractMavenRelevantCve returns null for non-Java CVEs", () => {
10
- const json = JSON.parse(fs.readFileSync(path.join(SAMPLES, "cve-non-java.json"), "utf8"));
11
- assert.equal(extractMavenRelevantCve(json), null);
12
- });
13
-
14
- test("extractMavenRelevantCve parses log4j CVE with packageName", () => {
15
- const json = JSON.parse(fs.readFileSync(path.join(SAMPLES, "cve-with-packagename.json"), "utf8"));
16
- const cve = extractMavenRelevantCve(json);
17
- assert.equal(cve.id, "CVE-2021-44228");
18
- assert.equal(cve.severity, "CRITICAL");
19
- assert.equal(cve.score, 10);
20
- assert.equal(cve.fixVersion, "2.15.0");
21
- assert.equal(cve.affected[0].packageName, "org.apache.logging.log4j:log4j-core");
22
- });
23
-
24
- test("extractMavenRelevantCve handles product-only CVEs (no packageName)", () => {
25
- const json = JSON.parse(fs.readFileSync(path.join(SAMPLES, "cve-product-only.json"), "utf8"));
26
- const cve = extractMavenRelevantCve(json);
27
- assert.equal(cve.id, "CVE-2017-5638");
28
- assert.equal(cve.affected[0].product, "struts2-core");
29
- assert.equal(cve.affected[0].packageName, null);
30
- });
31
-
32
- test("isMavenRelevant true on known java vendors", () => {
33
- assert.equal(isMavenRelevant([{ vendor: "apache", product: "log4j-core" }]), true);
34
- assert.equal(isMavenRelevant([{ vendor: "fasterxml", product: "jackson" }]), true);
35
- });
36
-
37
- test("isMavenRelevant false on unrelated ecosystems", () => {
38
- assert.equal(isMavenRelevant([{ vendor: "node-vendor", product: "left-pad", collectionURL: "https://registry.npmjs.org" }]), false);
39
- });
@@ -1,217 +0,0 @@
1
- const { test } = require("node:test");
2
- const assert = require("node:assert/strict");
3
- const path = require("path");
4
- const fs = require("fs");
5
- const core = require("../lib/core");
6
- const { collectResolvedDeps, matchDepsAgainstCves, vendorMatchesGroup } = require("../lib/cve-match");
7
- const { extractMavenRelevantCve, isMavenRelevant } = require("../lib/cve-download");
8
-
9
- const FIXTURES = path.join(__dirname, "fixtures");
10
- const COMPLEX = path.join(FIXTURES, "complex-enterprise");
11
- const CVE_SAMPLES = path.join(FIXTURES, "cve-samples");
12
-
13
- async function pipeline(src) {
14
- const store = core.newMetadataStore();
15
- const props = {};
16
- const pomFiles = core.findPomFiles(src);
17
- for (const f of pomFiles) await core.parsePom(f, store);
18
- for (const f of pomFiles) await core.getAllInheritedProps(f, store, props);
19
- return { store, props };
20
- }
21
-
22
- function buildIndexFromSamples() {
23
- const index = { meta: {}, byPackageName: {}, byProduct: {} };
24
- for (const f of fs.readdirSync(CVE_SAMPLES)) {
25
- const json = JSON.parse(fs.readFileSync(path.join(CVE_SAMPLES, f), "utf8"));
26
- const cve = extractMavenRelevantCve(json);
27
- if (!cve) continue;
28
- for (const a of cve.affected) {
29
- const e = { id: cve.id, severity: cve.severity, score: cve.score, description: cve.description, ranges: a.ranges, vendor: a.vendor, product: a.product, fixVersion: cve.fixVersion };
30
- if (a.packageName) (index.byPackageName[a.packageName.toLowerCase()] ||= []).push(e);
31
- if (a.product) (index.byProduct[a.product] ||= []).push(e);
32
- }
33
- }
34
- return index;
35
- }
36
-
37
- test("isMavenRelevant filters out non-Java CVEs", () => {
38
- const npmCve = JSON.parse(fs.readFileSync(path.join(CVE_SAMPLES, "cve-non-java.json"), "utf8"));
39
- assert.equal(isMavenRelevant(npmCve.containers.cna.affected), false);
40
-
41
- const log4jCve = JSON.parse(fs.readFileSync(path.join(CVE_SAMPLES, "cve-with-packagename.json"), "utf8"));
42
- assert.equal(isMavenRelevant(log4jCve.containers.cna.affected), true);
43
- });
44
-
45
- test("vendorMatchesGroup heuristic", () => {
46
- assert.equal(vendorMatchesGroup("apache", "org.apache.commons"), true);
47
- assert.equal(vendorMatchesGroup("apache", "org.eclipse.jetty"), false);
48
- assert.equal(vendorMatchesGroup("springframework", "org.springframework"), true);
49
- });
50
-
51
- test("vendorMatchesGroup rejects substring leaks (H2)", () => {
52
- // All these cases used to leak through unbounded `g.includes(v)` / `v.includes(g)`
53
- // branches. The fix keeps only equality + dot-segment membership.
54
- assert.equal(vendorMatchesGroup("a", "com.acme.client"), false);
55
- assert.equal(vendorMatchesGroup("ibm", "com.ibmcloudant.driver"), false);
56
- assert.equal(vendorMatchesGroup("go", "org.golang.x"), false);
57
- // "open" used to wrongly match every org.opensaml.* groupId via substring
58
- assert.equal(vendorMatchesGroup("open", "org.opensaml.core"), false);
59
- // Dot-segment match still works for legitimate cases
60
- assert.equal(vendorMatchesGroup("apache", "org.apache.commons"), true);
61
- assert.equal(vendorMatchesGroup("springframework", "org.springframework.boot"), true);
62
- });
63
-
64
- test("vendorMatchesGroup tokenizes multi-word legal-entity vendors", () => {
65
- // NVD records the logback vendor as "qos.ch sarl" — a legal entity name.
66
- // Neither equality nor plain dot-segment catches it, but tokenization on
67
- // non-alphanumerics finds "qos"/"ch" as group segments and confirms the
68
- // match. Without this, real logback CVEs landed in tier "possible".
69
- assert.equal(vendorMatchesGroup("qos.ch sarl", "ch.qos.logback"), true);
70
- assert.equal(vendorMatchesGroup("the apache software foundation", "org.apache.commons"), true);
71
- // A single-token vendor must NOT be tokenized into substrings — that
72
- // would re-introduce the H2 leak (e.g. "open" → ["open"] alone never
73
- // matches arbitrary "org.opensaml.*").
74
- assert.equal(vendorMatchesGroup("open", "org.opensaml.core"), false);
75
- });
76
-
77
- test("matchDepsAgainstCves hides 'possible' tier by default (H3)", () => {
78
- // product matches `log4j-core` but vendor `acme` doesn't match groupId.
79
- // Without opts.includePossibleTier we expect zero matches.
80
- const idx = {
81
- byPackageName: {},
82
- byProduct: {
83
- "log4j-core": [
84
- { id: "CVE-FAKE-0001", severity: "HIGH", vendor: "acme", product: "log4j-core",
85
- ranges: [{ version: "2.0", lessThan: "2.20.0", status: "affected" }] },
86
- ],
87
- },
88
- };
89
- const deps = new Map([
90
- ["org.apache.logging.log4j:log4j-core", {
91
- groupId: "org.apache.logging.log4j", artifactId: "log4j-core", version: "2.14.0", scope: "compile", pomPaths: [],
92
- }],
93
- ]);
94
- assert.equal(matchDepsAgainstCves(deps, idx).length, 0);
95
- assert.equal(matchDepsAgainstCves(deps, idx, { includePossibleTier: true }).length, 1);
96
- });
97
-
98
- test("matchDepsAgainstCves does not flag deps when range has no bounds (H1+H3)", () => {
99
- // Sparse range entry (`{status:"affected"}` with no bounds) used to flag
100
- // every version that hit the product bucket. Combined with H3 hiding the
101
- // possible tier, this should now produce zero matches even when the
102
- // vendor matches.
103
- const idx = {
104
- byPackageName: {},
105
- byProduct: {
106
- "log4j-core": [
107
- { id: "CVE-FAKE-0002", severity: "HIGH", vendor: "apache", product: "log4j-core",
108
- ranges: [{ status: "affected" }] },
109
- ],
110
- },
111
- };
112
- const deps = new Map([
113
- ["org.apache.logging.log4j:log4j-core", {
114
- groupId: "org.apache.logging.log4j", artifactId: "log4j-core", version: "2.99.0", scope: "compile", pomPaths: [],
115
- }],
116
- ]);
117
- assert.equal(matchDepsAgainstCves(deps, idx).length, 0);
118
- });
119
-
120
- test("collectResolvedDeps dedupes by g:a and includes external parent POMs", async () => {
121
- const { store, props } = await pipeline(COMPLEX);
122
- const deps = collectResolvedDeps(store, props, {});
123
- // External Spring Boot parent should be present as scope='parent'
124
- const sbParent = deps.get("org.springframework.boot:spring-boot-starter-parent");
125
- assert.ok(sbParent, "external parent should be collected");
126
- assert.equal(sbParent.scope, "parent");
127
- });
128
-
129
- test("collectResolvedDeps keeps highest version on conflict", async () => {
130
- const { store, props } = await pipeline(COMPLEX);
131
- const deps = collectResolvedDeps(store, props, {});
132
- const lang = deps.get("org.apache.commons:commons-lang3");
133
- if (lang) assert.equal(lang.version, "3.12.0");
134
- });
135
-
136
- test("collectResolvedDeps records every distinct version (e.g. two profiles)", () => {
137
- const depXml = (g, a, v) => ({ groupId: [g], artifactId: [a], version: [v], scope: ["compile"] });
138
- const store = { byPath: { a: {}, b: {} }, byId: {} };
139
- // Same g:a pinned to different versions in two modules/profiles.
140
- const props = {
141
- a: { properties: {}, dependencies: [depXml("com.x", "y", "1.0.0")], dependencyManagement: [] },
142
- b: { properties: {}, dependencies: [depXml("com.x", "y", "2.0.0")], dependencyManagement: [] },
143
- };
144
- const deps = collectResolvedDeps(store, props, {});
145
- const y = deps.get("com.x:y");
146
- assert.ok(y, "the dep is collected once, keyed by g:a");
147
- assert.equal(y.version, "2.0.0", "representative version is still the highest");
148
- assert.deepEqual([...y.versions].sort(), ["1.0.0", "2.0.0"], "both distinct versions are tracked");
149
- });
150
-
151
- test("matchDepsAgainstCves scans every distinct version, not just the highest", () => {
152
- // A CVE that only affects the OLD version. Profiles pin 2.14.0 (vulnerable)
153
- // and 2.20.0 (patched); the highest is patched, so deduping on max would
154
- // hide the real exposure. Each distinct version must be scanned.
155
- const idx = {
156
- byPackageName: {
157
- "org.apache.logging.log4j:log4j-core": [
158
- { id: "CVE-OLD-0001", severity: "CRITICAL", vendor: "apache", product: "log4j-core",
159
- ranges: [{ version: "2.0", lessThan: "2.15.0", status: "affected" }] },
160
- ],
161
- },
162
- byProduct: {},
163
- };
164
- const deps = new Map([
165
- ["org.apache.logging.log4j:log4j-core", {
166
- groupId: "org.apache.logging.log4j", artifactId: "log4j-core",
167
- version: "2.20.0", versions: ["2.14.0", "2.20.0"], scope: "compile", pomPaths: [],
168
- }],
169
- ]);
170
- const matches = matchDepsAgainstCves(deps, idx);
171
- assert.equal(matches.length, 1, "vulnerable 2.14.0 must be flagged despite 2.20.0 being highest");
172
- assert.equal(matches[0].cve.id, "CVE-OLD-0001");
173
- assert.equal(matches[0].dep.version, "2.14.0", "match carries the actually-vulnerable version");
174
- });
175
-
176
- test("collectResolvedDeps --ignore-test drops test-scope deps", async () => {
177
- const { store, props } = await pipeline(COMPLEX);
178
- const withTest = collectResolvedDeps(store, props, {});
179
- const withoutTest = collectResolvedDeps(store, props, { ignoreTest: true });
180
- assert.ok(withTest.has("junit:junit"));
181
- assert.ok(!withoutTest.has("junit:junit"));
182
- });
183
-
184
- test("matchDepsAgainstCves: log4j 2.14 is matched as CRITICAL via packageName", async () => {
185
- const { store, props } = await pipeline(COMPLEX);
186
- const deps = collectResolvedDeps(store, props, {});
187
- const idx = buildIndexFromSamples();
188
- const matches = matchDepsAgainstCves(deps, idx);
189
- const log4j = matches.find(m => m.cve.id === "CVE-2021-44228");
190
- assert.ok(log4j, "log4j CVE not matched");
191
- assert.equal(log4j.dep.artifactId, "log4j-core");
192
- assert.equal(log4j.cve.severity, "CRITICAL");
193
- assert.equal(log4j.confidence, "exact");
194
- });
195
-
196
- test("matchDepsAgainstCves dedupes by (dep, cveId)", () => {
197
- // Synthesize an index where the same dep+cveId appears twice
198
- const idx = {
199
- byPackageName: {
200
- "org.apache.logging.log4j:log4j-core": [
201
- { id: "CVE-2021-44228", severity: "CRITICAL", ranges: [{ version: "2.0", lessThan: "2.15.0" }], vendor: "apache", product: "log4j-core" },
202
- ],
203
- },
204
- byProduct: {
205
- "log4j-core": [
206
- { id: "CVE-2021-44228", severity: "CRITICAL", ranges: [{ version: "2.0", lessThan: "2.15.0" }], vendor: "apache", product: "log4j-core" },
207
- ],
208
- },
209
- };
210
- const deps = new Map([
211
- ["org.apache.logging.log4j:log4j-core", {
212
- groupId: "org.apache.logging.log4j", artifactId: "log4j-core", version: "2.14.0", scope: "compile", pomPaths: [],
213
- }],
214
- ]);
215
- const matches = matchDepsAgainstCves(deps, idx);
216
- assert.equal(matches.length, 1, "duplicate (dep, cveId) match should be dedup'd");
217
- });
@@ -1,171 +0,0 @@
1
- const { test } = require("node:test");
2
- const assert = require("node:assert/strict");
3
- const fs = require("fs");
4
- const path = require("path");
5
- const os = require("os");
6
- const { generateHtmlReport, generateWordReport, computeStats, writeReports, esc } = require("../lib/cve-report");
7
-
8
- const sampleMatches = [
9
- {
10
- dep: { groupId: "org.apache.logging.log4j", artifactId: "log4j-core", version: "2.14.0", scope: "compile" },
11
- cve: { id: "CVE-2021-44228", severity: "CRITICAL", score: 10, description: "Log4Shell", fixVersion: "2.15.0" },
12
- confidence: "exact",
13
- },
14
- {
15
- dep: { groupId: "com.fasterxml.jackson.core", artifactId: "jackson-databind", version: "2.13.0", scope: "compile" },
16
- cve: { id: "CVE-2022-42003", severity: "HIGH", score: 7.5, description: "DoS via deep nesting", fixVersion: "2.13.5" },
17
- confidence: "exact",
18
- },
19
- ];
20
-
21
- const projectInfo = { name: "demo", src: "/tmp/demo", generatedAt: "2026-05-21", cveDataDate: null };
22
-
23
- test("computeStats counts by severity", () => {
24
- const s = computeStats(sampleMatches);
25
- assert.equal(s.total, 2);
26
- assert.equal(s.critical, 1);
27
- assert.equal(s.high, 1);
28
- assert.equal(s.medium, 0);
29
- });
30
-
31
- test("computeStats handles undefined/null severity", () => {
32
- const s = computeStats([{ cve: { severity: null } }, { cve: {} }]);
33
- assert.equal(s.unknown, 2);
34
- });
35
-
36
- test("esc escapes HTML entities", () => {
37
- assert.equal(esc(`<script>alert("x")</script>&`), "&lt;script&gt;alert(&quot;x&quot;)&lt;/script&gt;&amp;");
38
- });
39
-
40
- test("generateHtmlReport produces well-formed self-contained HTML", () => {
41
- const html = generateHtmlReport({
42
- cveMatches: sampleMatches,
43
- eolResults: [], obsoleteResults: [], outdatedResults: [],
44
- projectInfo,
45
- });
46
- assert.ok(html.startsWith("<!doctype html>"));
47
- assert.ok(html.includes("CVE-2021-44228"));
48
- assert.ok(html.includes("Log4Shell"));
49
- assert.ok(html.includes("<style>"), "should have inline CSS");
50
- assert.ok(!html.includes("</body>\n<body>"), "no double body");
51
- });
52
-
53
- test("generateWordReport contains Word XML namespaces", () => {
54
- const doc = generateWordReport({
55
- cveMatches: sampleMatches,
56
- eolResults: [], obsoleteResults: [], outdatedResults: [],
57
- projectInfo,
58
- });
59
- assert.ok(doc.includes("xmlns:o=\"urn:schemas-microsoft-com:office:office\""));
60
- assert.ok(doc.includes("Word.Document"));
61
- });
62
-
63
- test("generateHtmlReport wraps tables with copy buttons", () => {
64
- const html = generateHtmlReport({
65
- cveMatches: sampleMatches,
66
- eolResults: [], obsoleteResults: [], outdatedResults: [],
67
- projectInfo,
68
- });
69
- // Wrapper div + copy button injected around every table
70
- assert.ok(html.includes('class="table-wrap"'), "should wrap tables");
71
- assert.ok(html.includes('class="btn-copy"'), "should have copy buttons");
72
- assert.ok(html.includes("📋 Copy table"), "button label");
73
- // Copy script wired up
74
- assert.ok(html.includes("navigator.clipboard"), "should include clipboard script");
75
- assert.ok(html.includes("inlineStylesForWord"), "should inline styles for Word paste");
76
- });
77
-
78
- test("generateWordReport does NOT wrap tables (no copy buttons in .doc)", () => {
79
- const doc = generateWordReport({
80
- cveMatches: sampleMatches,
81
- eolResults: [], obsoleteResults: [], outdatedResults: [],
82
- projectInfo,
83
- });
84
- assert.ok(!doc.includes('class="table-wrap"'), "no wrapper in Word output");
85
- assert.ok(!doc.includes('class="btn-copy"'), "no copy buttons in Word output");
86
- });
87
-
88
- test("CWE human names are shown in the CVE table and detail panel", () => {
89
- const html = generateHtmlReport({
90
- cveMatches: sampleMatches.map(m => ({
91
- ...m,
92
- cve: { ...m.cve, cwes: ["CWE-502", "CWE-917"] },
93
- })),
94
- eolResults: [], obsoleteResults: [], outdatedResults: [],
95
- projectInfo,
96
- });
97
- // Detail panel: full name inline
98
- assert.ok(html.includes("Deserialization of Untrusted Data"), "CWE-502 name in panel");
99
- assert.ok(html.includes("Expression Language Injection"), "CWE-917 name in panel");
100
- // Tooltip on the column link
101
- assert.ok(html.includes('title="CWE-502: Deserialization of Untrusted Data"'), "tooltip");
102
- // Weaknesses section in detail panel
103
- assert.ok(html.includes("Weaknesses (CWE)"), "weaknesses section heading");
104
- assert.ok(html.includes('class="cwe-list"'), "CWE list rendered");
105
- });
106
-
107
- test("Executive summary surfaces primary CWE name on each preview row", () => {
108
- const html = generateHtmlReport({
109
- cveMatches: [{
110
- dep: { groupId: "org.apache.logging.log4j", artifactId: "log4j-core", version: "2.14.0", scope: "compile" },
111
- cve: { id: "CVE-2021-44228", severity: "CRITICAL", score: 10, description: "Log4Shell", fixVersion: "2.15.0", cwes: ["CWE-502", "CWE-917"] },
112
- confidence: "exact",
113
- }],
114
- eolResults: [], obsoleteResults: [], outdatedResults: [],
115
- projectInfo,
116
- });
117
- assert.ok(html.includes('class="exec-cwe"'), "exec-cwe chip in summary");
118
- assert.ok(html.includes('class="exec-cwe-id">CWE-502'), "id in chip");
119
- assert.ok(html.includes("Deserialization of Untrusted Data"), "human name in chip");
120
- });
121
-
122
- test("Unknown CWE falls back to id-only display", () => {
123
- const html = generateHtmlReport({
124
- cveMatches: [{
125
- dep: { groupId: "x", artifactId: "y", version: "1", scope: "compile" },
126
- cve: { id: "CVE-2099-9999", severity: "HIGH", description: "n/a", cwes: ["CWE-99999"] },
127
- }],
128
- eolResults: [], obsoleteResults: [], outdatedResults: [],
129
- projectInfo,
130
- });
131
- assert.ok(html.includes("CWE-99999"), "unknown CWE id present");
132
- assert.ok(html.includes("(unknown weakness)"), "fallback label for unknown CWE in detail panel");
133
- });
134
-
135
- test("generateWordReport applies width:100% to every table for Word page-fit", () => {
136
- const doc = generateWordReport({
137
- cveMatches: sampleMatches,
138
- eolResults: [], obsoleteResults: [], outdatedResults: [],
139
- projectInfo,
140
- });
141
- // Every <table> rewritten with width="100%" + inline style (incl. fixed table-layout)
142
- assert.ok(doc.includes('width="100%"'), "tables get width=100% attribute");
143
- assert.ok(doc.includes("table-layout:fixed"), "tables get fixed layout");
144
- // Word section + landscape page setup
145
- assert.ok(doc.includes('class="WordSection1"'), "WordSection1 wrapper");
146
- assert.ok(doc.includes("mso-page-orientation: landscape"), "landscape declared via mso-* property");
147
- assert.ok(doc.includes("29.7cm 21cm"), "explicit A4 landscape dimensions");
148
- // MSO conditional comment block with twips-based <w:sectPr> equivalent
149
- assert.ok(doc.includes("w:WordDocument"), "Word document XML block");
150
- assert.ok(doc.includes("16838twips"), "twips-based page size in mso conditional");
151
- // Colgroups injected so fixed-layout tables have deterministic column widths
152
- assert.ok(doc.includes('<colgroup>'), "colgroup injected on tables");
153
- });
154
-
155
- test("writeReports writes both files to disk", async () => {
156
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "fad-checker-rep-"));
157
- const r = await writeReports({
158
- cveMatches: sampleMatches,
159
- eolResults: [{ dep: { groupId: "org.hibernate", artifactId: "hibernate-core", version: "5.6" }, product: "Hibernate ORM", eol: "2025-08-08" }],
160
- obsoleteResults: [{ dep: { groupId: "log4j", artifactId: "log4j", version: "1.2" }, severity: "CRITICAL", replacement: "log4j-core 2.x" }],
161
- outdatedResults: [],
162
- projectInfo,
163
- outputDir: tmp,
164
- });
165
- assert.ok(fs.existsSync(r.htmlPath));
166
- assert.ok(fs.existsSync(r.docPath));
167
- const html = fs.readFileSync(r.htmlPath, "utf8");
168
- assert.ok(html.includes("Hibernate ORM"));
169
- assert.ok(html.includes("log4j:log4j"));
170
- fs.rmSync(tmp, { recursive: true, force: true });
171
- });
@@ -1,32 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project xmlns="http://maven.apache.org/POM/4.0.0">
3
- <modelVersion>4.0.0</modelVersion>
4
- <parent>
5
- <groupId>com.acme.enterprise</groupId>
6
- <artifactId>enterprise-root</artifactId>
7
- <version>4.2.1</version>
8
- <relativePath>..</relativePath>
9
- </parent>
10
- <artifactId>api</artifactId>
11
- <dependencyManagement>
12
- <dependencies>
13
- <dependency>
14
- <groupId>com.acme.enterprise</groupId>
15
- <artifactId>build-bom</artifactId>
16
- <version>4.2.1</version>
17
- <type>pom</type>
18
- <scope>import</scope>
19
- </dependency>
20
- </dependencies>
21
- </dependencyManagement>
22
- <dependencies>
23
- <dependency>
24
- <groupId>com.fasterxml.jackson.core</groupId>
25
- <artifactId>jackson-databind</artifactId>
26
- </dependency>
27
- <dependency>
28
- <groupId>com.acme.private</groupId>
29
- <artifactId>acme-commons</artifactId>
30
- </dependency>
31
- </dependencies>
32
- </project>
@@ -1,46 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project xmlns="http://maven.apache.org/POM/4.0.0">
3
- <modelVersion>4.0.0</modelVersion>
4
- <parent>
5
- <groupId>com.acme.enterprise</groupId>
6
- <artifactId>enterprise-root</artifactId>
7
- <version>4.2.1</version>
8
- <relativePath>..</relativePath>
9
- </parent>
10
- <artifactId>build-bom</artifactId>
11
- <packaging>pom</packaging>
12
- <dependencyManagement>
13
- <dependencies>
14
- <dependency>
15
- <groupId>org.hibernate</groupId>
16
- <artifactId>hibernate-core</artifactId>
17
- <version>${hibernate.version}</version>
18
- </dependency>
19
- <dependency>
20
- <groupId>com.fasterxml.jackson.core</groupId>
21
- <artifactId>jackson-databind</artifactId>
22
- <version>${jackson.version}</version>
23
- </dependency>
24
- <dependency>
25
- <groupId>org.apache.logging.log4j</groupId>
26
- <artifactId>log4j-core</artifactId>
27
- <version>2.14.0</version>
28
- </dependency>
29
- <dependency>
30
- <groupId>com.acme.private</groupId>
31
- <artifactId>acme-commons</artifactId>
32
- <version>${acme-commons.version}</version>
33
- </dependency>
34
- <dependency>
35
- <groupId>commons-io</groupId>
36
- <artifactId>commons-io</artifactId>
37
- <version>2.7</version>
38
- </dependency>
39
- <dependency>
40
- <groupId>org.codehaus.jackson</groupId>
41
- <artifactId>jackson-mapper-asl</artifactId>
42
- <version>1.9.13</version>
43
- </dependency>
44
- </dependencies>
45
- </dependencyManagement>
46
- </project>