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.
- package/CHANGELOG.md +62 -0
- package/README.md +77 -15
- package/completions/fad-checker.bash +4 -1
- package/completions/fad-checker.zsh +9 -0
- package/data/eol-mapping.json +17 -0
- package/fad-checker.js +353 -241
- 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 +58 -35
- package/lib/{npm → codecs/npm}/parse.js +114 -10
- package/lib/{npm → codecs/npm}/registry.js +4 -3
- 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 +94 -0
- package/lib/codecs/pypi/parse.js +163 -0
- package/lib/codecs/pypi/registry.js +89 -0
- package/lib/codecs/pypi.codec.js +102 -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/core.js +35 -1
- package/lib/cve-match.js +18 -11
- package/lib/cve-report.js +34 -70
- package/lib/dep-record.js +60 -0
- package/lib/deps-descriptor.js +110 -0
- package/lib/nvd.js +4 -3
- package/lib/osv.js +29 -18
- package/lib/outdated.js +20 -4
- package/lib/retire.js +77 -13
- package/lib/transitive.js +3 -3
- package/lib/ui.js +87 -0
- package/package.json +4 -2
- package/CLAUDE.md +0 -129
- package/docs/ARCHITECTURE.md +0 -154
- package/docs/USAGE.md +0 -179
- 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 -171
- 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/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 -9
- 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/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/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 -146
- package/test/outdated.test.js +0 -101
- package/test/snyk.test.js +0 -64
- package/test/transitive.test.js +0 -305
- package/test/webjar.test.js +0 -33
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/codecs/codec.interface.js — contrat que tout codec doit respecter.
|
|
3
|
+
*
|
|
4
|
+
* Pas de classe imposée : un codec est un objet litéral exportant ces clés.
|
|
5
|
+
* Voir docs/superpowers/specs/2026-05-29-codecs-multi-ecosystem-design.md.
|
|
6
|
+
*/
|
|
7
|
+
const REQUIRED_KEYS = [
|
|
8
|
+
"id", "label", "osvEcosystem", "manifestNames",
|
|
9
|
+
"detect", "collect", "coordKey", "formatCoord", "osvPackageName",
|
|
10
|
+
"checkRegistry", "resolveEolProduct", "recipe", "nativeScanners",
|
|
11
|
+
];
|
|
12
|
+
const FUNCTION_KEYS = ["detect", "collect", "coordKey", "formatCoord", "osvPackageName", "checkRegistry", "resolveEolProduct"];
|
|
13
|
+
|
|
14
|
+
function assertCodecShape(codec) {
|
|
15
|
+
if (!codec || typeof codec !== "object") throw new Error("codec must be an object");
|
|
16
|
+
for (const k of REQUIRED_KEYS) {
|
|
17
|
+
if (!(k in codec)) throw new Error(`codec "${codec.id || "?"}" missing required key: ${k}`);
|
|
18
|
+
}
|
|
19
|
+
for (const k of FUNCTION_KEYS) {
|
|
20
|
+
if (typeof codec[k] !== "function") throw new Error(`codec "${codec.id}" key ${k} must be a function`);
|
|
21
|
+
}
|
|
22
|
+
if (!Array.isArray(codec.manifestNames)) throw new Error(`codec "${codec.id}" manifestNames must be an array`);
|
|
23
|
+
if (!Array.isArray(codec.nativeScanners)) throw new Error(`codec "${codec.id}" nativeScanners must be an array`);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { REQUIRED_KEYS, FUNCTION_KEYS, assertCodecShape };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/composer/parse.js — parse PHP Composer manifests.
|
|
3
|
+
*
|
|
4
|
+
* composer.lock — { packages: [{name, version}], "packages-dev": [...] }
|
|
5
|
+
* versions are concrete (resolved) — the ideal source.
|
|
6
|
+
* composer.json — { require: {name: range}, "require-dev": {...} }
|
|
7
|
+
* used only as a no-lock fallback (pinned exact versions).
|
|
8
|
+
*
|
|
9
|
+
* Composer names are "vendor/package", case-insensitive (lowercased by convention).
|
|
10
|
+
*/
|
|
11
|
+
const fs = require("fs");
|
|
12
|
+
|
|
13
|
+
// "1.2.3" | "v1.2.3" | "dev-main" | "1.0.x-dev". Strip a leading "v" for OSV/registry.
|
|
14
|
+
function normVersion(v) {
|
|
15
|
+
if (!v) return null;
|
|
16
|
+
return String(v).replace(/^v/, "");
|
|
17
|
+
}
|
|
18
|
+
function isConcrete(v) {
|
|
19
|
+
if (!v) return false;
|
|
20
|
+
return /^\d+(\.\d+)*([.\-+]\S+)?$/.test(String(v).replace(/^v/, ""));
|
|
21
|
+
}
|
|
22
|
+
function splitName(full) {
|
|
23
|
+
const i = full.indexOf("/");
|
|
24
|
+
if (i < 0) return { vendor: "", pkg: full };
|
|
25
|
+
return { vendor: full.slice(0, i), pkg: full.slice(i + 1) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseComposerLock(filePath) {
|
|
29
|
+
const json = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
30
|
+
const deps = [];
|
|
31
|
+
const push = (arr, scope) => {
|
|
32
|
+
for (const p of arr || []) {
|
|
33
|
+
if (!p.name) continue;
|
|
34
|
+
const { vendor, pkg } = splitName(p.name);
|
|
35
|
+
deps.push({ name: p.name, vendor, pkg, version: normVersion(p.version), scope, isDev: scope === "dev" });
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
push(json.packages, "prod");
|
|
39
|
+
push(json["packages-dev"], "dev");
|
|
40
|
+
return { manifestPath: filePath, manifestType: "composer.lock", deps };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseComposerJson(filePath) {
|
|
44
|
+
const json = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
45
|
+
const deps = [];
|
|
46
|
+
const push = (obj, scope) => {
|
|
47
|
+
for (const [name, version] of Object.entries(obj || {})) {
|
|
48
|
+
// Platform requirements aren't packages.
|
|
49
|
+
if (name === "php" || name.startsWith("ext-") || name.startsWith("lib-")) continue;
|
|
50
|
+
const { vendor, pkg } = splitName(name);
|
|
51
|
+
deps.push({ name, vendor, pkg, version: String(version), scope, isDev: scope === "dev" });
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
push(json.require, "prod");
|
|
55
|
+
push(json["require-dev"], "dev");
|
|
56
|
+
return { manifestPath: filePath, manifestType: "composer.json", deps };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { parseComposerLock, parseComposerJson, normVersion, isConcrete, splitName };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/composer/registry.js — query Packagist for a package's latest stable
|
|
3
|
+
* version and its `abandoned` flag (≈ npm `deprecated`).
|
|
4
|
+
*
|
|
5
|
+
* API: https://packagist.org/packages/{vendor}/{pkg}.json
|
|
6
|
+
* → { package: { abandoned: bool|string, versions: { "<v>": {...} } } }
|
|
7
|
+
*
|
|
8
|
+
* Cached in ~/.fad-checker/packagist-cache.json for 24h.
|
|
9
|
+
*/
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
const os = require("os");
|
|
13
|
+
let pLimit; try { pLimit = require("p-limit"); } catch { pLimit = () => (fn) => fn(); }
|
|
14
|
+
|
|
15
|
+
const CACHE_DIR = path.join(os.homedir(), ".fad-checker");
|
|
16
|
+
const CACHE_PATH = path.join(CACHE_DIR, "packagist-cache.json");
|
|
17
|
+
const CACHE_MAX_AGE_MS = 24 * 3600 * 1000;
|
|
18
|
+
const API = "https://packagist.org/packages";
|
|
19
|
+
|
|
20
|
+
function loadCache() { try { return JSON.parse(fs.readFileSync(CACHE_PATH, "utf8")); } catch { return { entries: {}, meta: {} }; } }
|
|
21
|
+
function saveCache(d) { try { fs.mkdirSync(CACHE_DIR, { recursive: true }); fs.writeFileSync(CACHE_PATH, JSON.stringify(d)); } catch { /* ignore */ } }
|
|
22
|
+
|
|
23
|
+
function isStable(v) { return /^\d+(\.\d+)*$/.test(String(v || "").replace(/^v/, "")); }
|
|
24
|
+
function cmp(a, b) {
|
|
25
|
+
const pa = String(a).replace(/^v/, "").split(/[.\-+]/).map(n => parseInt(n, 10) || 0);
|
|
26
|
+
const pb = String(b).replace(/^v/, "").split(/[.\-+]/).map(n => parseInt(n, 10) || 0);
|
|
27
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) { const d = (pa[i] || 0) - (pb[i] || 0); if (d) return d > 0 ? 1 : -1; }
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Extract {abandoned, outdated} from a Packagist `package` object.
|
|
32
|
+
function packagistToFindings(pkg, { version }) {
|
|
33
|
+
const out = { abandoned: null, outdated: null };
|
|
34
|
+
if (pkg.abandoned === true) out.abandoned = { replacement: null };
|
|
35
|
+
else if (typeof pkg.abandoned === "string") out.abandoned = { replacement: pkg.abandoned };
|
|
36
|
+
const stable = Object.keys(pkg.versions || {}).map(v => v.replace(/^v/, "")).filter(isStable);
|
|
37
|
+
if (stable.length) {
|
|
38
|
+
const latest = stable.sort(cmp).at(-1);
|
|
39
|
+
if (latest && cmp(latest, String(version).replace(/^v/, "")) > 0) out.outdated = { latest };
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function fetchPackage(name, { offline }) {
|
|
45
|
+
if (offline) return null;
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch(`${API}/${name}.json`, { headers: { "User-Agent": "fad-checker-packagist" } });
|
|
48
|
+
if (!res.ok) return { error: `HTTP ${res.status}` };
|
|
49
|
+
const json = await res.json();
|
|
50
|
+
return json.package || { error: "no package field" };
|
|
51
|
+
} catch (e) { return { error: e.message }; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Mirror of checkNpmRegistryDeps: returns { deprecated:[], outdated:[] }.
|
|
55
|
+
async function checkComposerRegistryDeps(deps, opts = {}) {
|
|
56
|
+
const { verbose, offline, allLibs = true, concurrency = 8 } = opts;
|
|
57
|
+
const targets = [...deps.values()].filter(d => d.ecosystem === "composer" && d.version);
|
|
58
|
+
const result = { deprecated: [], outdated: [] };
|
|
59
|
+
if (!targets.length) return result;
|
|
60
|
+
const cache = loadCache();
|
|
61
|
+
const fresh = cache.meta?.fetchedAt && (Date.now() - cache.meta.fetchedAt) < CACHE_MAX_AGE_MS;
|
|
62
|
+
if (!fresh && !offline) cache.entries = {};
|
|
63
|
+
const limit = pLimit(concurrency);
|
|
64
|
+
await Promise.all(targets.map(t => limit(async () => {
|
|
65
|
+
const name = `${t.namespace}/${t.name}`.toLowerCase();
|
|
66
|
+
const key = `${name}@${t.version}`;
|
|
67
|
+
let ex = cache.entries[key];
|
|
68
|
+
if (!ex) {
|
|
69
|
+
const pkg = await fetchPackage(name, { offline });
|
|
70
|
+
if (pkg && !pkg.error) {
|
|
71
|
+
const f = packagistToFindings(pkg, { version: t.version });
|
|
72
|
+
ex = { abandoned: f.abandoned, latest: f.outdated?.latest || null };
|
|
73
|
+
cache.entries[key] = ex;
|
|
74
|
+
} else {
|
|
75
|
+
ex = { abandoned: null, latest: null };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (ex.abandoned) {
|
|
79
|
+
result.deprecated.push({ dep: t, severity: "MEDIUM", replacement: ex.abandoned.replacement, reason: "Package marked abandoned on Packagist", source: "packagist" });
|
|
80
|
+
if (verbose) process.stdout.write(` abandoned: ${name}@${t.version}\n`);
|
|
81
|
+
}
|
|
82
|
+
if (allLibs && ex.latest) {
|
|
83
|
+
result.outdated.push({ dep: t, latest: ex.latest, releaseDate: null });
|
|
84
|
+
if (verbose) process.stdout.write(` outdated: ${name} ${t.version} → ${ex.latest}\n`);
|
|
85
|
+
}
|
|
86
|
+
})));
|
|
87
|
+
cache.meta = { fetchedAt: Date.now() };
|
|
88
|
+
saveCache(cache);
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { packagistToFindings, checkComposerRegistryDeps };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/codecs/composer.codec.js — codec PHP/Composer.
|
|
3
|
+
*
|
|
4
|
+
* Vuln scanning is OSV (ecosystem "Packagist", wired in Plan A). This codec adds
|
|
5
|
+
* collection (composer.lock, composer.json fallback), Packagist registry
|
|
6
|
+
* (abandoned + outdated), and EOL via endoflife.date.
|
|
7
|
+
*/
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const { makeDepRecord, coordKeyFor } = require("../dep-record");
|
|
11
|
+
const { parseComposerLock, parseComposerJson, isConcrete } = require("./composer/parse");
|
|
12
|
+
|
|
13
|
+
const SKIP = new Set(["vendor", ".git", ".idea", ".vscode", "node_modules", "dist", "build", "out", "target"]);
|
|
14
|
+
|
|
15
|
+
function findComposerManifests(dir) {
|
|
16
|
+
const groups = [];
|
|
17
|
+
const stack = [dir];
|
|
18
|
+
while (stack.length) {
|
|
19
|
+
const cur = stack.pop();
|
|
20
|
+
let entries; try { entries = fs.readdirSync(cur, { withFileTypes: true }); } catch { continue; }
|
|
21
|
+
const names = new Set(entries.filter(e => e.isFile()).map(e => e.name));
|
|
22
|
+
if (names.has("composer.json") || names.has("composer.lock")) {
|
|
23
|
+
groups.push({
|
|
24
|
+
dir: cur,
|
|
25
|
+
composerJson: names.has("composer.json") ? path.join(cur, "composer.json") : null,
|
|
26
|
+
composerLock: names.has("composer.lock") ? path.join(cur, "composer.lock") : null,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
for (const e of entries) if (e.isDirectory() && !SKIP.has(e.name)) stack.push(path.join(cur, e.name));
|
|
30
|
+
}
|
|
31
|
+
return groups;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
id: "composer",
|
|
36
|
+
label: "Composer",
|
|
37
|
+
osvEcosystem: "Packagist",
|
|
38
|
+
manifestNames: ["composer.json", "composer.lock"],
|
|
39
|
+
|
|
40
|
+
detect(dir) { return findComposerManifests(dir).length > 0; },
|
|
41
|
+
|
|
42
|
+
async collect(dir, opts = {}) {
|
|
43
|
+
const { ignoreTest, deps2Exclude } = opts;
|
|
44
|
+
const out = new Map();
|
|
45
|
+
const warnings = [];
|
|
46
|
+
for (const g of findComposerManifests(dir)) {
|
|
47
|
+
if (g.composerLock) {
|
|
48
|
+
let parsed;
|
|
49
|
+
try { parsed = parseComposerLock(g.composerLock); }
|
|
50
|
+
catch (e) { warnings.push({ type: "parse-error", manifestPath: g.composerLock, message: `composer.lock parse failed: ${e.message}` }); continue; }
|
|
51
|
+
const { deps } = parsed;
|
|
52
|
+
for (const d of deps) {
|
|
53
|
+
if (ignoreTest && d.isDev) continue;
|
|
54
|
+
if (deps2Exclude && deps2Exclude.test(d.name)) continue;
|
|
55
|
+
out.set(coordKeyFor("composer", d.vendor, d.pkg),
|
|
56
|
+
makeDepRecord({ ecosystem: "composer", namespace: d.vendor, name: d.pkg, version: d.version, manifestPath: g.composerLock, scope: d.scope, isDev: d.isDev }));
|
|
57
|
+
}
|
|
58
|
+
} else if (g.composerJson) {
|
|
59
|
+
// No lockfile → best-effort: pinned exact versions only + warning.
|
|
60
|
+
let parsed;
|
|
61
|
+
try { parsed = parseComposerJson(g.composerJson); }
|
|
62
|
+
catch (e) { warnings.push({ type: "parse-error", manifestPath: g.composerJson, message: `composer.json parse failed: ${e.message}` }); continue; }
|
|
63
|
+
const { deps } = parsed;
|
|
64
|
+
let pinned = 0, ranges = 0;
|
|
65
|
+
for (const d of deps) {
|
|
66
|
+
if (ignoreTest && d.isDev) continue;
|
|
67
|
+
if (deps2Exclude && deps2Exclude.test(d.name)) continue;
|
|
68
|
+
if (isConcrete(d.version)) {
|
|
69
|
+
out.set(coordKeyFor("composer", d.vendor, d.pkg),
|
|
70
|
+
makeDepRecord({ ecosystem: "composer", namespace: d.vendor, name: d.pkg, version: String(d.version).replace(/^v/, ""), manifestPath: g.composerJson, scope: d.scope, isDev: d.isDev }));
|
|
71
|
+
pinned++;
|
|
72
|
+
} else {
|
|
73
|
+
ranges++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
warnings.push({ type: "no-lockfile", manifestPath: g.composerJson, message: `composer.json without composer.lock — best-effort: ${pinned} pinned, ${ranges} range(s) skipped (run "composer install")` });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { deps: out, warnings };
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
coordKey(d) { return coordKeyFor("composer", d.namespace || "", d.name); },
|
|
83
|
+
formatCoord(d) { return `${d.namespace || ""}/${d.name}`; },
|
|
84
|
+
osvPackageName(d) { return `${d.namespace || ""}/${d.name}`; },
|
|
85
|
+
|
|
86
|
+
async checkRegistry(deps, opts = {}) {
|
|
87
|
+
const { checkComposerRegistryDeps } = require("./composer/registry");
|
|
88
|
+
return checkComposerRegistryDeps(deps, opts);
|
|
89
|
+
},
|
|
90
|
+
resolveEolProduct(d) { return require("../outdated").findEolProduct(d); },
|
|
91
|
+
recipe: require("./recipes").composer,
|
|
92
|
+
nativeScanners: [],
|
|
93
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/codecs/index.js — registre des codecs.
|
|
3
|
+
*
|
|
4
|
+
* getCodec(id) → le codec, ou null
|
|
5
|
+
* allCodecs() → tous les codecs enregistrés, dans l'ordre report stable
|
|
6
|
+
* detectCodecs(dir) → les codecs dont detect() est vrai sur ce répertoire
|
|
7
|
+
*/
|
|
8
|
+
const { assertCodecShape } = require("./codec.interface");
|
|
9
|
+
const maven = require("./maven.codec");
|
|
10
|
+
const npm = require("./npm.codec");
|
|
11
|
+
const yarn = require("./yarn.codec");
|
|
12
|
+
const composer = require("./composer.codec");
|
|
13
|
+
const pypi = require("./pypi.codec");
|
|
14
|
+
const nuget = require("./nuget.codec");
|
|
15
|
+
|
|
16
|
+
// Ordre stable pour le report (maven, npm, yarn, puis les nouveaux écosystèmes).
|
|
17
|
+
const ORDER = ["maven", "npm", "yarn", "nuget", "composer", "pypi"];
|
|
18
|
+
|
|
19
|
+
const REGISTRY = new Map();
|
|
20
|
+
for (const c of [maven, npm, yarn, composer, pypi, nuget]) {
|
|
21
|
+
assertCodecShape(c);
|
|
22
|
+
REGISTRY.set(c.id, c);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getCodec(id) { return REGISTRY.get(id) || null; }
|
|
26
|
+
|
|
27
|
+
function allCodecs() {
|
|
28
|
+
return [...REGISTRY.values()].sort((a, b) => ORDER.indexOf(a.id) - ORDER.indexOf(b.id));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// yarn est détecté via le même arbre JS que npm ; on ne le renvoie pas en
|
|
32
|
+
// doublon de détection (npm.collect ramasse déjà yarn.lock).
|
|
33
|
+
function detectCodecs(dir) {
|
|
34
|
+
return allCodecs().filter(c => c.id !== "yarn" && c.detect(dir));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { getCodec, allCodecs, detectCodecs, ORDER };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/codecs/maven.codec.js — codec Maven.
|
|
3
|
+
*
|
|
4
|
+
* N'INTRODUIT AUCUNE LOGIQUE NOUVELLE : enveloppe la résolution POM/BOM de
|
|
5
|
+
* lib/core.js (parse, parent, merge multi-profils, dependencyManagement, imports
|
|
6
|
+
* scope=import), le collecteur lib/cve-match.js, le walker transitif
|
|
7
|
+
* lib/transitive.js et le scanner natif CVE-index (cvelistV5).
|
|
8
|
+
*/
|
|
9
|
+
const core = require("../core");
|
|
10
|
+
const { collectResolvedDeps, expandWithTransitives, matchDepsAgainstCves } = require("../cve-match");
|
|
11
|
+
const { coordKeyFor } = require("../dep-record");
|
|
12
|
+
|
|
13
|
+
// Scanner natif : CVE-index local cvelistV5. scan(deps,opts) → {matches, meta}.
|
|
14
|
+
const cveIndexScanner = {
|
|
15
|
+
id: "cve-index",
|
|
16
|
+
kind: "cve", // résultats → chapitre CVE (mergés avec OSV)
|
|
17
|
+
async scan(deps, opts = {}) {
|
|
18
|
+
const { ensureCveIndex } = require("../cve-download");
|
|
19
|
+
const idx = await ensureCveIndex({
|
|
20
|
+
force: !!opts.cveRefresh && !opts.offline,
|
|
21
|
+
offline: !!opts.cveOffline || !!opts.offline,
|
|
22
|
+
verbose: opts.verbose,
|
|
23
|
+
});
|
|
24
|
+
return { matches: matchDepsAgainstCves(deps, idx), meta: { cveDataDate: idx?.meta?.builtAt || null } };
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
id: "maven",
|
|
30
|
+
label: "Maven",
|
|
31
|
+
osvEcosystem: "Maven",
|
|
32
|
+
manifestNames: ["pom.xml"],
|
|
33
|
+
|
|
34
|
+
detect(dir) { return core.findPomFiles(dir).length > 0; },
|
|
35
|
+
|
|
36
|
+
// collect enveloppe parse + inheritance + collectResolvedDeps existants.
|
|
37
|
+
// Expose _maven (store/propsByPom/pomFiles) pour que l'orchestrateur garde la
|
|
38
|
+
// phase de réécriture des POM nettoyés (lib/core.rewritePoms).
|
|
39
|
+
async collect(dir, opts = {}) {
|
|
40
|
+
const pomFiles = core.findPomFiles(dir);
|
|
41
|
+
const store = core.newMetadataStore();
|
|
42
|
+
const propsByPom = {};
|
|
43
|
+
for (const pom of pomFiles) {
|
|
44
|
+
try { await core.parsePom(pom, store); } catch { /* logged by orchestrator */ }
|
|
45
|
+
}
|
|
46
|
+
for (const pom of Object.keys(store.byPath)) {
|
|
47
|
+
try { await core.getAllInheritedProps(pom, store, propsByPom); } catch { /* logged */ }
|
|
48
|
+
}
|
|
49
|
+
const deps = collectResolvedDeps(store, propsByPom, { ignoreTest: opts.ignoreTest, deps2Exclude: opts.deps2Exclude });
|
|
50
|
+
return { deps, warnings: [], _maven: { store, propsByPom, pomFiles } };
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
coordKey(d) { return coordKeyFor("maven", d.namespace || d.groupId, d.name || d.artifactId); },
|
|
54
|
+
formatCoord(d) { return `${d.namespace || d.groupId}:${d.name || d.artifactId}`; },
|
|
55
|
+
osvPackageName(d) { return `${d.namespace || d.groupId}:${d.name || d.artifactId}`; },
|
|
56
|
+
|
|
57
|
+
async checkRegistry(deps, opts = {}) {
|
|
58
|
+
const outdated = require("../outdated");
|
|
59
|
+
const out = opts.allLibs ? await outdated.checkOutdatedDeps(deps, opts) : [];
|
|
60
|
+
const deprecated = outdated.checkObsoleteDeps(deps);
|
|
61
|
+
return { outdated: out, deprecated };
|
|
62
|
+
},
|
|
63
|
+
resolveEolProduct(d) { return require("../outdated").findEolProduct(d); },
|
|
64
|
+
|
|
65
|
+
recipe: require("./recipes").maven,
|
|
66
|
+
|
|
67
|
+
nativeScanners: [cveIndexScanner],
|
|
68
|
+
|
|
69
|
+
// Exposé pour l'orchestrateur (étape transitive, --transitive).
|
|
70
|
+
expandTransitives: expandWithTransitives,
|
|
71
|
+
};
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
const path = require("path");
|
|
18
18
|
const fs = require("fs");
|
|
19
|
-
const { parsePackageJson, parsePackageLock, parseYarnLockV1, findJsManifests } = require("./parse");
|
|
19
|
+
const { parsePackageJson, parsePackageLock, parseYarnLockV1, parsePnpmLock, findJsManifests } = require("./parse");
|
|
20
|
+
const { makeDepRecord } = require("../../dep-record");
|
|
20
21
|
|
|
21
22
|
const SCOPE_RANK = { prod: 4, peer: 3, optional: 2, dev: 1, transitive: 0 };
|
|
22
23
|
|
|
@@ -55,30 +56,30 @@ function upsert(out, dep, manifestPath, lockType) {
|
|
|
55
56
|
const ecosystemType = (lockType || "").startsWith("package-lock") ? "npm"
|
|
56
57
|
: (lockType || "").startsWith("yarn") ? "yarn"
|
|
57
58
|
: "npm";
|
|
58
|
-
const record = {
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
const record = makeDepRecord({
|
|
60
|
+
ecosystem: "npm",
|
|
61
|
+
ecosystemType,
|
|
62
|
+
namespace: "",
|
|
63
|
+
name: dep.name,
|
|
61
64
|
version: dep.version || null,
|
|
65
|
+
manifestPath,
|
|
62
66
|
scope: dep.scope || "prod",
|
|
63
67
|
isDev: !!dep.isDev,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
integrity: dep.integrity || null,
|
|
72
|
-
from: dep.from || null,
|
|
73
|
-
};
|
|
68
|
+
});
|
|
69
|
+
// npm-specific extras (not part of the shared depRecord contract)
|
|
70
|
+
record.lockType = lockType;
|
|
71
|
+
record.depth = dep.depth ?? null;
|
|
72
|
+
record.resolved = dep.resolved || null;
|
|
73
|
+
record.integrity = dep.integrity || null;
|
|
74
|
+
record.from = dep.from || null;
|
|
74
75
|
if (!existing) {
|
|
75
76
|
out.set(key, record);
|
|
76
77
|
return;
|
|
77
78
|
}
|
|
78
|
-
// Merge
|
|
79
|
+
// Merge. NB: pomPaths shares the manifestPaths array reference (makeDepRecord),
|
|
80
|
+
// so we push once — pushing to both would duplicate the entry.
|
|
79
81
|
if (!existing.manifestPaths.includes(manifestPath)) {
|
|
80
82
|
existing.manifestPaths.push(manifestPath);
|
|
81
|
-
existing.pomPaths.push(manifestPath);
|
|
82
83
|
}
|
|
83
84
|
// Prefer the resolved (lockfile) version over a range from package.json
|
|
84
85
|
const incoming = record.version;
|
|
@@ -132,27 +133,41 @@ function collectNpmDeps(rootDir, opts = {}) {
|
|
|
132
133
|
const pj = group.packageJson ? safeParse(parsePackageJson, group.packageJson, verbose) : null;
|
|
133
134
|
const pl = group.packageLock ? safeParse(parsePackageLock, group.packageLock, verbose) : null;
|
|
134
135
|
const yl = group.yarnLock ? safeParse(parseYarnLockV1, group.yarnLock, verbose) : null;
|
|
135
|
-
const
|
|
136
|
+
const pn = group.pnpmLock ? safeParse(parsePnpmLock, group.pnpmLock, verbose) : null;
|
|
137
|
+
const hasLock = !!(pl || yl || pn);
|
|
136
138
|
|
|
137
|
-
// package.json without lockfile →
|
|
139
|
+
// package.json without lockfile → best-effort fallback (changed contract):
|
|
140
|
+
// scan pinned exact versions ("1.2.3"), skip ranges ("^1.0.0"), and warn
|
|
141
|
+
// that results are partial. Previously this manifest was skipped entirely.
|
|
138
142
|
if (pj && !hasLock) {
|
|
143
|
+
let pinned = 0, ranges = 0;
|
|
144
|
+
for (const d of pj.deps) {
|
|
145
|
+
if (ignoreTest && (d.scope === "dev" || d.scope === "optional")) continue;
|
|
146
|
+
if (deps2Exclude && deps2Exclude.test(d.name)) continue;
|
|
147
|
+
if (isResolvedVersion(d.version)) {
|
|
148
|
+
upsert(out, { name: d.name, version: d.version, scope: d.scope || "prod", isDev: !!d.isDev }, group.packageJson, "package-json-nolock");
|
|
149
|
+
pinned++;
|
|
150
|
+
} else {
|
|
151
|
+
ranges++;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
139
154
|
warnings.push({
|
|
140
155
|
type: "no-lockfile",
|
|
141
156
|
manifestPath: group.packageJson,
|
|
142
157
|
packageName: pj.packageName || null,
|
|
143
|
-
message: `package.json without lockfile — skipped (run "npm install" or "yarn install"
|
|
158
|
+
message: `package.json without lockfile — best-effort: ${pinned} pinned dep(s) scanned, ${ranges} range(s) skipped (run "npm install" or "yarn install" for full coverage)`,
|
|
144
159
|
});
|
|
145
|
-
if (verbose) console.warn(`⚠️ ${group.packageJson}: no lockfile
|
|
160
|
+
if (verbose) console.warn(`⚠️ ${group.packageJson}: no lockfile — best-effort (${pinned} pinned, ${ranges} ranges skipped)`);
|
|
146
161
|
}
|
|
147
162
|
|
|
148
|
-
// Yarn-Berry lockfile
|
|
149
|
-
if (yl?.
|
|
163
|
+
// Yarn-Berry lockfile that failed to parse → warning (deps still empty).
|
|
164
|
+
if (yl?.parseError) {
|
|
150
165
|
warnings.push({
|
|
151
|
-
type: "
|
|
166
|
+
type: "parse-error",
|
|
152
167
|
manifestPath: group.yarnLock,
|
|
153
|
-
message: `yarn 2+/Berry lockfile
|
|
168
|
+
message: `yarn 2+/Berry lockfile parse failed: ${yl.parseError}`,
|
|
154
169
|
});
|
|
155
|
-
if (verbose) console.warn(`⚠️ ${group.yarnLock}: yarn-berry
|
|
170
|
+
if (verbose) console.warn(`⚠️ ${group.yarnLock}: yarn-berry parse failed — ${yl.parseError}`);
|
|
156
171
|
}
|
|
157
172
|
|
|
158
173
|
// Used only as a source of scope info — never emits deps on its own.
|
|
@@ -171,15 +186,23 @@ function collectNpmDeps(rootDir, opts = {}) {
|
|
|
171
186
|
}
|
|
172
187
|
if (yl) {
|
|
173
188
|
parsedCount++;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
189
|
+
const lockType = yl.lockfileVersion === "berry" ? "yarn-berry" : "yarn-v1";
|
|
190
|
+
for (const d of yl.deps) {
|
|
191
|
+
const explicit = directScopes?.get(d.name);
|
|
192
|
+
const scope = explicit || d.scope || "prod";
|
|
193
|
+
if (ignoreTest && (scope === "dev" || scope === "optional")) continue;
|
|
194
|
+
if (deps2Exclude && deps2Exclude.test(d.name)) continue;
|
|
195
|
+
upsert(out, { ...d, scope }, group.yarnLock, lockType);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (pn) {
|
|
199
|
+
parsedCount++;
|
|
200
|
+
for (const d of pn.deps) {
|
|
201
|
+
const explicit = directScopes?.get(d.name);
|
|
202
|
+
const scope = explicit || d.scope || "prod";
|
|
203
|
+
if (ignoreTest && (scope === "dev" || scope === "optional")) continue;
|
|
204
|
+
if (deps2Exclude && deps2Exclude.test(d.name)) continue;
|
|
205
|
+
upsert(out, { ...d, scope }, group.pnpmLock, "pnpm");
|
|
183
206
|
}
|
|
184
207
|
}
|
|
185
208
|
}
|
|
@@ -237,7 +260,7 @@ function hasJsManifests(rootDir) {
|
|
|
237
260
|
try { entries = fs.readdirSync(cur, { withFileTypes: true }); }
|
|
238
261
|
catch { continue; }
|
|
239
262
|
for (const e of entries) {
|
|
240
|
-
if (e.isFile() && (e.name === "package.json" || e.name === "package-lock.json" || e.name === "yarn.lock")) return true;
|
|
263
|
+
if (e.isFile() && (e.name === "package.json" || e.name === "package-lock.json" || e.name === "yarn.lock" || e.name === "pnpm-lock.yaml")) return true;
|
|
241
264
|
if (e.isDirectory() && !skip.has(e.name)) stack.push(path.join(cur, e.name));
|
|
242
265
|
}
|
|
243
266
|
}
|