fad-checker 1.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/CLAUDE.md +126 -0
- package/README.md +279 -0
- package/completions/fad-check.bash +23 -0
- package/completions/fad-check.zsh +27 -0
- package/data/cpe-coord-map.json +112 -0
- package/data/eol-mapping.json +34 -0
- package/data/known-obsolete.json +142 -0
- package/data/known-public-namespaces.json +79 -0
- package/docs/ARCHITECTURE.md +152 -0
- package/docs/USAGE.md +179 -0
- package/fad-check.js +665 -0
- package/lib/cache-archive.js +107 -0
- package/lib/config.js +87 -0
- package/lib/core.js +317 -0
- package/lib/cpe.js +287 -0
- package/lib/cve-download.js +369 -0
- package/lib/cve-match.js +228 -0
- package/lib/cve-report.js +1455 -0
- package/lib/maven-repo.js +134 -0
- package/lib/maven-version.js +153 -0
- package/lib/npm/collect.js +224 -0
- package/lib/npm/parse.js +291 -0
- package/lib/nvd.js +239 -0
- package/lib/osv.js +298 -0
- package/lib/outdated.js +265 -0
- package/lib/retire.js +211 -0
- package/lib/scan-completeness.js +67 -0
- package/lib/snyk.js +127 -0
- package/lib/transitive.js +410 -0
- package/package.json +35 -0
- package/test/core.test.js +153 -0
- package/test/cpe.test.js +148 -0
- package/test/cve-download.test.js +39 -0
- package/test/cve-match.test.js +108 -0
- package/test/cve-report.test.js +79 -0
- package/test/fixtures/complex-enterprise/api/pom.xml +32 -0
- package/test/fixtures/complex-enterprise/build/pom.xml +46 -0
- package/test/fixtures/complex-enterprise/dao/pom.xml +37 -0
- package/test/fixtures/complex-enterprise/pom.xml +66 -0
- package/test/fixtures/complex-enterprise/web/pom.xml +77 -0
- package/test/fixtures/cve-samples/cve-non-java.json +19 -0
- package/test/fixtures/cve-samples/cve-product-only.json +31 -0
- package/test/fixtures/cve-samples/cve-with-packagename.json +37 -0
- package/test/fixtures/cve-samples/nvd-log4shell.json +40 -0
- package/test/fixtures/cve-samples/nvd-npm-lodash.json +22 -0
- package/test/fixtures/monorepo-mixed/libs/common-bom/pom.xml +26 -0
- package/test/fixtures/monorepo-mixed/packages/cli/package.json +14 -0
- package/test/fixtures/monorepo-mixed/packages/cli/yarn.lock +41 -0
- package/test/fixtures/monorepo-mixed/packages/no-lock/package.json +9 -0
- package/test/fixtures/monorepo-mixed/packages/web-app/package-lock.json +71 -0
- package/test/fixtures/monorepo-mixed/packages/web-app/package.json +17 -0
- package/test/fixtures/monorepo-mixed/pom.xml +29 -0
- package/test/fixtures/monorepo-mixed/services/api/pom.xml +27 -0
- package/test/fixtures/monorepo-mixed/services/worker/pom.xml +28 -0
- package/test/fixtures/private-lib-detection/core/pom.xml +26 -0
- package/test/fixtures/private-lib-detection/plugin/pom.xml +23 -0
- package/test/fixtures/private-lib-detection/pom.xml +35 -0
- package/test/fixtures/simple/app/pom.xml +28 -0
- package/test/fixtures/simple/lib/pom.xml +18 -0
- package/test/fixtures/simple/pom.xml +24 -0
- package/test/maven-repo.test.js +111 -0
- package/test/maven-version.test.js +48 -0
- package/test/monorepo.test.js +132 -0
- package/test/npm.test.js +146 -0
- package/test/outdated.test.js +56 -0
- package/test/snyk.test.js +64 -0
- package/test/transitive.test.js +305 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/cache-archive.js — export / import the entire ~/.fad-check/ directory.
|
|
3
|
+
*
|
|
4
|
+
* Use case:
|
|
5
|
+
* - Move the warmed-up CVE/OSV/NVD/POM caches between machines
|
|
6
|
+
* - Snapshot the index before a scheduled refresh
|
|
7
|
+
* - Share a known-good cache with a teammate
|
|
8
|
+
*
|
|
9
|
+
* Format:
|
|
10
|
+
* .tar.gz — preferred when tar is available (Linux, macOS, Windows 10+)
|
|
11
|
+
* .zip — fallback for Windows-only envs without tar
|
|
12
|
+
*
|
|
13
|
+
* The format is selected from the file extension. Tar uses native `tar`
|
|
14
|
+
* binary (zero new deps).
|
|
15
|
+
*/
|
|
16
|
+
const fs = require("fs");
|
|
17
|
+
const path = require("path");
|
|
18
|
+
const os = require("os");
|
|
19
|
+
const { execFile } = require("child_process");
|
|
20
|
+
const { promisify } = require("util");
|
|
21
|
+
const execFileP = promisify(execFile);
|
|
22
|
+
|
|
23
|
+
const FAD_CACHE_DIR = path.join(os.homedir(), ".fad-check");
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Files inside ~/.fad-check/ that hold secrets and should NOT be shipped by default.
|
|
27
|
+
* Override by passing `includeConfig: true`.
|
|
28
|
+
*/
|
|
29
|
+
const SENSITIVE_FILES = ["config.json"];
|
|
30
|
+
|
|
31
|
+
async function exportCache(destFile, opts = {}) {
|
|
32
|
+
const { verbose, includeConfig } = opts;
|
|
33
|
+
if (!fs.existsSync(FAD_CACHE_DIR)) throw new Error(`no ~/.fad-check/ directory to export`);
|
|
34
|
+
const abs = path.resolve(destFile);
|
|
35
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
36
|
+
|
|
37
|
+
const excludes = includeConfig ? [] : SENSITIVE_FILES.map(f => `${path.basename(FAD_CACHE_DIR)}/${f}`);
|
|
38
|
+
if (excludes.length && verbose) console.log(` excluding (use --include-config to keep): ${excludes.join(", ")}`);
|
|
39
|
+
|
|
40
|
+
const ext = abs.toLowerCase();
|
|
41
|
+
if (ext.endsWith(".tar.gz") || ext.endsWith(".tgz")) {
|
|
42
|
+
const args = ["-czf", abs];
|
|
43
|
+
for (const e of excludes) args.push(`--exclude=${e}`);
|
|
44
|
+
args.push("-C", path.dirname(FAD_CACHE_DIR), path.basename(FAD_CACHE_DIR));
|
|
45
|
+
if (verbose) console.log(`📦 tar ${args.join(" ")}`);
|
|
46
|
+
await execFileP("tar", args, { maxBuffer: 1024 * 1024 * 32 });
|
|
47
|
+
} else if (ext.endsWith(".zip")) {
|
|
48
|
+
if (process.platform === "win32") {
|
|
49
|
+
if (verbose) console.log(`📦 Compress-Archive -Path ${FAD_CACHE_DIR}\\* -DestinationPath ${abs}`);
|
|
50
|
+
// Powershell Compress-Archive doesn't have a clean exclude flag — manual copy
|
|
51
|
+
await execFileP("powershell", ["-NoProfile", "-Command",
|
|
52
|
+
`$src='${FAD_CACHE_DIR}'; $dst='${abs}'; ${includeConfig ? `Compress-Archive -Path "$src\\*" -DestinationPath $dst -Force` : `$tmp=Join-Path $env:TEMP "fad-check-stage-$(Get-Random)"; Copy-Item $src $tmp -Recurse; ${SENSITIVE_FILES.map(f => `Remove-Item -Force -ErrorAction SilentlyContinue (Join-Path $tmp '${f}')`).join("; ")}; Compress-Archive -Path "$tmp\\*" -DestinationPath $dst -Force; Remove-Item $tmp -Recurse -Force`}`]);
|
|
53
|
+
} else {
|
|
54
|
+
const args = ["-r", "-q", abs, path.basename(FAD_CACHE_DIR)];
|
|
55
|
+
for (const e of excludes) { args.push("-x"); args.push(e); }
|
|
56
|
+
if (verbose) console.log(`📦 zip ${args.join(" ")}`);
|
|
57
|
+
await execFileP("zip", args, { cwd: path.dirname(FAD_CACHE_DIR), maxBuffer: 1024 * 1024 * 32 });
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
throw new Error(`unknown archive extension on ${destFile} (expected .tar.gz, .tgz, or .zip)`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const size = fs.statSync(abs).size;
|
|
64
|
+
return { path: abs, size, excluded: excludes };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function importCache(srcFile, opts = {}) {
|
|
68
|
+
const { verbose, force } = opts;
|
|
69
|
+
const abs = path.resolve(srcFile);
|
|
70
|
+
if (!fs.existsSync(abs)) throw new Error(`archive not found: ${abs}`);
|
|
71
|
+
|
|
72
|
+
if (fs.existsSync(FAD_CACHE_DIR) && !force) {
|
|
73
|
+
// Move existing aside as .fad-check.bak-<timestamp>
|
|
74
|
+
const backup = `${FAD_CACHE_DIR}.bak-${Date.now()}`;
|
|
75
|
+
fs.renameSync(FAD_CACHE_DIR, backup);
|
|
76
|
+
if (verbose) console.log(`💾 existing ~/.fad-check/ moved to ${backup}`);
|
|
77
|
+
} else if (force && fs.existsSync(FAD_CACHE_DIR)) {
|
|
78
|
+
fs.rmSync(FAD_CACHE_DIR, { recursive: true, force: true });
|
|
79
|
+
if (verbose) console.log(`🗑 --force: existing ~/.fad-check/ removed`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const parent = path.dirname(FAD_CACHE_DIR);
|
|
83
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
84
|
+
const ext = abs.toLowerCase();
|
|
85
|
+
if (ext.endsWith(".tar.gz") || ext.endsWith(".tgz")) {
|
|
86
|
+
if (verbose) console.log(`📦 tar -xzf ${abs} -C ${parent}`);
|
|
87
|
+
await execFileP("tar", ["-xzf", abs, "-C", parent], { maxBuffer: 1024 * 1024 * 32 });
|
|
88
|
+
} else if (ext.endsWith(".zip")) {
|
|
89
|
+
if (process.platform === "win32") {
|
|
90
|
+
if (verbose) console.log(`📦 Expand-Archive -Path ${abs} -DestinationPath ${parent}`);
|
|
91
|
+
await execFileP("powershell", ["-NoProfile", "-Command",
|
|
92
|
+
`Expand-Archive -Path '${abs}' -DestinationPath '${parent}' -Force`]);
|
|
93
|
+
} else {
|
|
94
|
+
if (verbose) console.log(`📦 unzip ${abs} -d ${parent}`);
|
|
95
|
+
await execFileP("unzip", ["-o", "-q", abs, "-d", parent], { maxBuffer: 1024 * 1024 * 32 });
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
throw new Error(`unknown archive extension on ${srcFile}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!fs.existsSync(FAD_CACHE_DIR)) {
|
|
102
|
+
throw new Error(`import completed but ~/.fad-check/ was not created — was the archive built with fad-check --export-cache?`);
|
|
103
|
+
}
|
|
104
|
+
return { dir: FAD_CACHE_DIR };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { exportCache, importCache, FAD_CACHE_DIR };
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/config.js — persistent user config in ~/.fad-check/config.json
|
|
3
|
+
*
|
|
4
|
+
* Stores credentials and per-user preferences that should survive across runs.
|
|
5
|
+
* Currently: NVD API key (so users don't have to re-export the env var).
|
|
6
|
+
*/
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const os = require("os");
|
|
10
|
+
|
|
11
|
+
const CONFIG_DIR = path.join(os.homedir(), ".fad-check");
|
|
12
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
13
|
+
|
|
14
|
+
function load() {
|
|
15
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8")); }
|
|
16
|
+
catch { return {}; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function save(cfg) {
|
|
20
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
21
|
+
// 0o600 so an API key isn't world-readable
|
|
22
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), { mode: 0o600 });
|
|
23
|
+
try { fs.chmodSync(CONFIG_PATH, 0o600); } catch { /* ignore on platforms without chmod */ }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function set(key, value) {
|
|
27
|
+
const cfg = load();
|
|
28
|
+
if (value == null || value === "") delete cfg[key];
|
|
29
|
+
else cfg[key] = value;
|
|
30
|
+
save(cfg);
|
|
31
|
+
return cfg;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function get(key) {
|
|
35
|
+
return load()[key];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** NVD API key resolution: env var first, then ~/.fad-check/config.json. */
|
|
39
|
+
function getNvdApiKey() {
|
|
40
|
+
return process.env.NVD_API_KEY || get("nvd_api_key") || null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Custom Maven repositories (Nexus, Artifactory, JBoss, …) the user has
|
|
45
|
+
* configured. Returned as an array of { name, url, auth? } where `auth` is
|
|
46
|
+
* pre-encoded "user:pass" (caller wraps as Basic <base64>).
|
|
47
|
+
*
|
|
48
|
+
* Maven Central is intentionally NOT included here — callers append it as
|
|
49
|
+
* the final fallback. That keeps the user's repos in priority order while
|
|
50
|
+
* always ensuring Central works as a safety net.
|
|
51
|
+
*/
|
|
52
|
+
function getMavenRepos() {
|
|
53
|
+
const list = get("maven_repos") || [];
|
|
54
|
+
return Array.isArray(list) ? list : [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function setMavenRepos(list) {
|
|
58
|
+
return set("maven_repos", Array.isArray(list) && list.length ? list : null);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function addMavenRepo(name, url, auth = null) {
|
|
62
|
+
const list = getMavenRepos().filter(r => r.name !== name);
|
|
63
|
+
list.push({ name, url, ...(auth ? { auth } : {}) });
|
|
64
|
+
setMavenRepos(list);
|
|
65
|
+
return list;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function removeMavenRepo(name) {
|
|
69
|
+
const before = getMavenRepos();
|
|
70
|
+
const after = before.filter(r => r.name !== name);
|
|
71
|
+
setMavenRepos(after);
|
|
72
|
+
return before.length !== after.length;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
CONFIG_PATH,
|
|
77
|
+
CONFIG_DIR,
|
|
78
|
+
load,
|
|
79
|
+
save,
|
|
80
|
+
set,
|
|
81
|
+
get,
|
|
82
|
+
getNvdApiKey,
|
|
83
|
+
getMavenRepos,
|
|
84
|
+
setMavenRepos,
|
|
85
|
+
addMavenRepo,
|
|
86
|
+
removeMavenRepo,
|
|
87
|
+
};
|
package/lib/core.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/core.js — pure logic for parsing and cleaning Maven POMs.
|
|
3
|
+
* No CLI, no console formatting. Callers pass options explicitly.
|
|
4
|
+
*/
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { parseStringPromise, Builder } = require("xml2js");
|
|
8
|
+
|
|
9
|
+
const SKIP_DIRS = new Set([
|
|
10
|
+
"target", "node_modules", "bower_components", "jspm_packages",
|
|
11
|
+
".git", ".idea", ".vscode", ".gradle", ".mvn",
|
|
12
|
+
"dist", "build-output", "out", "coverage", ".next", ".nuxt",
|
|
13
|
+
// NOTE: "build" is intentionally NOT skipped — Maven multi-module projects
|
|
14
|
+
// sometimes use a "build/" module to hold a BOM or shared parent.
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const coord = v => (v == null ? null : String(v).trim() || null);
|
|
18
|
+
|
|
19
|
+
function findPomFiles(dir) {
|
|
20
|
+
const out = [];
|
|
21
|
+
const stack = [dir];
|
|
22
|
+
while (stack.length) {
|
|
23
|
+
const cur = stack.pop();
|
|
24
|
+
let entries;
|
|
25
|
+
try { entries = fs.readdirSync(cur, { withFileTypes: true }); }
|
|
26
|
+
catch { continue; }
|
|
27
|
+
for (const e of entries) {
|
|
28
|
+
if (e.isDirectory()) {
|
|
29
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
30
|
+
stack.push(path.join(cur, e.name));
|
|
31
|
+
} else if (e.name === "pom.xml") {
|
|
32
|
+
out.push(path.join(cur, e.name));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function newMetadataStore() {
|
|
40
|
+
return {
|
|
41
|
+
byPath: {},
|
|
42
|
+
byId: {},
|
|
43
|
+
excludedById: {},
|
|
44
|
+
missingById: {},
|
|
45
|
+
anyMissingById: {},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function parsePom(pomPath, allPomMetadata) {
|
|
50
|
+
const xml = fs.readFileSync(pomPath, "utf8");
|
|
51
|
+
// Skip POM templates that use literal `\${...}` placeholders.
|
|
52
|
+
if (/\\\$\{/g.test(xml)) return;
|
|
53
|
+
|
|
54
|
+
let json;
|
|
55
|
+
try { json = await parseStringPromise(xml); }
|
|
56
|
+
catch (e) { throw new Error(`xml parse failed: ${e.message}`); }
|
|
57
|
+
|
|
58
|
+
const project = json?.project || {};
|
|
59
|
+
const parent = project?.parent?.[0];
|
|
60
|
+
const parentInfo = parent ? {
|
|
61
|
+
groupId: coord(parent.groupId?.[0]),
|
|
62
|
+
artifactId: coord(parent.artifactId?.[0]),
|
|
63
|
+
version: coord(parent.version?.[0]),
|
|
64
|
+
relativePath: parent.relativePath?.[0],
|
|
65
|
+
} : null;
|
|
66
|
+
|
|
67
|
+
const groupId = coord(project.groupId?.[0]) || parentInfo?.groupId || null;
|
|
68
|
+
const artifactId = coord(project.artifactId?.[0]);
|
|
69
|
+
const version = coord(project.version?.[0]) || parentInfo?.version || null;
|
|
70
|
+
|
|
71
|
+
let profilesById = null;
|
|
72
|
+
let defaultProfileId = null;
|
|
73
|
+
if (project.profiles?.[0]?.profile) {
|
|
74
|
+
profilesById = {};
|
|
75
|
+
for (const profile of project.profiles[0].profile) {
|
|
76
|
+
if (!profile.id?.[0]) continue;
|
|
77
|
+
const hasContent = profile.properties?.[0] || profile.dependencyManagement?.[0] || profile.dependencies?.[0];
|
|
78
|
+
if (!hasContent) continue;
|
|
79
|
+
profilesById[profile.id[0]] = {
|
|
80
|
+
dependencyManagement: profile.dependencyManagement?.[0],
|
|
81
|
+
dependencies: profile.dependencies?.[0],
|
|
82
|
+
properties: profile.properties?.[0],
|
|
83
|
+
};
|
|
84
|
+
if (profile.activation?.[0]?.activeByDefault?.[0] === "true") {
|
|
85
|
+
defaultProfileId = profile.id[0];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (Object.keys(profilesById).length === 0) profilesById = null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const descr = {
|
|
92
|
+
dependencyManagement: project.dependencyManagement?.[0],
|
|
93
|
+
dependencies: project.dependencies?.[0],
|
|
94
|
+
pomPath,
|
|
95
|
+
properties: (project?.properties?.[0] && typeof project.properties[0] !== "string") ? project.properties[0] : {},
|
|
96
|
+
profilesById,
|
|
97
|
+
defaultProfileId,
|
|
98
|
+
parentInfo,
|
|
99
|
+
groupId,
|
|
100
|
+
artifactId,
|
|
101
|
+
version,
|
|
102
|
+
localVars: {
|
|
103
|
+
"project.groupId": groupId,
|
|
104
|
+
"project.artifactId": artifactId,
|
|
105
|
+
"project.version": version,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Skip indexing by id if we don't have both groupId and artifactId — avoids
|
|
110
|
+
// "undefined:foo" lookup collisions across POMs.
|
|
111
|
+
if (groupId && artifactId) {
|
|
112
|
+
allPomMetadata.byId[`${groupId}:${artifactId}`] = descr;
|
|
113
|
+
if (version) allPomMetadata.byId[`${groupId}:${artifactId}:${version}`] = descr;
|
|
114
|
+
}
|
|
115
|
+
allPomMetadata.byPath[pomPath] = descr;
|
|
116
|
+
return descr;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function resolveParentPath(pomPath, parentInfo, allPomMetadata) {
|
|
120
|
+
const self = allPomMetadata.byPath[pomPath];
|
|
121
|
+
if (!parentInfo) return null;
|
|
122
|
+
|
|
123
|
+
if (parentInfo.relativePath != null && parentInfo.relativePath !== "") {
|
|
124
|
+
const rel = parentInfo.relativePath.trim();
|
|
125
|
+
let resolved = path.resolve(path.dirname(pomPath), rel);
|
|
126
|
+
if (fs.existsSync(resolved)) {
|
|
127
|
+
// Maven appends /pom.xml when relativePath points at a directory.
|
|
128
|
+
try {
|
|
129
|
+
if (fs.statSync(resolved).isDirectory()) resolved = path.join(resolved, "pom.xml");
|
|
130
|
+
} catch { /* ignore */ }
|
|
131
|
+
if (allPomMetadata.byPath[resolved]) {
|
|
132
|
+
self.parentDescr = allPomMetadata.byPath[resolved];
|
|
133
|
+
return resolved;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const keyV = `${parentInfo.groupId}:${parentInfo.artifactId}:${parentInfo.version}`;
|
|
139
|
+
const key = `${parentInfo.groupId}:${parentInfo.artifactId}`;
|
|
140
|
+
if (allPomMetadata.byId[keyV]) {
|
|
141
|
+
self.parentDescr = allPomMetadata.byId[keyV];
|
|
142
|
+
return self.parentDescr.pomPath;
|
|
143
|
+
}
|
|
144
|
+
if (allPomMetadata.byId[key]) {
|
|
145
|
+
self.parentDescr = allPomMetadata.byId[key];
|
|
146
|
+
return self.parentDescr.pomPath;
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function getAllInheritedProps(pomPath, allPomMetadata, cache) {
|
|
152
|
+
if (cache[pomPath]) return cache[pomPath];
|
|
153
|
+
const meta = allPomMetadata.byPath[pomPath];
|
|
154
|
+
if (!meta) return { properties: {}, dependencies: [], dependencyManagement: [] };
|
|
155
|
+
|
|
156
|
+
const { dependencyManagement, dependencies, properties, parentInfo, profilesById } = meta;
|
|
157
|
+
|
|
158
|
+
const merged = {
|
|
159
|
+
properties: { ...(properties || {}) },
|
|
160
|
+
dependencies: [...(dependencies?.dependency || [])],
|
|
161
|
+
dependencyManagement: [...(dependencyManagement?.dependencies?.[0]?.dependency || [])],
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Merge every profile so the scan covers any dep any profile could pull in.
|
|
165
|
+
if (profilesById) {
|
|
166
|
+
const ids = Object.keys(profilesById);
|
|
167
|
+
const ordered = meta.defaultProfileId
|
|
168
|
+
? [meta.defaultProfileId, ...ids.filter(id => id !== meta.defaultProfileId)]
|
|
169
|
+
: ids;
|
|
170
|
+
for (const id of ordered) {
|
|
171
|
+
const prof = profilesById[id];
|
|
172
|
+
if (prof.properties)
|
|
173
|
+
merged.properties = { ...prof.properties, ...merged.properties };
|
|
174
|
+
if (prof.dependencies?.dependency)
|
|
175
|
+
merged.dependencies.push(...prof.dependencies.dependency);
|
|
176
|
+
if (prof.dependencyManagement?.dependencies?.[0]?.dependency)
|
|
177
|
+
merged.dependencyManagement.push(...prof.dependencyManagement.dependencies[0].dependency);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Resolve local BOM imports (scope=import) — pull their managed deps in.
|
|
182
|
+
const toImport = [];
|
|
183
|
+
const doImports = dep => {
|
|
184
|
+
if (dep.scope?.[0] !== "import") return;
|
|
185
|
+
const g = coord(dep.groupId?.[0]);
|
|
186
|
+
const a = coord(dep.artifactId?.[0]);
|
|
187
|
+
const v = coord(dep.version?.[0]);
|
|
188
|
+
if (!g || !a) return;
|
|
189
|
+
const local = (v && allPomMetadata.byId[`${g}:${a}:${v}`]) || allPomMetadata.byId[`${g}:${a}`];
|
|
190
|
+
if (local) toImport.push(local);
|
|
191
|
+
};
|
|
192
|
+
for (const dep of merged.dependencies) doImports(dep);
|
|
193
|
+
for (const dep of merged.dependencyManagement) doImports(dep);
|
|
194
|
+
|
|
195
|
+
for (const pom of toImport) {
|
|
196
|
+
const imported = await getAllInheritedProps(pom.pomPath, allPomMetadata, cache);
|
|
197
|
+
merged.properties = { ...merged.properties, ...imported.properties };
|
|
198
|
+
merged.dependencies.push(...imported.dependencies);
|
|
199
|
+
merged.dependencyManagement.push(...imported.dependencyManagement);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
resolveParentPath(pomPath, parentInfo, allPomMetadata);
|
|
203
|
+
|
|
204
|
+
cache[pomPath] = merged;
|
|
205
|
+
return merged;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const NODES_TO_KEEP = new Set([
|
|
209
|
+
"groupId", "artifactId", "version", "packaging", "modules",
|
|
210
|
+
"properties", "dependencyManagement", "name", "dependencies",
|
|
211
|
+
"modelVersion", "$",
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
async function rewritePoms(pomPath, allPomMetadata, allPropsByPom, opts) {
|
|
215
|
+
const { srcRoot, targetRoot, deps2Exclude, verbose, readOnly } = opts;
|
|
216
|
+
const descr = allPomMetadata.byPath[pomPath];
|
|
217
|
+
if (!descr) return false;
|
|
218
|
+
|
|
219
|
+
const xmlData = fs.readFileSync(pomPath, "utf8").replace("", "");
|
|
220
|
+
const tPom = readOnly ? null : path.join(targetRoot, path.relative(srcRoot, pomPath));
|
|
221
|
+
|
|
222
|
+
let json;
|
|
223
|
+
try { json = await parseStringPromise(xmlData); }
|
|
224
|
+
catch (e) { throw new Error(`xml parse failed: ${e.message}`); }
|
|
225
|
+
|
|
226
|
+
const props = json.project || {};
|
|
227
|
+
const keep = new Set(NODES_TO_KEEP);
|
|
228
|
+
|
|
229
|
+
// Re-resolve parent if first pass missed it.
|
|
230
|
+
if (!descr.parentDescr && descr.parentInfo) {
|
|
231
|
+
const p = descr.parentInfo;
|
|
232
|
+
descr.parentDescr =
|
|
233
|
+
allPomMetadata.byId[`${p.groupId}:${p.artifactId}:${p.version}`] ||
|
|
234
|
+
allPomMetadata.byId[`${p.groupId}:${p.artifactId}`];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!descr.parentDescr && descr.parentInfo) {
|
|
238
|
+
const p = descr.parentInfo;
|
|
239
|
+
const isExcluded = deps2Exclude && deps2Exclude.test(p.groupId || "");
|
|
240
|
+
allPomMetadata.missingById[`${p.groupId}:${p.artifactId}`] = true;
|
|
241
|
+
allPomMetadata.missingById[`${p.groupId}:${p.artifactId}:${p.version}`] = true;
|
|
242
|
+
if (!isExcluded) {
|
|
243
|
+
allPomMetadata.anyMissingById[`${p.groupId}:${p.artifactId}`] = true;
|
|
244
|
+
allPomMetadata.anyMissingById[`${p.groupId}:${p.artifactId}:${p.version}`] = true;
|
|
245
|
+
keep.add("parent");
|
|
246
|
+
if (verbose) console.warn(`⚠️ parent not found locally for ${pomPath} → ${p.groupId}:${p.artifactId}:${p.version} (will be treated as public)`);
|
|
247
|
+
} else if (verbose) {
|
|
248
|
+
console.warn(`⚠️⚠️ excluded parent for ${pomPath} → ${p.groupId}:${p.artifactId}:${p.version} (snyk may fail)`);
|
|
249
|
+
}
|
|
250
|
+
} else if (descr.parentDescr) {
|
|
251
|
+
keep.add("parent");
|
|
252
|
+
const pd = descr.parentDescr;
|
|
253
|
+
props.parent[0].relativePath = [path.relative(path.dirname(pomPath), path.dirname(pd.pomPath))];
|
|
254
|
+
if (pd.version) props.parent[0].version = [pd.version];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (const k of Object.keys(props)) {
|
|
258
|
+
if (!keep.has(k)) delete props[k];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const cleanDeps = list =>
|
|
262
|
+
list?.filter(dep => {
|
|
263
|
+
const g = coord(dep.groupId?.[0]);
|
|
264
|
+
const a = coord(dep.artifactId?.[0]);
|
|
265
|
+
const v = coord(dep.version?.[0]);
|
|
266
|
+
if (!g || !a) return false;
|
|
267
|
+
if (deps2Exclude) {
|
|
268
|
+
// Versionless deps inside dependencyManagement-merged result are
|
|
269
|
+
// often resolvable via a managed parent — keep them unless we
|
|
270
|
+
// can prove they're excluded.
|
|
271
|
+
if (deps2Exclude.test(g)) {
|
|
272
|
+
const local = (v && allPomMetadata.byId[`${g}:${a}:${v}`]) || allPomMetadata.byId[`${g}:${a}`];
|
|
273
|
+
if (local && dep.scope?.[0] === "import") {
|
|
274
|
+
dep.systemPath = [path.resolve(local.pomPath)];
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
if (!local && dep.scope?.[0] === "import" && verbose) {
|
|
278
|
+
console.warn(`⚠️ excluded import-scope BOM in ${pomPath}: ${g}:${a}:${v}`);
|
|
279
|
+
}
|
|
280
|
+
allPomMetadata.excludedById[`${g}:${a}`] = true;
|
|
281
|
+
if (v) allPomMetadata.excludedById[`${g}:${a}:${v}`] = true;
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
allPomMetadata.anyMissingById[`${g}:${a}`] = true;
|
|
285
|
+
if (v) allPomMetadata.anyMissingById[`${g}:${a}:${v}`] = true;
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
allPomMetadata.anyMissingById[`${g}:${a}`] = true;
|
|
289
|
+
if (v) allPomMetadata.anyMissingById[`${g}:${a}:${v}`] = true;
|
|
290
|
+
return true;
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const propsForPom = allPropsByPom[pomPath] || { properties: {}, dependencies: [], dependencyManagement: [] };
|
|
294
|
+
|
|
295
|
+
if (props.dependencies?.[0]?.dependency)
|
|
296
|
+
props.dependencies[0].dependency = cleanDeps(propsForPom.dependencies);
|
|
297
|
+
if (props.dependencyManagement?.[0]?.dependencies?.[0]?.dependency)
|
|
298
|
+
props.dependencyManagement[0].dependencies[0].dependency = cleanDeps(propsForPom.dependencyManagement);
|
|
299
|
+
props.properties = [propsForPom.properties];
|
|
300
|
+
|
|
301
|
+
if (!readOnly) {
|
|
302
|
+
await fs.promises.mkdir(path.dirname(tPom), { recursive: true });
|
|
303
|
+
const builder = new Builder();
|
|
304
|
+
fs.writeFileSync(tPom, builder.buildObject(json));
|
|
305
|
+
}
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
module.exports = {
|
|
310
|
+
coord,
|
|
311
|
+
findPomFiles,
|
|
312
|
+
newMetadataStore,
|
|
313
|
+
parsePom,
|
|
314
|
+
resolveParentPath,
|
|
315
|
+
getAllInheritedProps,
|
|
316
|
+
rewritePoms,
|
|
317
|
+
};
|