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.
- package/CHANGELOG.md +15 -0
- package/README.md +58 -10
- package/data/eol-mapping.json +5 -5
- package/fad-checker.js +289 -203
- package/lib/codecs/npm/collect.js +26 -17
- package/lib/codecs/npm/parse.js +114 -10
- package/lib/codecs/npm/registry.js +4 -3
- package/lib/codecs/npm.codec.js +1 -1
- package/lib/codecs/nuget.codec.js +4 -2
- package/lib/codecs/pypi/parse.js +104 -12
- package/lib/codecs/pypi.codec.js +27 -6
- package/lib/core.js +35 -1
- package/lib/cve-match.js +15 -5
- package/lib/deps-descriptor.js +110 -0
- package/lib/nvd.js +4 -3
- package/lib/osv.js +7 -4
- package/lib/outdated.js +7 -2
- package/lib/retire.js +77 -13
- package/lib/transitive.js +3 -3
- package/lib/ui.js +87 -0
- package/package.json +3 -2
- package/CLAUDE.md +0 -147
- package/docs/ARCHITECTURE.md +0 -182
- package/docs/USAGE.md +0 -187
- package/docs/superpowers/plans/2026-05-29-codec-composer.md +0 -556
- package/docs/superpowers/plans/2026-05-29-codec-foundation.md +0 -851
- package/docs/superpowers/plans/2026-05-29-codec-nuget.md +0 -432
- package/docs/superpowers/plans/2026-05-29-codec-pypi.md +0 -450
- package/docs/superpowers/specs/2026-05-29-codecs-multi-ecosystem-design.md +0 -251
- package/test/cli-ecosystem.test.js +0 -30
- package/test/codec-edge-cases.test.js +0 -131
- package/test/codec-integration.test.js +0 -119
- package/test/codecs.test.js +0 -92
- package/test/composer.test.js +0 -71
- package/test/core.test.js +0 -153
- package/test/cpe.test.js +0 -166
- package/test/cve-download.test.js +0 -39
- package/test/cve-match.test.js +0 -217
- package/test/cve-report.test.js +0 -180
- package/test/dep-record.test.js +0 -31
- package/test/fixtures/complex-enterprise/api/pom.xml +0 -32
- package/test/fixtures/complex-enterprise/build/pom.xml +0 -46
- package/test/fixtures/complex-enterprise/dao/pom.xml +0 -37
- package/test/fixtures/complex-enterprise/pom.xml +0 -66
- package/test/fixtures/complex-enterprise/web/pom.xml +0 -77
- package/test/fixtures/csharp-config/packages.config +0 -4
- package/test/fixtures/csharp-csproj/Directory.Packages.props +0 -5
- package/test/fixtures/csharp-csproj/app.csproj +0 -7
- package/test/fixtures/csharp-lock/packages.lock.json +0 -6
- package/test/fixtures/cve-samples/cve-non-java.json +0 -19
- package/test/fixtures/cve-samples/cve-product-only.json +0 -31
- package/test/fixtures/cve-samples/cve-with-packagename.json +0 -37
- package/test/fixtures/cve-samples/nvd-log4shell.json +0 -40
- package/test/fixtures/cve-samples/nvd-npm-lodash.json +0 -22
- package/test/fixtures/monorepo-mixed/libs/common-bom/pom.xml +0 -26
- package/test/fixtures/monorepo-mixed/packages/cli/package.json +0 -14
- package/test/fixtures/monorepo-mixed/packages/cli/yarn.lock +0 -41
- package/test/fixtures/monorepo-mixed/packages/no-lock/package.json +0 -10
- package/test/fixtures/monorepo-mixed/packages/web-app/package-lock.json +0 -71
- package/test/fixtures/monorepo-mixed/packages/web-app/package.json +0 -17
- package/test/fixtures/monorepo-mixed/pom.xml +0 -29
- package/test/fixtures/monorepo-mixed/services/api/pom.xml +0 -27
- package/test/fixtures/monorepo-mixed/services/worker/pom.xml +0 -28
- package/test/fixtures/php-app/composer.json +0 -5
- package/test/fixtures/php-app/composer.lock +0 -10
- package/test/fixtures/polyglot/cs/packages.lock.json +0 -1
- package/test/fixtures/polyglot/js/package-lock.json +0 -1
- package/test/fixtures/polyglot/js/package.json +0 -1
- package/test/fixtures/polyglot/mvn/pom.xml +0 -7
- package/test/fixtures/polyglot/php/composer.lock +0 -1
- package/test/fixtures/polyglot/py/poetry.lock +0 -3
- package/test/fixtures/private-lib-detection/core/pom.xml +0 -26
- package/test/fixtures/private-lib-detection/plugin/pom.xml +0 -23
- package/test/fixtures/private-lib-detection/pom.xml +0 -35
- package/test/fixtures/python-pipenv/Pipfile.lock +0 -1
- package/test/fixtures/python-poetry/poetry.lock +0 -7
- package/test/fixtures/python-reqs/requirements.txt +0 -5
- package/test/fixtures/python-uv/uv.lock +0 -3
- package/test/fixtures/simple/app/pom.xml +0 -28
- package/test/fixtures/simple/lib/pom.xml +0 -18
- package/test/fixtures/simple/pom.xml +0 -24
- package/test/maven-repo.test.js +0 -111
- package/test/maven-version.test.js +0 -57
- package/test/monorepo.test.js +0 -132
- package/test/npm-registry.test.js +0 -64
- package/test/npm.test.js +0 -150
- package/test/nuget.test.js +0 -66
- package/test/osv.test.js +0 -62
- package/test/outdated.test.js +0 -101
- package/test/pypi.test.js +0 -72
- package/test/snyk.test.js +0 -64
- package/test/transitive.test.js +0 -305
- package/test/webjar.test.js +0 -33
package/test/composer.test.js
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
const test = require("node:test");
|
|
2
|
-
const assert = require("node:assert");
|
|
3
|
-
const path = require("path");
|
|
4
|
-
const { parseComposerLock, parseComposerJson } = require("../lib/codecs/composer/parse");
|
|
5
|
-
|
|
6
|
-
const FIX = path.join(__dirname, "fixtures", "php-app");
|
|
7
|
-
|
|
8
|
-
test("parseComposerLock reads prod + dev packages, strips leading v", () => {
|
|
9
|
-
const r = parseComposerLock(path.join(FIX, "composer.lock"));
|
|
10
|
-
const byName = Object.fromEntries(r.deps.map(d => [d.name, d]));
|
|
11
|
-
assert.strictEqual(byName["guzzlehttp/guzzle"].version, "7.4.5");
|
|
12
|
-
assert.strictEqual(byName["guzzlehttp/guzzle"].scope, "prod");
|
|
13
|
-
assert.strictEqual(byName["symfony/console"].version, "6.2.10"); // "v6.2.10" → "6.2.10"
|
|
14
|
-
assert.strictEqual(byName["phpunit/phpunit"].scope, "dev");
|
|
15
|
-
assert.strictEqual(byName["phpunit/phpunit"].isDev, true);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test("parseComposerJson reads require + require-dev with pinned-vs-range info", () => {
|
|
19
|
-
const r = parseComposerJson(path.join(FIX, "composer.json"));
|
|
20
|
-
const byName = Object.fromEntries(r.deps.map(d => [d.name, d]));
|
|
21
|
-
assert.strictEqual(byName["monolog/monolog"].version, "2.9.1");
|
|
22
|
-
assert.strictEqual(byName["guzzlehttp/guzzle"].version, "^7.0");
|
|
23
|
-
assert.strictEqual(byName["phpunit/phpunit"].scope, "dev");
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const { packagistToFindings } = require("../lib/codecs/composer/registry");
|
|
27
|
-
|
|
28
|
-
test("packagistToFindings extracts latest stable + abandoned flag", () => {
|
|
29
|
-
const pkg = {
|
|
30
|
-
"abandoned": "psr/log",
|
|
31
|
-
"versions": {
|
|
32
|
-
"2.9.1": { "version": "2.9.1" },
|
|
33
|
-
"3.0.0": { "version": "3.0.0" },
|
|
34
|
-
"dev-main": { "version": "dev-main" },
|
|
35
|
-
"2.8.0": { "version": "2.8.0" },
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
const f = packagistToFindings(pkg, { version: "2.9.1" });
|
|
39
|
-
assert.strictEqual(f.outdated.latest, "3.0.0");
|
|
40
|
-
assert.deepStrictEqual(f.abandoned, { replacement: "psr/log" });
|
|
41
|
-
|
|
42
|
-
const f2 = packagistToFindings({ "abandoned": true, "versions": { "1.0.0": {} } }, { version: "1.0.0" });
|
|
43
|
-
assert.deepStrictEqual(f2.abandoned, { replacement: null });
|
|
44
|
-
assert.strictEqual(f2.outdated, null);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const composer = require("../lib/codecs/composer.codec");
|
|
48
|
-
const { assertCodecShape } = require("../lib/codecs/codec.interface");
|
|
49
|
-
|
|
50
|
-
test("composer codec: shape, detect, collect with composer:vendor/pkg coordKeys", async () => {
|
|
51
|
-
assertCodecShape(composer);
|
|
52
|
-
assert.strictEqual(composer.detect(FIX), true);
|
|
53
|
-
const { deps } = await composer.collect(FIX, {});
|
|
54
|
-
const g = deps.get("composer:guzzlehttp/guzzle");
|
|
55
|
-
assert.ok(g, "guzzle should be collected under composer:guzzlehttp/guzzle");
|
|
56
|
-
assert.strictEqual(g.ecosystem, "composer");
|
|
57
|
-
assert.strictEqual(g.namespace, "guzzlehttp");
|
|
58
|
-
assert.strictEqual(g.name, "guzzle");
|
|
59
|
-
assert.strictEqual(composer.osvPackageName(g), "guzzlehttp/guzzle");
|
|
60
|
-
assert.strictEqual(composer.formatCoord(g), "guzzlehttp/guzzle");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("composer collect falls back to composer.json (pinned only) with warning when no lock", async () => {
|
|
64
|
-
const os2 = require("os"); const fs2 = require("fs"); const p2 = require("path");
|
|
65
|
-
const dir = fs2.mkdtempSync(p2.join(os2.tmpdir(), "composer-nolock-"));
|
|
66
|
-
fs2.writeFileSync(p2.join(dir, "composer.json"), JSON.stringify({ name: "x/y", require: { "a/pinned": "1.2.3", "b/range": "^2.0" } }));
|
|
67
|
-
const { deps, warnings } = await composer.collect(dir, {});
|
|
68
|
-
assert.ok(deps.has("composer:a/pinned"));
|
|
69
|
-
assert.ok(!deps.has("composer:b/range"));
|
|
70
|
-
assert.ok(warnings.find(w => w.type === "no-lockfile"));
|
|
71
|
-
});
|
package/test/core.test.js
DELETED
|
@@ -1,153 +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 os = require("os");
|
|
6
|
-
const { parseStringPromise } = require("xml2js");
|
|
7
|
-
|
|
8
|
-
const core = require("../lib/core");
|
|
9
|
-
|
|
10
|
-
const FIXTURES = path.join(__dirname, "fixtures");
|
|
11
|
-
const SIMPLE = path.join(FIXTURES, "simple");
|
|
12
|
-
const COMPLEX = path.join(FIXTURES, "complex-enterprise");
|
|
13
|
-
const PRIVATE_FIX = path.join(FIXTURES, "private-lib-detection");
|
|
14
|
-
|
|
15
|
-
async function pipeline(src, { deps2Exclude } = {}) {
|
|
16
|
-
const store = core.newMetadataStore();
|
|
17
|
-
const props = {};
|
|
18
|
-
const pomFiles = core.findPomFiles(src);
|
|
19
|
-
for (const f of pomFiles) await core.parsePom(f, store);
|
|
20
|
-
for (const f of pomFiles) await core.getAllInheritedProps(f, store, props);
|
|
21
|
-
return { store, props, pomFiles };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
test("findPomFiles skips target/.git/node_modules", () => {
|
|
25
|
-
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "fad-checker-test-"));
|
|
26
|
-
fs.mkdirSync(path.join(tmp, "target"));
|
|
27
|
-
fs.writeFileSync(path.join(tmp, "target", "pom.xml"), "<project/>");
|
|
28
|
-
fs.writeFileSync(path.join(tmp, "pom.xml"), "<project/>");
|
|
29
|
-
const found = core.findPomFiles(tmp);
|
|
30
|
-
assert.equal(found.length, 1, "target/ should be skipped");
|
|
31
|
-
assert.equal(found[0], path.join(tmp, "pom.xml"));
|
|
32
|
-
fs.rmSync(tmp, { recursive: true, force: true });
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("parsePom extracts groupId/artifactId/version + parent + profiles", async () => {
|
|
36
|
-
const { store } = await pipeline(SIMPLE);
|
|
37
|
-
const root = store.byPath[path.join(SIMPLE, "pom.xml")];
|
|
38
|
-
assert.equal(root.groupId, "com.example.simple");
|
|
39
|
-
assert.equal(root.artifactId, "simple-parent");
|
|
40
|
-
assert.equal(root.version, "1.0.0");
|
|
41
|
-
assert.equal(root.parentInfo, null);
|
|
42
|
-
|
|
43
|
-
const app = store.byPath[path.join(SIMPLE, "app", "pom.xml")];
|
|
44
|
-
assert.equal(app.parentInfo.groupId, "com.example.simple");
|
|
45
|
-
assert.equal(app.parentInfo.artifactId, "simple-parent");
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("byId does not get polluted with undefined keys", async () => {
|
|
49
|
-
const { store } = await pipeline(SIMPLE);
|
|
50
|
-
for (const key of Object.keys(store.byId)) {
|
|
51
|
-
assert.ok(!key.includes("undefined"), `byId key has 'undefined': ${key}`);
|
|
52
|
-
assert.ok(!key.includes("null"), `byId key has 'null': ${key}`);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("relativePath as directory resolves to dir/pom.xml", async () => {
|
|
57
|
-
const { store } = await pipeline(SIMPLE);
|
|
58
|
-
const app = store.byPath[path.join(SIMPLE, "app", "pom.xml")];
|
|
59
|
-
// resolveParentPath populates parentDescr
|
|
60
|
-
core.resolveParentPath(app.pomPath, app.parentInfo, store);
|
|
61
|
-
assert.ok(app.parentDescr, "parent descriptor should be resolved");
|
|
62
|
-
assert.equal(app.parentDescr.artifactId, "simple-parent");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("activeByDefault profile is detected and used for property overrides", async () => {
|
|
66
|
-
const { store, props } = await pipeline(COMPLEX);
|
|
67
|
-
const root = store.byPath[path.join(COMPLEX, "pom.xml")];
|
|
68
|
-
assert.equal(root.defaultProfileId, "dev");
|
|
69
|
-
const merged = props[path.join(COMPLEX, "pom.xml")];
|
|
70
|
-
// env.profile should be 'dev' (from activeByDefault) — but properties are
|
|
71
|
-
// stored as xml2js arrays; unwrap when reading.
|
|
72
|
-
const envProfile = merged.properties["env.profile"];
|
|
73
|
-
const val = Array.isArray(envProfile) ? envProfile[0] : envProfile;
|
|
74
|
-
assert.equal(val, "dev");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("all-profile merge picks up deps from every profile", async () => {
|
|
78
|
-
const { props } = await pipeline(COMPLEX);
|
|
79
|
-
const root = props[path.join(COMPLEX, "pom.xml")];
|
|
80
|
-
const ids = root.dependencies.map(d => `${d.groupId?.[0]}:${d.artifactId?.[0]}`);
|
|
81
|
-
// Each profile contributed a dep — all three must be present.
|
|
82
|
-
assert.ok(ids.includes("com.h2database:h2"), "dev profile (h2) missing");
|
|
83
|
-
assert.ok(ids.includes("org.postgresql:postgresql"), "prod profile (postgres) missing");
|
|
84
|
-
assert.ok(ids.includes("com.acme.private:acme-oracle-driver"), "oracle profile (private) missing");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test("BOM import (scope=import) pulls in managed deps from local BOM", async () => {
|
|
88
|
-
const { props } = await pipeline(COMPLEX);
|
|
89
|
-
const api = props[path.join(COMPLEX, "api", "pom.xml")];
|
|
90
|
-
const mgmtIds = api.dependencyManagement.map(d => `${d.groupId?.[0]}:${d.artifactId?.[0]}`);
|
|
91
|
-
assert.ok(mgmtIds.includes("org.hibernate:hibernate-core"), "BOM hibernate not imported");
|
|
92
|
-
assert.ok(mgmtIds.includes("com.fasterxml.jackson.core:jackson-databind"), "BOM jackson not imported");
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("rewritePoms writes a clean tree in --target mode, target ≠ src", async () => {
|
|
96
|
-
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "fad-checker-target-"));
|
|
97
|
-
const { store, props, pomFiles } = await pipeline(COMPLEX);
|
|
98
|
-
const opts = {
|
|
99
|
-
srcRoot: COMPLEX, targetRoot: tmp,
|
|
100
|
-
deps2Exclude: /^com\.acme\.private$/, verbose: false, readOnly: false,
|
|
101
|
-
};
|
|
102
|
-
let wrote = 0;
|
|
103
|
-
for (const f of pomFiles) if (await core.rewritePoms(f, store, props, opts)) wrote++;
|
|
104
|
-
assert.ok(wrote >= 4, `expected ≥4 POMs written, got ${wrote}`);
|
|
105
|
-
const apiPomOut = path.join(tmp, "api", "pom.xml");
|
|
106
|
-
assert.ok(fs.existsSync(apiPomOut));
|
|
107
|
-
const apiOut = await parseStringPromise(fs.readFileSync(apiPomOut, "utf8"));
|
|
108
|
-
const deps = apiOut.project?.dependencies?.[0]?.dependency || [];
|
|
109
|
-
const ids = deps.map(d => `${d.groupId?.[0]}:${d.artifactId?.[0]}`);
|
|
110
|
-
// Private dep must be filtered out
|
|
111
|
-
assert.ok(!ids.includes("com.acme.private:acme-commons"), "private dep should be excluded");
|
|
112
|
-
// Public dep must remain
|
|
113
|
-
assert.ok(ids.includes("com.fasterxml.jackson.core:jackson-databind"), "public dep dropped unexpectedly");
|
|
114
|
-
// And excludedById must contain the private coord
|
|
115
|
-
assert.ok(store.excludedById["com.acme.private:acme-commons"], "excluded coord not flagged");
|
|
116
|
-
fs.rmSync(tmp, { recursive: true, force: true });
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test("rewritePoms in --test (readOnly) mode does not crash with undefined target", async () => {
|
|
120
|
-
const { store, props, pomFiles } = await pipeline(SIMPLE);
|
|
121
|
-
const opts = { srcRoot: SIMPLE, targetRoot: undefined, deps2Exclude: null, verbose: false, readOnly: true };
|
|
122
|
-
// Should not throw despite target being undefined
|
|
123
|
-
for (const f of pomFiles) await core.rewritePoms(f, store, props, opts);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test("missing external parent is flagged in missingById", async () => {
|
|
127
|
-
const { store, props, pomFiles } = await pipeline(PRIVATE_FIX);
|
|
128
|
-
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "fad-checker-priv-"));
|
|
129
|
-
const opts = {
|
|
130
|
-
srcRoot: PRIVATE_FIX, targetRoot: tmp,
|
|
131
|
-
deps2Exclude: /^(com\.client\.private|org\.megacorp)/,
|
|
132
|
-
verbose: false, readOnly: false,
|
|
133
|
-
};
|
|
134
|
-
for (const f of pomFiles) await core.rewritePoms(f, store, props, opts);
|
|
135
|
-
// The root pom's external parent org.megacorp.parents:megacorp-super-parent must be in missingById
|
|
136
|
-
assert.ok(
|
|
137
|
-
store.missingById["org.megacorp.parents:megacorp-super-parent"] ||
|
|
138
|
-
store.missingById["org.megacorp.parents:megacorp-super-parent:9.9.9-PRIVATE"],
|
|
139
|
-
"external private parent not tracked as missing"
|
|
140
|
-
);
|
|
141
|
-
fs.rmSync(tmp, { recursive: true, force: true });
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test("parent version in rewritten POM uses parent's version, not child's", async () => {
|
|
145
|
-
// Simple has child app with no own <version>; the rewritten parent ref should be 1.0.0
|
|
146
|
-
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "fad-checker-pv-"));
|
|
147
|
-
const { store, props, pomFiles } = await pipeline(SIMPLE);
|
|
148
|
-
const opts = { srcRoot: SIMPLE, targetRoot: tmp, deps2Exclude: null, verbose: false, readOnly: false };
|
|
149
|
-
for (const f of pomFiles) await core.rewritePoms(f, store, props, opts);
|
|
150
|
-
const appOut = await parseStringPromise(fs.readFileSync(path.join(tmp, "app", "pom.xml"), "utf8"));
|
|
151
|
-
assert.equal(appOut.project.parent[0].version[0], "1.0.0", "parent.version must equal parent's version");
|
|
152
|
-
fs.rmSync(tmp, { recursive: true, force: true });
|
|
153
|
-
});
|
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
|
-
});
|
package/test/cve-match.test.js
DELETED
|
@@ -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
|
-
});
|