fad-checker 1.0.6 → 2.0.0
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 +47 -0
- package/CLAUDE.md +27 -9
- package/README.md +27 -13
- package/completions/fad-checker.bash +4 -1
- package/completions/fad-checker.zsh +9 -0
- package/data/eol-mapping.json +17 -0
- package/docs/ARCHITECTURE.md +35 -7
- package/docs/USAGE.md +15 -7
- package/docs/superpowers/plans/2026-05-29-codec-composer.md +556 -0
- package/docs/superpowers/plans/2026-05-29-codec-foundation.md +851 -0
- package/docs/superpowers/plans/2026-05-29-codec-nuget.md +432 -0
- package/docs/superpowers/plans/2026-05-29-codec-pypi.md +450 -0
- package/docs/superpowers/specs/2026-05-29-codecs-multi-ecosystem-design.md +251 -0
- package/fad-checker.js +108 -82
- package/lib/codecs/codec.interface.js +27 -0
- package/lib/codecs/composer/parse.js +59 -0
- package/lib/codecs/composer/registry.js +92 -0
- package/lib/codecs/composer.codec.js +93 -0
- package/lib/codecs/index.js +37 -0
- package/lib/codecs/maven.codec.js +71 -0
- package/lib/{npm → codecs/npm}/collect.js +32 -18
- package/lib/codecs/npm.codec.js +52 -0
- package/lib/codecs/nuget/parse.js +75 -0
- package/lib/codecs/nuget/registry.js +88 -0
- package/lib/codecs/nuget.codec.js +92 -0
- package/lib/codecs/pypi/parse.js +71 -0
- package/lib/codecs/pypi/registry.js +89 -0
- package/lib/codecs/pypi.codec.js +81 -0
- package/lib/codecs/recipes.js +108 -0
- package/lib/codecs/select.js +27 -0
- package/lib/codecs/yarn.codec.js +19 -0
- package/lib/cve-match.js +3 -6
- package/lib/cve-report.js +34 -70
- package/lib/dep-record.js +60 -0
- package/lib/osv.js +22 -14
- package/lib/outdated.js +13 -2
- package/package.json +3 -2
- package/test/cli-ecosystem.test.js +30 -0
- package/test/codec-edge-cases.test.js +131 -0
- package/test/codec-integration.test.js +119 -0
- package/test/codecs.test.js +92 -0
- package/test/composer.test.js +71 -0
- package/test/cve-report.test.js +9 -0
- package/test/dep-record.test.js +31 -0
- package/test/fixtures/csharp-config/packages.config +4 -0
- package/test/fixtures/csharp-csproj/Directory.Packages.props +5 -0
- package/test/fixtures/csharp-csproj/app.csproj +7 -0
- package/test/fixtures/csharp-lock/packages.lock.json +6 -0
- package/test/fixtures/monorepo-mixed/packages/no-lock/package.json +2 -1
- package/test/fixtures/php-app/composer.json +5 -0
- package/test/fixtures/php-app/composer.lock +10 -0
- package/test/fixtures/polyglot/cs/packages.lock.json +1 -0
- package/test/fixtures/polyglot/js/package-lock.json +1 -0
- package/test/fixtures/polyglot/js/package.json +1 -0
- package/test/fixtures/polyglot/mvn/pom.xml +7 -0
- package/test/fixtures/polyglot/php/composer.lock +1 -0
- package/test/fixtures/polyglot/py/poetry.lock +3 -0
- package/test/fixtures/python-pipenv/Pipfile.lock +1 -0
- package/test/fixtures/python-poetry/poetry.lock +7 -0
- package/test/fixtures/python-reqs/requirements.txt +5 -0
- package/test/fixtures/python-uv/uv.lock +3 -0
- package/test/monorepo.test.js +1 -1
- package/test/npm-registry.test.js +1 -1
- package/test/npm.test.js +8 -4
- package/test/nuget.test.js +66 -0
- package/test/osv.test.js +62 -0
- package/test/pypi.test.js +72 -0
- package/test/webjar.test.js +1 -1
- /package/lib/{npm → codecs/npm}/parse.js +0 -0
- /package/lib/{npm → codecs/npm}/registry.js +0 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { assertCodecShape } = require("../lib/codecs/codec.interface");
|
|
5
|
+
|
|
6
|
+
const COMPLETE_STUB = {
|
|
7
|
+
id: "x", label: "X", osvEcosystem: "npm",
|
|
8
|
+
manifestNames: ["x.json"],
|
|
9
|
+
detect: () => false,
|
|
10
|
+
collect: async () => ({ deps: new Map(), warnings: [] }),
|
|
11
|
+
coordKey: d => `x:${d.name}`,
|
|
12
|
+
formatCoord: d => d.name,
|
|
13
|
+
osvPackageName: d => d.name,
|
|
14
|
+
checkRegistry: async () => ({ outdated: [], deprecated: [] }),
|
|
15
|
+
resolveEolProduct: () => null,
|
|
16
|
+
recipe: { label: "X", pinSection: "", pinIntro: () => "", snippet: () => "", directSection: "" },
|
|
17
|
+
nativeScanners: [],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
test("assertCodecShape accepts a complete codec stub", () => {
|
|
21
|
+
assert.doesNotThrow(() => assertCodecShape(COMPLETE_STUB));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("assertCodecShape rejects a codec missing a required method", () => {
|
|
25
|
+
assert.throws(() => assertCodecShape({ id: "y" }), /missing|y/i);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const maven = require("../lib/codecs/maven.codec");
|
|
29
|
+
|
|
30
|
+
test("maven codec detects the simple fixture and collects deps with bare g:a coordKeys", async () => {
|
|
31
|
+
const dir = path.join(__dirname, "fixtures", "simple");
|
|
32
|
+
assert.strictEqual(maven.detect(dir), true);
|
|
33
|
+
const { deps } = await maven.collect(dir, {});
|
|
34
|
+
assert.ok(deps.size > 0);
|
|
35
|
+
for (const [k, d] of deps) {
|
|
36
|
+
assert.strictEqual(d.ecosystem, "maven");
|
|
37
|
+
assert.strictEqual(d.coordKey, k);
|
|
38
|
+
assert.ok(!k.startsWith("npm:") && !k.startsWith("nuget:"), `maven key ${k} must stay bare`);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const npm = require("../lib/codecs/npm.codec");
|
|
43
|
+
|
|
44
|
+
test("npm codec collects from monorepo-mixed with npm: coordKeys", async () => {
|
|
45
|
+
const dir = path.join(__dirname, "fixtures", "monorepo-mixed");
|
|
46
|
+
assert.strictEqual(npm.detect(dir), true);
|
|
47
|
+
const { deps } = await npm.collect(dir, {});
|
|
48
|
+
assert.ok(deps.size > 0);
|
|
49
|
+
for (const [k, d] of deps) {
|
|
50
|
+
assert.ok(k.startsWith("npm:"), `key ${k} should be npm-namespaced`);
|
|
51
|
+
assert.strictEqual(d.ecosystem, "npm");
|
|
52
|
+
assert.strictEqual(d.coordKey, k);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("yarn codec is a no-op collector (npm codec does the JS scan)", async () => {
|
|
57
|
+
const yarn = require("../lib/codecs/yarn.codec");
|
|
58
|
+
assert.strictEqual(yarn.id, "yarn");
|
|
59
|
+
const { deps } = await yarn.collect("/whatever", {});
|
|
60
|
+
assert.strictEqual(deps.size, 0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const { getCodec, allCodecs, detectCodecs } = require("../lib/codecs");
|
|
64
|
+
|
|
65
|
+
test("registry exposes maven/npm/yarn and validates their shape", () => {
|
|
66
|
+
const ids = allCodecs().map(c => c.id).sort();
|
|
67
|
+
assert.deepStrictEqual(ids, ["composer", "maven", "npm", "nuget", "pypi", "yarn"]);
|
|
68
|
+
for (const c of allCodecs()) assertCodecShape(c);
|
|
69
|
+
assert.strictEqual(getCodec("maven").id, "maven");
|
|
70
|
+
assert.strictEqual(getCodec("nope"), null);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("detectCodecs finds maven+npm on monorepo-mixed, not yarn duplicate", () => {
|
|
74
|
+
const dir = path.join(__dirname, "fixtures", "monorepo-mixed");
|
|
75
|
+
const detected = detectCodecs(dir).map(c => c.id);
|
|
76
|
+
assert.ok(detected.includes("maven"));
|
|
77
|
+
assert.ok(detected.includes("npm"));
|
|
78
|
+
assert.ok(!detected.includes("yarn"));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const recipes = require("../lib/codecs/recipes");
|
|
82
|
+
|
|
83
|
+
test("each codec recipe exposes label + snippet function", () => {
|
|
84
|
+
for (const c of allCodecs()) {
|
|
85
|
+
assert.strictEqual(typeof c.recipe.label, "string");
|
|
86
|
+
assert.strictEqual(typeof c.recipe.snippet, "function");
|
|
87
|
+
}
|
|
88
|
+
// snippet output matches the legacy format
|
|
89
|
+
const xml = recipes.maven.snippet([{ groupId: "g", artifactId: "a", fixVersion: "1.0" }]);
|
|
90
|
+
assert.match(xml, /<dependencyManagement>/);
|
|
91
|
+
assert.match(xml, /<artifactId>a<\/artifactId>/);
|
|
92
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
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/cve-report.test.js
CHANGED
|
@@ -169,3 +169,12 @@ test("writeReports writes both files to disk", async () => {
|
|
|
169
169
|
assert.ok(html.includes("log4j:log4j"));
|
|
170
170
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
171
171
|
});
|
|
172
|
+
|
|
173
|
+
const { depDisplayName, depDisplayParts } = require("../lib/cve-report");
|
|
174
|
+
|
|
175
|
+
test("depDisplayName delegates to codec and preserves legacy display", () => {
|
|
176
|
+
assert.strictEqual(depDisplayName({ ecosystem: "maven", ecosystemType: "maven", namespace: "org.apache", name: "log4j", groupId: "org.apache", artifactId: "log4j" }), "org.apache:log4j");
|
|
177
|
+
assert.strictEqual(depDisplayName({ ecosystem: "npm", ecosystemType: "npm", namespace: "", name: "lodash", artifactId: "lodash" }), "npm:lodash");
|
|
178
|
+
const parts = depDisplayParts({ ecosystem: "npm", ecosystemType: "npm", namespace: "", name: "lodash", artifactId: "lodash" });
|
|
179
|
+
assert.deepStrictEqual(parts, { groupLine: "npm:", nameLine: "lodash" });
|
|
180
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert");
|
|
3
|
+
const { makeDepRecord, coordKeyFor } = require("../lib/dep-record");
|
|
4
|
+
|
|
5
|
+
test("maven depRecord builds bare g:a coordKey and keeps groupId/artifactId aliases", () => {
|
|
6
|
+
const d = makeDepRecord({ ecosystem: "maven", namespace: "org.apache", name: "log4j", version: "2.14.0", manifestPath: "/p/pom.xml", scope: "compile" });
|
|
7
|
+
assert.strictEqual(d.coordKey, "org.apache:log4j"); // clé Maven brute (pas de préfixe)
|
|
8
|
+
assert.strictEqual(d.groupId, "org.apache"); // alias rétro-compat
|
|
9
|
+
assert.strictEqual(d.artifactId, "log4j"); // alias rétro-compat
|
|
10
|
+
assert.deepStrictEqual(d.versions, ["2.14.0"]);
|
|
11
|
+
assert.strictEqual(d.isDev, false);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("npm depRecord has empty namespace and npm-prefixed coordKey", () => {
|
|
15
|
+
const d = makeDepRecord({ ecosystem: "npm", namespace: "", name: "lodash", version: "4.17.20", manifestPath: "/p/package-lock.json", scope: "prod" });
|
|
16
|
+
assert.strictEqual(d.coordKey, "npm:lodash");
|
|
17
|
+
assert.strictEqual(d.groupId, "");
|
|
18
|
+
assert.strictEqual(d.artifactId, "lodash");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("coordKeyFor composes ecosystem + namespace + name", () => {
|
|
22
|
+
assert.strictEqual(coordKeyFor("composer", "guzzlehttp", "guzzle"), "composer:guzzlehttp/guzzle");
|
|
23
|
+
assert.strictEqual(coordKeyFor("pypi", "", "requests"), "pypi:requests");
|
|
24
|
+
assert.strictEqual(coordKeyFor("nuget", "", "Newtonsoft.Json"), "nuget:newtonsoft.json");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("pomPaths shares the manifestPaths array reference (push stays in sync)", () => {
|
|
28
|
+
const d = makeDepRecord({ ecosystem: "maven", namespace: "g", name: "a", version: "1.0", manifestPath: "/p/pom.xml" });
|
|
29
|
+
d.manifestPaths.push("/q/pom.xml");
|
|
30
|
+
assert.deepStrictEqual(d.pomPaths, ["/p/pom.xml", "/q/pom.xml"]);
|
|
31
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"packages": [
|
|
3
|
+
{ "name": "guzzlehttp/guzzle", "version": "7.4.5" },
|
|
4
|
+
{ "name": "monolog/monolog", "version": "2.9.1" },
|
|
5
|
+
{ "name": "symfony/console", "version": "v6.2.10" }
|
|
6
|
+
],
|
|
7
|
+
"packages-dev": [
|
|
8
|
+
{ "name": "phpunit/phpunit", "version": "10.1.0" }
|
|
9
|
+
]
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "version":1,"dependencies":{"net6.0":{"Newtonsoft.Json":{"type":"Direct","resolved":"13.0.1"}}} }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "name":"poly-js","lockfileVersion":3,"packages":{"node_modules/lodash":{"version":"4.17.21"}} }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "name":"poly-js","dependencies":{"lodash":"^4.17.0"} }
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
|
2
|
+
<modelVersion>4.0.0</modelVersion>
|
|
3
|
+
<groupId>com.acme</groupId><artifactId>poly</artifactId><version>1.0.0</version>
|
|
4
|
+
<dependencies>
|
|
5
|
+
<dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-core</artifactId><version>2.14.0</version></dependency>
|
|
6
|
+
</dependencies>
|
|
7
|
+
</project>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "packages":[{"name":"guzzlehttp/guzzle","version":"7.4.5"}],"packages-dev":[] }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "default": { "django": { "version": "==4.2.0" } }, "develop": { "pytest": { "version": "==7.4.0" } } }
|
package/test/monorepo.test.js
CHANGED
|
@@ -11,7 +11,7 @@ const assert = require("node:assert/strict");
|
|
|
11
11
|
const path = require("path");
|
|
12
12
|
const core = require("../lib/core");
|
|
13
13
|
const { collectResolvedDeps } = require("../lib/cve-match");
|
|
14
|
-
const { collectNpmDeps, hasJsManifests } = require("../lib/npm/collect");
|
|
14
|
+
const { collectNpmDeps, hasJsManifests } = require("../lib/codecs/npm/collect");
|
|
15
15
|
|
|
16
16
|
const FIX = path.join(__dirname, "fixtures", "monorepo-mixed");
|
|
17
17
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { test } = require("node:test");
|
|
2
2
|
const assert = require("node:assert/strict");
|
|
3
|
-
const { packumentToFindings } = require("../lib/npm/registry");
|
|
3
|
+
const { packumentToFindings } = require("../lib/codecs/npm/registry");
|
|
4
4
|
|
|
5
5
|
const dep = (name, version) => ({ ecosystem: "npm", groupId: "", artifactId: name, version });
|
|
6
6
|
|
package/test/npm.test.js
CHANGED
|
@@ -6,8 +6,8 @@ const {
|
|
|
6
6
|
parsePackageLock,
|
|
7
7
|
parseYarnLockV1,
|
|
8
8
|
findJsManifests,
|
|
9
|
-
} = require("../lib/npm/parse");
|
|
10
|
-
const { collectNpmDeps } = require("../lib/npm/collect");
|
|
9
|
+
} = require("../lib/codecs/npm/parse");
|
|
10
|
+
const { collectNpmDeps } = require("../lib/codecs/npm/collect");
|
|
11
11
|
|
|
12
12
|
const FIX = path.join(__dirname, "fixtures", "monorepo-mixed");
|
|
13
13
|
|
|
@@ -110,13 +110,17 @@ test("collectNpmDeps skips package.json when a lockfile is present in the same d
|
|
|
110
110
|
}
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
-
test("collectNpmDeps
|
|
113
|
+
test("collectNpmDeps no-lockfile fallback: warns + scans pinned, skips ranges", () => {
|
|
114
114
|
const map = collectNpmDeps(FIX, { verbose: false });
|
|
115
115
|
assert.ok(Array.isArray(map.warnings), "warnings array should be present");
|
|
116
116
|
const w = map.warnings.find(x => x.type === "no-lockfile" && x.manifestPath.includes("no-lock"));
|
|
117
117
|
assert.ok(w, "expected a no-lockfile warning for packages/no-lock/package.json");
|
|
118
|
-
|
|
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").
|
|
119
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");
|
|
120
124
|
});
|
|
121
125
|
|
|
122
126
|
test("parsePackageLock tags flattened-transitive entries as scope='transitive'", () => {
|
|
@@ -0,0 +1,66 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { pep503, parsePoetryLock, parsePipfileLock, parseUvLock, parseRequirementsTxt } = require("../lib/codecs/pypi/parse");
|
|
5
|
+
|
|
6
|
+
const F = n => path.join(__dirname, "fixtures", n);
|
|
7
|
+
|
|
8
|
+
test("pep503 normalizes names (lowercase, collapse separators to -)", () => {
|
|
9
|
+
assert.strictEqual(pep503("Flask-SQLAlchemy"), "flask-sqlalchemy");
|
|
10
|
+
assert.strictEqual(pep503("zope.interface"), "zope-interface");
|
|
11
|
+
assert.strictEqual(pep503("My__Pkg"), "my-pkg");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("parsePoetryLock returns PEP503 names + versions", () => {
|
|
15
|
+
const r = parsePoetryLock(F("python-poetry/poetry.lock"));
|
|
16
|
+
const m = Object.fromEntries(r.deps.map(d => [d.name, d.version]));
|
|
17
|
+
assert.strictEqual(m["requests"], "2.31.0");
|
|
18
|
+
assert.strictEqual(m["flask-sqlalchemy"], "3.0.5"); // normalized
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("parsePipfileLock splits default/develop, strips ==", () => {
|
|
22
|
+
const r = parsePipfileLock(F("python-pipenv/Pipfile.lock"));
|
|
23
|
+
const m = Object.fromEntries(r.deps.map(d => [d.name, d]));
|
|
24
|
+
assert.strictEqual(m["django"].version, "4.2.0");
|
|
25
|
+
assert.strictEqual(m["django"].scope, "prod");
|
|
26
|
+
assert.strictEqual(m["pytest"].scope, "dev");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("parseUvLock reads [[package]]", () => {
|
|
30
|
+
const r = parseUvLock(F("python-uv/uv.lock"));
|
|
31
|
+
assert.strictEqual(r.deps.find(d => d.name === "numpy").version, "1.26.0");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("parseRequirementsTxt keeps == pins, skips ranges/flags/comments", () => {
|
|
35
|
+
const r = parseRequirementsTxt(F("python-reqs/requirements.txt"));
|
|
36
|
+
const names = r.deps.map(d => d.name).sort();
|
|
37
|
+
assert.deepStrictEqual(names, ["fastapi", "urllib3"]);
|
|
38
|
+
assert.strictEqual(r.skipped, 1); // flask>=2.0
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const { pypiToFindings } = require("../lib/codecs/pypi/registry");
|
|
42
|
+
test("pypiToFindings extracts latest, yanked-for-version, inactive classifier", () => {
|
|
43
|
+
const data = {
|
|
44
|
+
info: { version: "2.1.0", classifiers: ["Development Status :: 7 - Inactive"] },
|
|
45
|
+
releases: { "2.0.4": [{ yanked: true, yanked_reason: "security" }], "2.1.0": [{ yanked: false }] },
|
|
46
|
+
};
|
|
47
|
+
const f = pypiToFindings(data, { version: "2.0.4" });
|
|
48
|
+
assert.strictEqual(f.outdated.latest, "2.1.0");
|
|
49
|
+
assert.strictEqual(f.yanked.reason, "security");
|
|
50
|
+
assert.strictEqual(f.inactive, true);
|
|
51
|
+
const f2 = pypiToFindings(data, { version: "2.1.0" });
|
|
52
|
+
assert.strictEqual(f2.yanked, null);
|
|
53
|
+
assert.strictEqual(f2.outdated, null);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const pypi = require("../lib/codecs/pypi.codec");
|
|
57
|
+
const { assertCodecShape } = require("../lib/codecs/codec.interface");
|
|
58
|
+
test("pypi codec: shape, detect, collect, coordKey pypi:<name>", async () => {
|
|
59
|
+
assertCodecShape(pypi);
|
|
60
|
+
assert.strictEqual(pypi.detect(F("python-poetry")), true);
|
|
61
|
+
const { deps } = await pypi.collect(F("python-poetry"), {});
|
|
62
|
+
const r = deps.get("pypi:requests");
|
|
63
|
+
assert.ok(r);
|
|
64
|
+
assert.strictEqual(r.ecosystem, "pypi");
|
|
65
|
+
assert.strictEqual(pypi.osvPackageName(r), "requests");
|
|
66
|
+
});
|
|
67
|
+
test("pypi collect: requirements.txt fallback warns + scans pins only", async () => {
|
|
68
|
+
const { deps, warnings } = await pypi.collect(F("python-reqs"), {});
|
|
69
|
+
assert.ok(deps.has("pypi:fastapi"));
|
|
70
|
+
assert.ok(!deps.has("pypi:flask"));
|
|
71
|
+
assert.ok(warnings.find(w => w.type === "no-lockfile"));
|
|
72
|
+
});
|
package/test/webjar.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { test } = require("node:test");
|
|
2
2
|
const assert = require("node:assert/strict");
|
|
3
|
-
const { webjarToNpm } = require("../lib/npm/collect");
|
|
3
|
+
const { webjarToNpm } = require("../lib/codecs/npm/collect");
|
|
4
4
|
|
|
5
5
|
test("webjarToNpm derives the npm name from org.webjars.npm (deterministic mirror)", () => {
|
|
6
6
|
assert.deepEqual(
|
|
File without changes
|
|
File without changes
|