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
package/lib/retire.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/retire.js — wrap retire.js (the CLI) to find vulnerable
|
|
3
|
+
* vendored JavaScript libraries living in the source tree as
|
|
4
|
+
* unmanaged .js / .min.js files (no package-lock to back them).
|
|
5
|
+
*
|
|
6
|
+
* retire ships its own signature DB updated weekly; we just shell
|
|
7
|
+
* out to it and normalise the output to fad-check match shape so the
|
|
8
|
+
* report can render it like any other CVE source.
|
|
9
|
+
*
|
|
10
|
+
* Cache: ~/.fad-check/retire-cache/<md5(src)>.json, 24 h TTL.
|
|
11
|
+
*
|
|
12
|
+
* The CLI is expected at node_modules/.bin/retire (declared in
|
|
13
|
+
* package.json deps). When bundled with bun, we also try `retire`
|
|
14
|
+
* on PATH as a fallback.
|
|
15
|
+
*/
|
|
16
|
+
const fs = require("fs");
|
|
17
|
+
const path = require("path");
|
|
18
|
+
const os = require("os");
|
|
19
|
+
const crypto = require("crypto");
|
|
20
|
+
const { execFile } = require("child_process");
|
|
21
|
+
const { promisify } = require("util");
|
|
22
|
+
const execFileP = promisify(execFile);
|
|
23
|
+
|
|
24
|
+
const RETIRE_CACHE_DIR = path.join(os.homedir(), ".fad-check", "retire-cache");
|
|
25
|
+
const RETIRE_CACHE_TTL_MS = 24 * 3600 * 1000;
|
|
26
|
+
|
|
27
|
+
function cacheKey(srcDir) {
|
|
28
|
+
return crypto.createHash("md5").update(path.resolve(srcDir)).digest("hex") + ".json";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readCache(srcDir) {
|
|
32
|
+
const p = path.join(RETIRE_CACHE_DIR, cacheKey(srcDir));
|
|
33
|
+
if (!fs.existsSync(p)) return null;
|
|
34
|
+
try {
|
|
35
|
+
const data = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
36
|
+
if (Date.now() - data._fetchedAt < RETIRE_CACHE_TTL_MS) return data.body;
|
|
37
|
+
} catch { /* ignore */ }
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function writeCache(srcDir, body) {
|
|
42
|
+
fs.mkdirSync(RETIRE_CACHE_DIR, { recursive: true });
|
|
43
|
+
fs.writeFileSync(path.join(RETIRE_CACHE_DIR, cacheKey(srcDir)),
|
|
44
|
+
JSON.stringify({ _fetchedAt: Date.now(), body }));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function findRetireBin() {
|
|
48
|
+
const local = path.join(__dirname, "..", "node_modules", ".bin", "retire");
|
|
49
|
+
if (fs.existsSync(local)) return local;
|
|
50
|
+
return "retire"; // fall back to PATH
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Run retire.js against `srcDir`. Returns the parsed JSON output (an array
|
|
55
|
+
* of file-level findings) or null if retire is unavailable.
|
|
56
|
+
*
|
|
57
|
+
* `--outputformat json` is the structured form; `--ignore` takes a list of
|
|
58
|
+
* dirs to skip; we add the standard suspects to keep runtime bounded.
|
|
59
|
+
*/
|
|
60
|
+
async function runRetire(srcDir, opts = {}) {
|
|
61
|
+
const { verbose, force, offline } = opts;
|
|
62
|
+
if (offline) {
|
|
63
|
+
const cached = readCache(srcDir);
|
|
64
|
+
if (cached) return cached;
|
|
65
|
+
if (verbose) console.warn("retire: --offline and no cache — skipped");
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (!force) {
|
|
69
|
+
const cached = readCache(srcDir);
|
|
70
|
+
if (cached) {
|
|
71
|
+
if (verbose) console.log("retire: using cached results (<24h)");
|
|
72
|
+
return cached;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const bin = findRetireBin();
|
|
77
|
+
const ignoredDirs = [
|
|
78
|
+
"node_modules", "bower_components", "jspm_packages",
|
|
79
|
+
".git", ".idea", ".vscode", ".gradle", ".mvn",
|
|
80
|
+
"target", "dist", "build", "build-output", "out", "coverage", ".next", ".nuxt",
|
|
81
|
+
].join(",");
|
|
82
|
+
|
|
83
|
+
// retire.js refuses to write to /dev/stdout, so we use a real temp file
|
|
84
|
+
// and read it back. Falls back to stdout if --outputpath is rejected.
|
|
85
|
+
const tmpOut = path.join(os.tmpdir(), `fad-check-retire-${process.pid}-${Date.now()}.json`);
|
|
86
|
+
const args = [
|
|
87
|
+
"--outputformat", "json",
|
|
88
|
+
"--outputpath", tmpOut,
|
|
89
|
+
"--jspath", srcDir,
|
|
90
|
+
"--ignore", ignoredDirs,
|
|
91
|
+
];
|
|
92
|
+
if (verbose) console.log(`retire: scanning ${srcDir}…`);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
// retire.js exits with code 13 when it finds vulnerabilities — that's
|
|
96
|
+
// expected. Catch and ignore the non-zero exit; the JSON file is still
|
|
97
|
+
// produced.
|
|
98
|
+
await execFileP(bin, args, { maxBuffer: 1024 * 1024 * 64 });
|
|
99
|
+
} catch (err) {
|
|
100
|
+
// exit code 13 (or anything where the output file exists) is OK
|
|
101
|
+
if (!fs.existsSync(tmpOut)) {
|
|
102
|
+
if (verbose) console.warn(`retire: failed to run — ${err.message}`);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let parsed;
|
|
108
|
+
try {
|
|
109
|
+
const body = fs.readFileSync(tmpOut, "utf8");
|
|
110
|
+
parsed = JSON.parse(body);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
if (verbose) console.warn(`retire: could not parse output — ${err.message}`);
|
|
113
|
+
return null;
|
|
114
|
+
} finally {
|
|
115
|
+
try { fs.unlinkSync(tmpOut); } catch { /* best effort */ }
|
|
116
|
+
}
|
|
117
|
+
writeCache(srcDir, parsed);
|
|
118
|
+
return parsed;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Normalise a retire.js result tree to fad-check match shape.
|
|
123
|
+
*
|
|
124
|
+
* retire output (jsonsimple-ish):
|
|
125
|
+
* {
|
|
126
|
+
* data: [
|
|
127
|
+
* {
|
|
128
|
+
* file: "/abs/path/jquery-1.4.4.min.js",
|
|
129
|
+
* results: [
|
|
130
|
+
* {
|
|
131
|
+
* component: "jquery",
|
|
132
|
+
* version: "1.4.4",
|
|
133
|
+
* vulnerabilities: [
|
|
134
|
+
* {
|
|
135
|
+
* severity: "high",
|
|
136
|
+
* identifiers: { CVE: ["CVE-…"], summary: "…", issue?: "…" },
|
|
137
|
+
* info: ["https://…"],
|
|
138
|
+
* below?: "1.6.3",
|
|
139
|
+
* atOrAbove?: "1.4.0",
|
|
140
|
+
* },
|
|
141
|
+
* ],
|
|
142
|
+
* },
|
|
143
|
+
* ],
|
|
144
|
+
* },
|
|
145
|
+
* ],
|
|
146
|
+
* }
|
|
147
|
+
*/
|
|
148
|
+
function normaliseRetireResults(raw, srcDir) {
|
|
149
|
+
const out = [];
|
|
150
|
+
if (!raw) return out;
|
|
151
|
+
const files = Array.isArray(raw) ? raw : (raw.data || []);
|
|
152
|
+
for (const f of files) {
|
|
153
|
+
const file = f.file;
|
|
154
|
+
const relFile = srcDir && file?.startsWith(srcDir) ? path.relative(srcDir, file) : file;
|
|
155
|
+
for (const res of f.results || []) {
|
|
156
|
+
const component = res.component;
|
|
157
|
+
const version = res.version;
|
|
158
|
+
for (const v of res.vulnerabilities || []) {
|
|
159
|
+
const ids = v.identifiers || {};
|
|
160
|
+
const cveIds = Array.isArray(ids.CVE) && ids.CVE.length ? ids.CVE : [ids.issue || ids.summary || `RETIRE-${component}-${version}`];
|
|
161
|
+
const severity = (v.severity || "medium").toUpperCase();
|
|
162
|
+
const description = ids.summary || v.info?.[0] || "";
|
|
163
|
+
const refs = (v.info || []).map(u => ({ type: "WEB", url: u }));
|
|
164
|
+
for (const cveId of cveIds) {
|
|
165
|
+
out.push({
|
|
166
|
+
dep: {
|
|
167
|
+
groupId: "",
|
|
168
|
+
artifactId: component,
|
|
169
|
+
version,
|
|
170
|
+
scope: "vendored",
|
|
171
|
+
ecosystem: "npm",
|
|
172
|
+
ecosystemType: "retire",
|
|
173
|
+
pomPaths: file ? [file] : [],
|
|
174
|
+
manifestPaths: file ? [file] : [],
|
|
175
|
+
vendoredFile: relFile,
|
|
176
|
+
},
|
|
177
|
+
cve: {
|
|
178
|
+
id: cveId,
|
|
179
|
+
severity,
|
|
180
|
+
score: null,
|
|
181
|
+
description,
|
|
182
|
+
fixVersion: v.below || null,
|
|
183
|
+
osvRefs: refs,
|
|
184
|
+
},
|
|
185
|
+
source: "retire",
|
|
186
|
+
confidence: "exact",
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Public entry point. Returns an array of match objects or [] if retire
|
|
197
|
+
* couldn't run or found nothing.
|
|
198
|
+
*/
|
|
199
|
+
async function scanWithRetire(srcDir, opts = {}) {
|
|
200
|
+
const raw = await runRetire(srcDir, opts);
|
|
201
|
+
if (!raw) return [];
|
|
202
|
+
return normaliseRetireResults(raw, srcDir);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = {
|
|
206
|
+
scanWithRetire,
|
|
207
|
+
runRetire,
|
|
208
|
+
normaliseRetireResults,
|
|
209
|
+
findRetireBin,
|
|
210
|
+
RETIRE_CACHE_DIR,
|
|
211
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/scan-completeness.js — produce warnings telling the user when fad-check
|
|
3
|
+
* has gone as far as it can without running the real build tool.
|
|
4
|
+
*
|
|
5
|
+
* The two big "you need a real scan" cases:
|
|
6
|
+
* 1. BOMs (Maven `<scope>import</scope>`): version pins live in another
|
|
7
|
+
* POM that may itself be transitive. We follow LOCAL BOMs in core.js,
|
|
8
|
+
* but external BOMs (Spring Boot, Quarkus, JBoss…) bring in a managed
|
|
9
|
+
* version table that we can't enumerate without resolving them. The
|
|
10
|
+
* resolved deps map will still list deps with unresolved `${prop}` or
|
|
11
|
+
* missing versions in this case.
|
|
12
|
+
* 2. Unresolved versions: any dep whose `version` is null or still
|
|
13
|
+
* contains a `${…}` placeholder — we can't query OSV/Maven Central
|
|
14
|
+
* for these, so they silently fall out of the scan.
|
|
15
|
+
*
|
|
16
|
+
* For npm/yarn, lockfiles already pin everything, so we only flag the
|
|
17
|
+
* already-handled "no lockfile" case from lib/npm/collect.js elsewhere.
|
|
18
|
+
*
|
|
19
|
+
* Output: an array of warning objects matching the same shape as
|
|
20
|
+
* lib/npm/collect.js warnings ({ type, manifestPath?, message, ... }).
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Scan the resolved deps map and return warnings.
|
|
24
|
+
*
|
|
25
|
+
* opts:
|
|
26
|
+
* ranSnyk — true if --snyk was run (suppresses the "run snyk" hint)
|
|
27
|
+
* ranTransitive — true if --transitive was on (changes wording slightly)
|
|
28
|
+
*/
|
|
29
|
+
function detectScanCompletenessWarnings(resolvedDeps, opts = {}) {
|
|
30
|
+
const warnings = [];
|
|
31
|
+
if (!resolvedDeps || typeof resolvedDeps.values !== "function") return warnings;
|
|
32
|
+
|
|
33
|
+
// Deps without a concrete version are the actionable signal — these
|
|
34
|
+
// silently fall out of the CVE / OSV / outdated scans. Their root cause
|
|
35
|
+
// is almost always an external BOM (spring-boot-dependencies, …) or a
|
|
36
|
+
// property defined outside the source tree. We report the deps, not the
|
|
37
|
+
// BOMs themselves: a present-and-correct BOM is not a problem.
|
|
38
|
+
const unresolved = [];
|
|
39
|
+
for (const d of resolvedDeps.values()) {
|
|
40
|
+
if (d.ecosystem === "npm") continue; // lockfile already pins everything
|
|
41
|
+
if (d.scope === "import") continue; // BOM-pointer entries themselves: not a dep
|
|
42
|
+
if (!d.version) unresolved.push({ ...d, reason: "no-version" });
|
|
43
|
+
else if (/\$\{[^}]+\}/.test(String(d.version))) unresolved.push({ ...d, reason: "unresolved-property" });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (unresolved.length) {
|
|
47
|
+
// Dedupe by g:a (same coord may appear in several modules)
|
|
48
|
+
const seenKeys = new Set();
|
|
49
|
+
const items = [];
|
|
50
|
+
for (const d of unresolved) {
|
|
51
|
+
const k = `${d.groupId}:${d.artifactId}`;
|
|
52
|
+
if (seenKeys.has(k)) continue;
|
|
53
|
+
seenKeys.add(k);
|
|
54
|
+
items.push(`${k}${d.version ? " (" + d.version + ")" : " (no version resolved)"}`);
|
|
55
|
+
}
|
|
56
|
+
warnings.push({
|
|
57
|
+
type: "unresolved-versions",
|
|
58
|
+
count: items.length,
|
|
59
|
+
items,
|
|
60
|
+
message: `${items.length} Maven dep(s) without a concrete version — silently skipped from CVE/OSV/outdated scans. Their versions are likely pinned by an external BOM or a property defined outside the source tree. Run "mvn dependency:tree" against the source (or "snyk test --all-projects" against the cleaned POMs${opts.ranSnyk ? " — already done with --snyk" : ", or re-run fad-check with --snyk"}) to resolve them and complete the scan.`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return warnings;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { detectScanCompletenessWarnings };
|
package/lib/snyk.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/snyk.js — optional Snyk integration.
|
|
3
|
+
*
|
|
4
|
+
* Runs `snyk test --all-projects --json` on the cleaned POM directory and
|
|
5
|
+
* normalises the output to fad-check's CVE match shape so the report can
|
|
6
|
+
* merge findings from both engines.
|
|
7
|
+
*/
|
|
8
|
+
const { execFile } = require("child_process");
|
|
9
|
+
const { promisify } = require("util");
|
|
10
|
+
const execFileP = promisify(execFile);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Run `snyk test --all-projects --json` in outputDir.
|
|
14
|
+
* Snyk exits with code 1 when vulnerabilities are found — that's not an
|
|
15
|
+
* error for us, so we accept exit codes 0 and 1.
|
|
16
|
+
*/
|
|
17
|
+
async function runSnykTest(outputDir, opts = {}) {
|
|
18
|
+
const { verbose, timeoutMs = 10 * 60 * 1000 } = opts;
|
|
19
|
+
if (!outputDir) throw new Error("runSnykTest: outputDir required");
|
|
20
|
+
try {
|
|
21
|
+
const { stdout } = await execFileP("snyk", ["test", "--all-projects", "--json"], {
|
|
22
|
+
cwd: outputDir,
|
|
23
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
24
|
+
timeout: timeoutMs,
|
|
25
|
+
});
|
|
26
|
+
return parseSnykStdout(stdout);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
// snyk exits 1 when vulns are found — stdout still contains the JSON
|
|
29
|
+
if (err.stdout) return parseSnykStdout(err.stdout);
|
|
30
|
+
if (err.code === "ENOENT") throw new Error("snyk CLI not found on PATH — install snyk separately");
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseSnykStdout(stdout) {
|
|
36
|
+
if (!stdout) return [];
|
|
37
|
+
const str = String(stdout).trim();
|
|
38
|
+
if (!str) return [];
|
|
39
|
+
// --all-projects can produce either a JSON array of project results
|
|
40
|
+
// or a single object. Normalise to array.
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(str);
|
|
43
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
44
|
+
} catch (_) {
|
|
45
|
+
// Some snyk versions emit one JSON object per line.
|
|
46
|
+
const lines = str.split(/\n+/).filter(Boolean);
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const l of lines) {
|
|
49
|
+
try { out.push(JSON.parse(l)); } catch { /* skip */ }
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Normalise snyk JSON to fad-check match objects:
|
|
57
|
+
* [{ dep: {groupId, artifactId, version}, cve: {id, severity, score, ...}, source: 'snyk' }]
|
|
58
|
+
*/
|
|
59
|
+
function parseSnykResults(snykProjectsJson) {
|
|
60
|
+
const out = [];
|
|
61
|
+
const projects = Array.isArray(snykProjectsJson) ? snykProjectsJson : [snykProjectsJson];
|
|
62
|
+
for (const proj of projects) {
|
|
63
|
+
const vulns = proj?.vulnerabilities || [];
|
|
64
|
+
for (const v of vulns) {
|
|
65
|
+
const pkg = v.packageName || v.moduleName || "";
|
|
66
|
+
const [groupId, artifactId] = pkg.includes(":") ? pkg.split(":") : [null, pkg];
|
|
67
|
+
const cveIds = v.identifiers?.CVE || [];
|
|
68
|
+
const cveId = cveIds[0] || v.id;
|
|
69
|
+
const fixVersions = Array.isArray(v.fixedIn) ? v.fixedIn : [];
|
|
70
|
+
out.push({
|
|
71
|
+
dep: {
|
|
72
|
+
groupId: groupId || "",
|
|
73
|
+
artifactId: artifactId || "",
|
|
74
|
+
version: v.version || "",
|
|
75
|
+
scope: "compile",
|
|
76
|
+
pomPaths: [],
|
|
77
|
+
},
|
|
78
|
+
cve: {
|
|
79
|
+
id: cveId,
|
|
80
|
+
severity: (v.severity || "UNKNOWN").toUpperCase(),
|
|
81
|
+
score: typeof v.cvssScore === "number" ? v.cvssScore : null,
|
|
82
|
+
description: v.title || v.description || "",
|
|
83
|
+
fixVersion: fixVersions[0] || null,
|
|
84
|
+
},
|
|
85
|
+
source: "snyk",
|
|
86
|
+
confidence: "exact",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Merge fad-check and Snyk matches, deduping by (groupId:artifactId, cve.id).
|
|
95
|
+
* When a finding exists in both, fad-check's row is kept but tagged source='both'.
|
|
96
|
+
*/
|
|
97
|
+
function mergeWithFadResults(fadMatches, snykMatches) {
|
|
98
|
+
const byKey = new Map();
|
|
99
|
+
const k = m => `${m.dep.groupId}:${m.dep.artifactId}|${m.cve.id}`;
|
|
100
|
+
for (const m of fadMatches || []) byKey.set(k(m), { ...m, source: "fad" });
|
|
101
|
+
for (const s of snykMatches || []) {
|
|
102
|
+
const key = k(s);
|
|
103
|
+
if (byKey.has(key)) {
|
|
104
|
+
const existing = byKey.get(key);
|
|
105
|
+
byKey.set(key, { ...existing, source: "both" });
|
|
106
|
+
} else {
|
|
107
|
+
byKey.set(key, s);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const merged = [...byKey.values()];
|
|
111
|
+
// Re-sort by severity (Snyk additions throw off ordering)
|
|
112
|
+
const rank = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1, NONE: 0, UNKNOWN: 0 };
|
|
113
|
+
merged.sort((a, b) => {
|
|
114
|
+
const sa = rank[(a.cve.severity || "UNKNOWN").toUpperCase()] || 0;
|
|
115
|
+
const sb = rank[(b.cve.severity || "UNKNOWN").toUpperCase()] || 0;
|
|
116
|
+
if (sb !== sa) return sb - sa;
|
|
117
|
+
return (a.cve.id || "").localeCompare(b.cve.id || "");
|
|
118
|
+
});
|
|
119
|
+
return merged;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
runSnykTest,
|
|
124
|
+
parseSnykResults,
|
|
125
|
+
parseSnykStdout,
|
|
126
|
+
mergeWithFadResults,
|
|
127
|
+
};
|