fad-checker 2.0.0 → 2.1.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 +65 -0
- package/README.md +147 -25
- package/completions/fad-checker.bash +1 -1
- package/completions/fad-checker.zsh +14 -1
- package/data/eol-mapping.json +5 -5
- package/data/license-policy.json +97 -0
- package/fad-checker-report/cve-report.doc +630 -0
- package/fad-checker-report/cve-report.html +748 -0
- package/fad-checker.js +556 -245
- package/lib/cache-archive.js +3 -0
- package/lib/codecs/codec.interface.js +3 -0
- package/lib/codecs/composer/parse.js +3 -0
- package/lib/codecs/composer/registry.js +13 -5
- package/lib/codecs/composer.codec.js +3 -0
- package/lib/codecs/go/parse.js +65 -0
- package/lib/codecs/go/registry.js +74 -0
- package/lib/codecs/go.codec.js +76 -0
- package/lib/codecs/index.js +7 -2
- package/lib/codecs/maven/jar-scan.js +199 -0
- package/lib/codecs/maven.codec.js +17 -1
- package/lib/codecs/npm/collect.js +37 -17
- package/lib/codecs/npm/parse.js +117 -10
- package/lib/codecs/npm/registry.js +14 -5
- package/lib/codecs/npm.codec.js +4 -1
- package/lib/codecs/nuget/parse.js +3 -0
- package/lib/codecs/nuget/registry.js +11 -4
- package/lib/codecs/nuget.codec.js +7 -2
- package/lib/codecs/pypi/parse.js +111 -14
- package/lib/codecs/pypi/registry.js +24 -5
- package/lib/codecs/pypi.codec.js +30 -6
- package/lib/codecs/recipes.js +28 -1
- package/lib/codecs/ruby/parse.js +42 -0
- package/lib/codecs/ruby/registry.js +76 -0
- package/lib/codecs/ruby.codec.js +66 -0
- package/lib/codecs/select.js +3 -0
- package/lib/codecs/yarn.codec.js +3 -0
- package/lib/config.js +3 -0
- package/lib/core.js +38 -1
- package/lib/cpe.js +30 -5
- package/lib/csaf-export.js +159 -0
- package/lib/cve-download.js +3 -0
- package/lib/cve-match.js +27 -6
- package/lib/cve-report.js +157 -28
- package/lib/dep-record.js +15 -2
- package/lib/deps-descriptor.js +113 -0
- package/lib/epss.js +115 -0
- package/lib/gate.js +45 -0
- package/lib/json-export.js +110 -0
- package/lib/kev.js +88 -0
- package/lib/license-policy.js +110 -0
- package/lib/maven-license.js +52 -0
- package/lib/maven-repo.js +3 -0
- package/lib/maven-version.js +8 -3
- package/lib/nvd.js +17 -6
- package/lib/osv.js +75 -23
- package/lib/outdated.js +10 -2
- package/lib/priority.js +90 -0
- package/lib/purl.js +77 -0
- package/lib/retire.js +80 -13
- package/lib/sarif-export.js +134 -0
- package/lib/sbom-export.js +153 -0
- package/lib/scan-completeness.js +3 -0
- package/lib/snyk.js +3 -0
- package/lib/suppress.js +113 -0
- package/lib/transitive.js +6 -3
- package/lib/ui.js +90 -0
- package/package.json +49 -2
- package/CLAUDE.md +0 -147
- package/docs/ARCHITECTURE.md +0 -182
- package/docs/USAGE.md +0 -187
- package/docs/superpowers/plans/2026-05-29-codec-composer.md +0 -556
- package/docs/superpowers/plans/2026-05-29-codec-foundation.md +0 -851
- package/docs/superpowers/plans/2026-05-29-codec-nuget.md +0 -432
- package/docs/superpowers/plans/2026-05-29-codec-pypi.md +0 -450
- package/docs/superpowers/specs/2026-05-29-codecs-multi-ecosystem-design.md +0 -251
- package/test/cli-ecosystem.test.js +0 -30
- package/test/codec-edge-cases.test.js +0 -131
- package/test/codec-integration.test.js +0 -119
- package/test/codecs.test.js +0 -92
- package/test/composer.test.js +0 -71
- package/test/core.test.js +0 -153
- package/test/cpe.test.js +0 -166
- package/test/cve-download.test.js +0 -39
- package/test/cve-match.test.js +0 -217
- package/test/cve-report.test.js +0 -180
- package/test/dep-record.test.js +0 -31
- package/test/fixtures/complex-enterprise/api/pom.xml +0 -32
- package/test/fixtures/complex-enterprise/build/pom.xml +0 -46
- package/test/fixtures/complex-enterprise/dao/pom.xml +0 -37
- package/test/fixtures/complex-enterprise/pom.xml +0 -66
- package/test/fixtures/complex-enterprise/web/pom.xml +0 -77
- package/test/fixtures/csharp-config/packages.config +0 -4
- package/test/fixtures/csharp-csproj/Directory.Packages.props +0 -5
- package/test/fixtures/csharp-csproj/app.csproj +0 -7
- package/test/fixtures/csharp-lock/packages.lock.json +0 -6
- package/test/fixtures/cve-samples/cve-non-java.json +0 -19
- package/test/fixtures/cve-samples/cve-product-only.json +0 -31
- package/test/fixtures/cve-samples/cve-with-packagename.json +0 -37
- package/test/fixtures/cve-samples/nvd-log4shell.json +0 -40
- package/test/fixtures/cve-samples/nvd-npm-lodash.json +0 -22
- package/test/fixtures/monorepo-mixed/libs/common-bom/pom.xml +0 -26
- package/test/fixtures/monorepo-mixed/packages/cli/package.json +0 -14
- package/test/fixtures/monorepo-mixed/packages/cli/yarn.lock +0 -41
- package/test/fixtures/monorepo-mixed/packages/no-lock/package.json +0 -10
- package/test/fixtures/monorepo-mixed/packages/web-app/package-lock.json +0 -71
- package/test/fixtures/monorepo-mixed/packages/web-app/package.json +0 -17
- package/test/fixtures/monorepo-mixed/pom.xml +0 -29
- package/test/fixtures/monorepo-mixed/services/api/pom.xml +0 -27
- package/test/fixtures/monorepo-mixed/services/worker/pom.xml +0 -28
- package/test/fixtures/php-app/composer.json +0 -5
- package/test/fixtures/php-app/composer.lock +0 -10
- package/test/fixtures/polyglot/cs/packages.lock.json +0 -1
- package/test/fixtures/polyglot/js/package-lock.json +0 -1
- package/test/fixtures/polyglot/js/package.json +0 -1
- package/test/fixtures/polyglot/mvn/pom.xml +0 -7
- package/test/fixtures/polyglot/php/composer.lock +0 -1
- package/test/fixtures/polyglot/py/poetry.lock +0 -3
- package/test/fixtures/private-lib-detection/core/pom.xml +0 -26
- package/test/fixtures/private-lib-detection/plugin/pom.xml +0 -23
- package/test/fixtures/private-lib-detection/pom.xml +0 -35
- package/test/fixtures/python-pipenv/Pipfile.lock +0 -1
- package/test/fixtures/python-poetry/poetry.lock +0 -7
- package/test/fixtures/python-reqs/requirements.txt +0 -5
- package/test/fixtures/python-uv/uv.lock +0 -3
- package/test/fixtures/simple/app/pom.xml +0 -28
- package/test/fixtures/simple/lib/pom.xml +0 -18
- package/test/fixtures/simple/pom.xml +0 -24
- package/test/maven-repo.test.js +0 -111
- package/test/maven-version.test.js +0 -57
- package/test/monorepo.test.js +0 -132
- package/test/npm-registry.test.js +0 -64
- package/test/npm.test.js +0 -150
- package/test/nuget.test.js +0 -66
- package/test/osv.test.js +0 -62
- package/test/outdated.test.js +0 -101
- package/test/pypi.test.js +0 -72
- package/test/snyk.test.js +0 -64
- package/test/transitive.test.js +0 -305
- package/test/webjar.test.js +0 -33
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/sarif-export.js — emit a SARIF 2.1.0 log from CVE matches.
|
|
3
|
+
*
|
|
4
|
+
* SARIF is the integration standard for GitHub Code Scanning and GitLab: any
|
|
5
|
+
* tool that emits it shows up in their dashboards with no custom glue. One
|
|
6
|
+
* `reportingDescriptor` (rule) per unique CVE, one `result` per (CVE, dep)
|
|
7
|
+
* match, located at the manifest that declares the dep.
|
|
8
|
+
*
|
|
9
|
+
* buildSarif is pure; writeSarif writes the JSON to disk.
|
|
10
|
+
*
|
|
11
|
+
* @author: N.BRAUN
|
|
12
|
+
* @email: pp9ping@gmail.com
|
|
13
|
+
*/
|
|
14
|
+
const fs = require("fs");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
const { purlFor } = require("./purl");
|
|
17
|
+
|
|
18
|
+
// CVSS/severity → SARIF result level.
|
|
19
|
+
function sarifLevel(sev) {
|
|
20
|
+
switch ((sev || "").toUpperCase()) {
|
|
21
|
+
case "CRITICAL":
|
|
22
|
+
case "HIGH": return "error";
|
|
23
|
+
case "MEDIUM": return "warning";
|
|
24
|
+
default: return "note";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function relUri(p, srcRoot) {
|
|
29
|
+
if (!p) return null;
|
|
30
|
+
if (srcRoot && path.isAbsolute(p)) {
|
|
31
|
+
const r = path.relative(srcRoot, p);
|
|
32
|
+
if (r && !r.startsWith("..")) return r.split(path.sep).join("/");
|
|
33
|
+
}
|
|
34
|
+
return String(p).split(path.sep).join("/");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function depCoord(dep) {
|
|
38
|
+
const ns = dep.namespace || dep.groupId || "";
|
|
39
|
+
const name = dep.name || dep.artifactId;
|
|
40
|
+
if (dep.ecosystem === "maven" && ns) return `${ns}:${name}`;
|
|
41
|
+
if (dep.ecosystem === "composer" && ns) return `${ns}/${name}`;
|
|
42
|
+
return name;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build a SARIF 2.1.0 object from matches.
|
|
47
|
+
* opts: { projectInfo, toolVersion }
|
|
48
|
+
*/
|
|
49
|
+
function buildSarif(matches, opts = {}) {
|
|
50
|
+
const { projectInfo = {}, toolVersion = "0" } = opts;
|
|
51
|
+
const srcRoot = projectInfo.src && projectInfo.src.startsWith("(") ? null : projectInfo.src;
|
|
52
|
+
|
|
53
|
+
const rulesById = new Map();
|
|
54
|
+
const results = [];
|
|
55
|
+
|
|
56
|
+
for (const m of matches || []) {
|
|
57
|
+
const id = m.cve?.id;
|
|
58
|
+
if (!id) continue;
|
|
59
|
+
const cve = m.cve;
|
|
60
|
+
|
|
61
|
+
if (!rulesById.has(id)) {
|
|
62
|
+
const rule = {
|
|
63
|
+
id,
|
|
64
|
+
name: id,
|
|
65
|
+
shortDescription: { text: `${id} affecting ${depCoord(m.dep)}` },
|
|
66
|
+
helpUri: id.startsWith("CVE-") ? `https://nvd.nist.gov/vuln/detail/${id}` : `https://osv.dev/vulnerability/${id}`,
|
|
67
|
+
properties: {},
|
|
68
|
+
};
|
|
69
|
+
// GitHub Code Scanning reads `security-severity` (a CVSS number string).
|
|
70
|
+
if (cve.score != null) rule.properties["security-severity"] = String(cve.score);
|
|
71
|
+
const tags = ["security", "external/cwe", "dependency"];
|
|
72
|
+
if (cve.kev) tags.push("known-exploited", "cisa-kev");
|
|
73
|
+
rule.properties.tags = tags;
|
|
74
|
+
if (Array.isArray(cve.cwes)) rule.properties.cwe = cve.cwes;
|
|
75
|
+
rulesById.set(id, rule);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const coord = depCoord(m.dep);
|
|
79
|
+
const purl = purlFor(m.dep);
|
|
80
|
+
const bits = [`${coord}@${m.dep.version || "?"}`];
|
|
81
|
+
if (cve.kev) bits.push("CISA-KEV (exploited)");
|
|
82
|
+
if (cve.epssPercentile != null) bits.push(`EPSS ${Math.round(cve.epssPercentile * 100)}%`);
|
|
83
|
+
if (cve.fixVersion) bits.push(`fix ≥ ${cve.fixVersion}`);
|
|
84
|
+
const text = `${id} in ${bits.join(" · ")}${cve.description ? ` — ${cve.description.slice(0, 300)}` : ""}`;
|
|
85
|
+
|
|
86
|
+
const locations = (m.dep.manifestPaths || m.dep.pomPaths || [])
|
|
87
|
+
.map(p => relUri(p, srcRoot))
|
|
88
|
+
.filter(Boolean)
|
|
89
|
+
.map(uri => ({ physicalLocation: { artifactLocation: { uri } } }));
|
|
90
|
+
|
|
91
|
+
const props = {};
|
|
92
|
+
if (purl) props.purl = purl;
|
|
93
|
+
if (cve.epssScore != null) props.epss = cve.epssScore;
|
|
94
|
+
if (cve.epssPercentile != null) props.epssPercentile = cve.epssPercentile;
|
|
95
|
+
if (cve.kev) props.kev = true;
|
|
96
|
+
if (cve.priority?.band) props.priorityBand = cve.priority.band;
|
|
97
|
+
if (m.dep.provenance && m.dep.provenance !== "manifest") props.provenance = m.dep.provenance;
|
|
98
|
+
if (m.cpeFiltered) props.cpeFiltered = true;
|
|
99
|
+
if (m.source) props.sources = m.source;
|
|
100
|
+
|
|
101
|
+
results.push({
|
|
102
|
+
ruleId: id,
|
|
103
|
+
level: sarifLevel(cve.severity),
|
|
104
|
+
message: { text },
|
|
105
|
+
...(locations.length ? { locations } : {}),
|
|
106
|
+
partialFingerprints: { fadKey: `${coord}@${m.dep.version || "?"}|${id}` },
|
|
107
|
+
properties: props,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
113
|
+
version: "2.1.0",
|
|
114
|
+
runs: [{
|
|
115
|
+
tool: {
|
|
116
|
+
driver: {
|
|
117
|
+
name: "fad-checker",
|
|
118
|
+
version: String(toolVersion),
|
|
119
|
+
informationUri: "https://github.com/n8tz/fad-checker",
|
|
120
|
+
rules: [...rulesById.values()],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
results,
|
|
124
|
+
}],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function writeSarif(matches, outputPath, opts = {}) {
|
|
129
|
+
const doc = buildSarif(matches, opts);
|
|
130
|
+
fs.writeFileSync(outputPath, JSON.stringify(doc, null, 2) + "\n");
|
|
131
|
+
return doc;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { buildSarif, writeSarif, sarifLevel };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/sbom-export.js — emit a CycloneDX 1.6 SBOM with vulnerabilities inline
|
|
3
|
+
* (a VDR — Vulnerability Disclosure Report) from the resolved deps + matches.
|
|
4
|
+
*
|
|
5
|
+
* buildCycloneDx is pure (testable); writeCycloneDx writes the JSON to disk.
|
|
6
|
+
*
|
|
7
|
+
* @author: N.BRAUN
|
|
8
|
+
* @email: pp9ping@gmail.com
|
|
9
|
+
*/
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const { purlFor } = require("./purl");
|
|
12
|
+
|
|
13
|
+
// NVD "CVSS:3.1" → CycloneDX rating method enum. Tolerates the legacy
|
|
14
|
+
// "CVSS:V31"/"V30"/"V40"/"V2" labels left in pre-fix NVD caches.
|
|
15
|
+
function cvssMethod(cvssVersion) {
|
|
16
|
+
const v = String(cvssVersion || "");
|
|
17
|
+
if (v.includes("4.0") || /v40\b/i.test(v)) return "CVSSv4";
|
|
18
|
+
if (v.includes("3.1") || /v31\b/i.test(v)) return "CVSSv31";
|
|
19
|
+
if (v.includes("3.0") || v === "CVSS:3" || /v30\b/i.test(v)) return "CVSSv3";
|
|
20
|
+
if (v.includes("2.0") || v === "CVSS:2" || /v2\b/i.test(v)) return "CVSSv2";
|
|
21
|
+
return "other";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Point the advisory source at the right database for the id type — a GHSA/OSV id
|
|
25
|
+
// has no page on nvd.nist.gov, so the old hardcoded NVD url was a dead link.
|
|
26
|
+
function advisorySource(id, source) {
|
|
27
|
+
if (/^CVE-/i.test(id)) return { name: "NVD", url: `https://nvd.nist.gov/vuln/detail/${encodeURIComponent(id)}` };
|
|
28
|
+
if (/^GHSA-/i.test(id)) return { name: "GitHub Advisory Database", url: `https://github.com/advisories/${encodeURIComponent(id)}` };
|
|
29
|
+
return { name: (source || "").includes("nvd") ? "NVD" : "OSV", url: `https://osv.dev/vulnerability/${encodeURIComponent(id)}` };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Stable, UNIQUE bom-ref. Declared deps use their purl (unchanged). Embedded
|
|
33
|
+
// binaries get a location-qualified ref so two copies of the same coordinate
|
|
34
|
+
// (one declared, one shaded into a jar) don't collide on the same bom-ref.
|
|
35
|
+
function bomRefFor(dep, purl) {
|
|
36
|
+
if (dep.provenance === "embedded") return dep.coordKey; // already "embedded:<path>" — unique
|
|
37
|
+
return purl || null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function cdxSeverity(sev) {
|
|
41
|
+
const s = (sev || "unknown").toLowerCase();
|
|
42
|
+
return ["critical", "high", "medium", "low", "none", "info"].includes(s) ? s : "unknown";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function cweNum(c) {
|
|
46
|
+
const m = String(c).match(/(\d+)/);
|
|
47
|
+
return m ? parseInt(m[1], 10) : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function licenseEntry(ids) {
|
|
51
|
+
// CycloneDX: {license:{id}} for SPDX ids, {license:{name}} otherwise.
|
|
52
|
+
return (ids || []).map(x => /^[A-Za-z0-9.+-]+$/.test(x) && /[A-Z]/.test(x) ? { license: { id: x } } : { license: { name: x } });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build a CycloneDX 1.6 object.
|
|
57
|
+
* opts: { projectInfo, toolVersion, timestamp, licenseResults }
|
|
58
|
+
*/
|
|
59
|
+
function buildCycloneDx(resolvedDeps, cveMatches, opts = {}) {
|
|
60
|
+
const { projectInfo = {}, toolVersion = "0", timestamp, licenseResults } = opts;
|
|
61
|
+
|
|
62
|
+
// coordKey → [spdx ids] for component licenses (optional).
|
|
63
|
+
const licByCoord = new Map();
|
|
64
|
+
for (const e of licenseResults?.assessed || []) {
|
|
65
|
+
const key = e.dep?.coordKey || `${e.dep?.namespace || ""}:${e.dep?.name}`;
|
|
66
|
+
const ids = (e.ids || []).concat(e.raw || []);
|
|
67
|
+
if (ids.length) licByCoord.set(key, ids);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const components = [];
|
|
71
|
+
for (const dep of resolvedDeps.values()) {
|
|
72
|
+
const purl = purlFor(dep);
|
|
73
|
+
const comp = { type: "library", name: dep.name || dep.artifactId, version: dep.version || undefined };
|
|
74
|
+
if (dep.namespace || dep.groupId) comp.group = dep.namespace || dep.groupId;
|
|
75
|
+
const ref = bomRefFor(dep, purl);
|
|
76
|
+
if (ref) comp["bom-ref"] = ref;
|
|
77
|
+
if (purl) comp.purl = purl;
|
|
78
|
+
const lic = licByCoord.get(dep.coordKey);
|
|
79
|
+
if (lic) comp.licenses = licenseEntry(lic);
|
|
80
|
+
// Mark embedded-binary components so the SBOM consumer can tell a vendored/fat-jar
|
|
81
|
+
// coordinate apart from a declared dependency, and where it physically lives.
|
|
82
|
+
if (dep.provenance === "embedded") {
|
|
83
|
+
comp.properties = [
|
|
84
|
+
{ name: "fad:provenance", value: "embedded" },
|
|
85
|
+
...((dep.manifestPaths || [])[0] ? [{ name: "fad:location", value: dep.manifestPaths[0] }] : []),
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
components.push(comp);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// One vulnerability entry per CVE id, aggregating affected component refs.
|
|
92
|
+
const byCve = new Map();
|
|
93
|
+
for (const m of cveMatches || []) {
|
|
94
|
+
const id = m.cve?.id;
|
|
95
|
+
if (!id) continue;
|
|
96
|
+
const ref = bomRefFor(m.dep, purlFor(m.dep)); // match the component bom-ref (unique for embedded)
|
|
97
|
+
let v = byCve.get(id);
|
|
98
|
+
if (!v) {
|
|
99
|
+
const cve = m.cve;
|
|
100
|
+
v = {
|
|
101
|
+
"bom-ref": `vuln-${id}`,
|
|
102
|
+
id,
|
|
103
|
+
source: advisorySource(id, m.source),
|
|
104
|
+
ratings: cve.score != null ? [{
|
|
105
|
+
source: { name: "NVD" },
|
|
106
|
+
score: cve.score,
|
|
107
|
+
severity: cdxSeverity(cve.severity),
|
|
108
|
+
method: cvssMethod(cve.cvssVersion),
|
|
109
|
+
...(cve.cvssVector ? { vector: cve.cvssVector } : {}),
|
|
110
|
+
}] : [],
|
|
111
|
+
...(Array.isArray(cve.cwes) && cve.cwes.length ? { cwes: cve.cwes.map(cweNum).filter(n => n != null) } : {}),
|
|
112
|
+
...(cve.description ? { description: cve.description } : {}),
|
|
113
|
+
affects: [],
|
|
114
|
+
properties: buildVulnProps(cve),
|
|
115
|
+
};
|
|
116
|
+
byCve.set(id, v);
|
|
117
|
+
}
|
|
118
|
+
if (ref && !v.affects.some(a => a.ref === ref)) v.affects.push({ ref });
|
|
119
|
+
if (m.cpeFiltered) v.properties.push({ name: "fad:cpe-filtered", value: "true" });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const bom = {
|
|
123
|
+
bomFormat: "CycloneDX",
|
|
124
|
+
specVersion: "1.6",
|
|
125
|
+
version: 1,
|
|
126
|
+
metadata: {
|
|
127
|
+
...(timestamp ? { timestamp } : {}),
|
|
128
|
+
tools: { components: [{ type: "application", name: "fad-checker", version: String(toolVersion) }] },
|
|
129
|
+
component: { type: "application", name: projectInfo.name || "project" },
|
|
130
|
+
},
|
|
131
|
+
components,
|
|
132
|
+
vulnerabilities: [...byCve.values()],
|
|
133
|
+
};
|
|
134
|
+
return bom;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function buildVulnProps(cve) {
|
|
138
|
+
const props = [];
|
|
139
|
+
if (cve.epssScore != null) props.push({ name: "fad:epss", value: String(cve.epssScore) });
|
|
140
|
+
if (cve.epssPercentile != null) props.push({ name: "fad:epssPercentile", value: String(cve.epssPercentile) });
|
|
141
|
+
if (cve.kev) props.push({ name: "fad:kev", value: "true" });
|
|
142
|
+
if (cve.priority?.band) props.push({ name: "fad:priorityBand", value: cve.priority.band });
|
|
143
|
+
if (cve.priority?.score != null) props.push({ name: "fad:priorityScore", value: String(cve.priority.score) });
|
|
144
|
+
return props;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function writeCycloneDx(resolvedDeps, cveMatches, outputPath, opts = {}) {
|
|
148
|
+
const bom = buildCycloneDx(resolvedDeps, cveMatches, opts);
|
|
149
|
+
fs.writeFileSync(outputPath, JSON.stringify(bom, null, 2) + "\n");
|
|
150
|
+
return bom;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = { buildCycloneDx, writeCycloneDx, cvssMethod, cdxSeverity };
|
package/lib/scan-completeness.js
CHANGED
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Output: an array of warning objects matching the same shape as
|
|
20
20
|
* lib/npm/collect.js warnings ({ type, manifestPath?, message, ... }).
|
|
21
|
+
*
|
|
22
|
+
* @author: N.BRAUN
|
|
23
|
+
* @email: pp9ping@gmail.com
|
|
21
24
|
*/
|
|
22
25
|
/**
|
|
23
26
|
* Scan the resolved deps map and return warnings.
|
package/lib/snyk.js
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* Runs `snyk test --all-projects --json` on the cleaned POM directory and
|
|
5
5
|
* normalises the output to fad-checker's CVE match shape so the report can
|
|
6
6
|
* merge findings from both engines.
|
|
7
|
+
*
|
|
8
|
+
* @author: N.BRAUN
|
|
9
|
+
* @email: pp9ping@gmail.com
|
|
7
10
|
*/
|
|
8
11
|
const { execFile } = require("child_process");
|
|
9
12
|
const { promisify } = require("util");
|
package/lib/suppress.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/suppress.js — triage: suppress accepted-risk / false-positive findings so
|
|
3
|
+
* re-audits of the same codebase stay signal-rich.
|
|
4
|
+
*
|
|
5
|
+
* Two inputs:
|
|
6
|
+
* --ignore <file> : plain text rules, one per line:
|
|
7
|
+
* CVE-2021-44228 # suppress this CVE everywhere
|
|
8
|
+
* CVE-2021-44228 org.apache.* # …only for matching coord/purl (glob)
|
|
9
|
+
* * com.acme.internal:* # any CVE for these coords
|
|
10
|
+
* anything after '#' is a reason
|
|
11
|
+
* --vex <file> : a CSAF VEX document — CVEs whose product_status is
|
|
12
|
+
* known_not_affected / fixed are suppressed (round-trips fad's own
|
|
13
|
+
* --report-csaf output; products mapped back to coords via their purl).
|
|
14
|
+
*
|
|
15
|
+
* Pure parsing + matching; the caller reads the files and mutates matches.
|
|
16
|
+
*
|
|
17
|
+
* @author: N.BRAUN
|
|
18
|
+
* @email: pp9ping@gmail.com
|
|
19
|
+
*/
|
|
20
|
+
const { purlFor } = require("./purl");
|
|
21
|
+
|
|
22
|
+
function coordOf(dep) {
|
|
23
|
+
const ns = dep.namespace || dep.groupId || "";
|
|
24
|
+
const name = dep.name || dep.artifactId;
|
|
25
|
+
if (dep.ecosystem === "maven" && ns) return `${ns}:${name}`;
|
|
26
|
+
if (dep.ecosystem === "composer" && ns) return `${ns}/${name}`;
|
|
27
|
+
return name;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Glob → anchored RegExp ('*' = any run). Everything else is literal.
|
|
31
|
+
function globToRe(glob) {
|
|
32
|
+
const esc = String(glob).replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
33
|
+
return new RegExp(`^${esc}$`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Parse an --ignore file body into rules [{ cve, coordRe, coord, reason }]. */
|
|
37
|
+
function parseIgnoreFile(text) {
|
|
38
|
+
const rules = [];
|
|
39
|
+
for (const raw of String(text || "").split(/\r?\n/)) {
|
|
40
|
+
const hash = raw.indexOf("#");
|
|
41
|
+
const reason = hash >= 0 ? raw.slice(hash + 1).trim() : null;
|
|
42
|
+
const line = (hash >= 0 ? raw.slice(0, hash) : raw).trim();
|
|
43
|
+
if (!line) continue;
|
|
44
|
+
const parts = line.split(/\s+/);
|
|
45
|
+
const cve = parts[0];
|
|
46
|
+
const coord = parts[1] || null;
|
|
47
|
+
rules.push({ cve, coord, coordRe: coord ? globToRe(coord) : null, reason: reason || "ignored" });
|
|
48
|
+
}
|
|
49
|
+
return rules;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Parse a CSAF VEX document into suppression rules for not_affected/fixed CVEs. */
|
|
53
|
+
function parseVex(csaf) {
|
|
54
|
+
const rules = [];
|
|
55
|
+
if (!csaf || typeof csaf !== "object") return rules;
|
|
56
|
+
// product_id → purl, to map suppressed products back to coordinates.
|
|
57
|
+
const purlByPid = {};
|
|
58
|
+
for (const p of csaf.product_tree?.full_product_names || []) {
|
|
59
|
+
const purl = p.product_identification_helper?.purl;
|
|
60
|
+
if (p.product_id && purl) purlByPid[p.product_id] = purl;
|
|
61
|
+
}
|
|
62
|
+
for (const v of csaf.vulnerabilities || []) {
|
|
63
|
+
const cve = v.cve;
|
|
64
|
+
if (!cve) continue;
|
|
65
|
+
const cleared = [
|
|
66
|
+
...(v.product_status?.known_not_affected || []),
|
|
67
|
+
...(v.product_status?.fixed || []),
|
|
68
|
+
];
|
|
69
|
+
if (!cleared.length) {
|
|
70
|
+
// No product scope → suppress the CVE globally.
|
|
71
|
+
rules.push({ cve, coord: null, coordRe: null, reason: "VEX: not affected / fixed" });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
for (const pid of cleared) {
|
|
75
|
+
const purl = purlByPid[pid];
|
|
76
|
+
// A product we can't map back to a coord must NOT become a coordRe:null
|
|
77
|
+
// rule — that would suppress this CVE for EVERY dependency (a security
|
|
78
|
+
// false-negative). Skip it: a product-scoped clearance only ever narrows.
|
|
79
|
+
if (!purl) continue;
|
|
80
|
+
rules.push({ cve, coord: purl, coordRe: globToRe(purl), reason: "VEX: not affected / fixed" });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return rules;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function ruleMatches(rule, m) {
|
|
87
|
+
if (rule.cve && rule.cve !== "*" && rule.cve !== m.cve?.id) return false;
|
|
88
|
+
if (!rule.coordRe) return true;
|
|
89
|
+
const coord = coordOf(m.dep);
|
|
90
|
+
const purl = purlFor(m.dep) || "";
|
|
91
|
+
return rule.coordRe.test(coord) || rule.coordRe.test(purl) || rule.coordRe.test(m.dep.name || "");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Build a matcher from a flat rule list. */
|
|
95
|
+
function buildSuppressor(rules) {
|
|
96
|
+
return (m) => {
|
|
97
|
+
for (const r of rules || []) if (ruleMatches(r, m)) return { suppressed: true, reason: r.reason };
|
|
98
|
+
return null;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Mark matches in place; returns the number suppressed. */
|
|
103
|
+
function applySuppressions(matches, rules) {
|
|
104
|
+
const suppressor = buildSuppressor(rules);
|
|
105
|
+
let n = 0;
|
|
106
|
+
for (const m of matches || []) {
|
|
107
|
+
const hit = suppressor(m);
|
|
108
|
+
if (hit) { m.suppressed = true; m.suppressedReason = hit.reason; n++; }
|
|
109
|
+
}
|
|
110
|
+
return n;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { parseIgnoreFile, parseVex, buildSuppressor, applySuppressions, globToRe };
|
package/lib/transitive.js
CHANGED
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
* - <relocation> handling (rare in modern artifacts)
|
|
19
19
|
* - non-central repositories (everything fetched from repo1.maven.org)
|
|
20
20
|
* - SNAPSHOT version resolution (we just return the literal version)
|
|
21
|
+
*
|
|
22
|
+
* @author: N.BRAUN
|
|
23
|
+
* @email: pp9ping@gmail.com
|
|
21
24
|
*/
|
|
22
25
|
const fs = require("fs");
|
|
23
26
|
const path = require("path");
|
|
@@ -396,12 +399,12 @@ async function resolveTransitiveDeps(directDeps, opts = {}) {
|
|
|
396
399
|
// Progress visible whenever stdout is a TTY — transitive resolution
|
|
397
400
|
// dominates wall-clock time on first run and used to look like a hang.
|
|
398
401
|
// On non-TTY (pipes, CI, tests) we skip the \r-overwrite spam.
|
|
399
|
-
if (process.stdout.isTTY) process.stdout.write(`\r resolved ${out.size} transitives, queue=${queue.length - head} `);
|
|
402
|
+
if (verbose && process.stdout.isTTY) process.stdout.write(`\r resolved ${out.size} transitives, queue=${queue.length - head} `);
|
|
400
403
|
}
|
|
401
404
|
});
|
|
402
405
|
await Promise.all(workers);
|
|
403
|
-
if (process.stdout.isTTY) process.stdout.write(`\r resolved ${out.size} transitives \n`);
|
|
404
|
-
else if (out.size) console.log(` resolved ${out.size} transitives`);
|
|
406
|
+
if (verbose && process.stdout.isTTY) process.stdout.write(`\r resolved ${out.size} transitives \n`);
|
|
407
|
+
else if (verbose && out.size) console.log(` resolved ${out.size} transitives`);
|
|
405
408
|
|
|
406
409
|
return out;
|
|
407
410
|
}
|
package/lib/ui.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/ui.js — shared CLI presentation: banner, section headers, severity colors,
|
|
3
|
+
* and a global step progress indicator for the cache/database update phase.
|
|
4
|
+
*
|
|
5
|
+
* The Progress indicator renders a "[n/N] <spinner> label — summary" checklist.
|
|
6
|
+
* TTY: one animated line per step, rewritten in place, finalized with ✓/⊘/✗.
|
|
7
|
+
* Non-TTY (pipes, CI, files): a single plain line per finished step, no escapes.
|
|
8
|
+
*
|
|
9
|
+
* @author: N.BRAUN
|
|
10
|
+
* @email: pp9ping@gmail.com
|
|
11
|
+
*/
|
|
12
|
+
const chalk = require("chalk");
|
|
13
|
+
|
|
14
|
+
const isTTY = !!(process.stdout && process.stdout.isTTY) && process.env.TERM !== "dumb";
|
|
15
|
+
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
16
|
+
|
|
17
|
+
const TITLE_A = "fad-checker";
|
|
18
|
+
const TITLE_B = "Autonomous Dependency Checker";
|
|
19
|
+
|
|
20
|
+
function banner() {
|
|
21
|
+
const raw = `${TITLE_A} · ${TITLE_B}`;
|
|
22
|
+
const bar = "─".repeat(raw.length + 2);
|
|
23
|
+
console.log(chalk.cyan(`\n╭${bar}╮`));
|
|
24
|
+
console.log(chalk.cyan("│ ") + chalk.bold.white(TITLE_A) + chalk.cyan(" · ") + chalk.whiteBright(TITLE_B) + chalk.cyan(" │"));
|
|
25
|
+
console.log(chalk.cyan(`╰${bar}╯`));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function section(title) {
|
|
29
|
+
console.log(chalk.bold.cyan("\n▸ ") + chalk.bold(title));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// " label value" aligned key/value line under a section.
|
|
33
|
+
function kv(label, value, { pad = 10 } = {}) {
|
|
34
|
+
console.log(" " + chalk.dim(String(label).padEnd(pad)) + " " + value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ok(msg) { console.log(" " + chalk.green("✓") + " " + msg); }
|
|
38
|
+
function warn(msg) { console.log(" " + chalk.yellow("⚠") + " " + msg); }
|
|
39
|
+
function info(msg) { console.log(" " + chalk.dim("·") + " " + msg); }
|
|
40
|
+
|
|
41
|
+
function sevColor(sev) {
|
|
42
|
+
switch (String(sev || "").toUpperCase()) {
|
|
43
|
+
case "CRITICAL": return chalk.bold.red;
|
|
44
|
+
case "HIGH": return chalk.red;
|
|
45
|
+
case "MEDIUM": return chalk.yellow;
|
|
46
|
+
case "LOW": return chalk.blue;
|
|
47
|
+
default: return chalk.gray;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class Step {
|
|
52
|
+
constructor(n, total, label) {
|
|
53
|
+
this.n = n; this.total = total; this.label = label;
|
|
54
|
+
this.live = ""; this.frame = 0; this.timer = null; this.ended = false;
|
|
55
|
+
this.prefix = chalk.dim(`[${n}/${total}]`);
|
|
56
|
+
if (isTTY) {
|
|
57
|
+
this._render();
|
|
58
|
+
this.timer = setInterval(() => { this.frame++; this._render(); }, 80);
|
|
59
|
+
if (this.timer.unref) this.timer.unref();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
_render() {
|
|
63
|
+
if (!isTTY || this.ended) return;
|
|
64
|
+
const sp = chalk.cyan(SPINNER[this.frame % SPINNER.length]);
|
|
65
|
+
const live = this.live ? chalk.dim(" " + this.live) : chalk.dim(" …");
|
|
66
|
+
process.stdout.write(`\r ${this.prefix} ${sp} ${this.label}${live}\x1b[K`);
|
|
67
|
+
}
|
|
68
|
+
tick(processed, total) {
|
|
69
|
+
this.live = total ? `${processed}/${total}` : String(processed || "");
|
|
70
|
+
// rendering is driven by the timer; nothing to print on non-TTY
|
|
71
|
+
}
|
|
72
|
+
_finalize(symbol, color, summary) {
|
|
73
|
+
if (this.ended) return;
|
|
74
|
+
this.ended = true;
|
|
75
|
+
if (this.timer) { clearInterval(this.timer); this.timer = null; }
|
|
76
|
+
const line = ` ${this.prefix} ${color(symbol)} ${this.label}` + (summary ? chalk.dim(" — " + summary) : "");
|
|
77
|
+
if (isTTY) process.stdout.write(`\r${line}\x1b[K\n`);
|
|
78
|
+
else console.log(line);
|
|
79
|
+
}
|
|
80
|
+
done(summary) { this._finalize("✓", chalk.green, summary); }
|
|
81
|
+
skip(reason) { this._finalize("⊘", chalk.gray, reason); }
|
|
82
|
+
fail(msg) { this._finalize("✗", chalk.red, msg); }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
class Progress {
|
|
86
|
+
constructor(total) { this.total = total || 0; this.n = 0; }
|
|
87
|
+
start(label) { this.n += 1; return new Step(this.n, this.total, label); }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { banner, section, kv, ok, warn, info, sevColor, Progress, isTTY };
|
package/package.json
CHANGED
|
@@ -1,9 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fad-checker",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "Scan ALL Maven, npm, Yarn, Composer, Python, C#/.NET, Go & Ruby dependencies — plus embedded JARs (fat-jars/war/ear) — in a source tree ONE SHOT without mvn/python/etc — CVE (EPSS/KEV-prioritised), EOL, obsolete, outdated & licenses, with SBOM/CSAF/SARIF/JSON exports, CI gating and fix recos",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"sca",
|
|
7
|
+
"software-composition-analysis",
|
|
8
|
+
"dependency-scanner",
|
|
9
|
+
"vulnerability-scanner",
|
|
10
|
+
"security",
|
|
11
|
+
"cve",
|
|
12
|
+
"osv",
|
|
13
|
+
"nvd",
|
|
14
|
+
"epss",
|
|
15
|
+
"kev",
|
|
16
|
+
"cyclonedx",
|
|
17
|
+
"sbom",
|
|
18
|
+
"csaf",
|
|
19
|
+
"vex",
|
|
20
|
+
"sarif",
|
|
21
|
+
"license-compliance",
|
|
22
|
+
"eol",
|
|
23
|
+
"end-of-life",
|
|
24
|
+
"outdated",
|
|
25
|
+
"deprecated",
|
|
26
|
+
"audit",
|
|
27
|
+
"devsecops",
|
|
28
|
+
"appsec",
|
|
29
|
+
"supply-chain",
|
|
30
|
+
"offline",
|
|
31
|
+
"air-gapped",
|
|
32
|
+
"passi",
|
|
33
|
+
"maven",
|
|
34
|
+
"npm",
|
|
35
|
+
"yarn",
|
|
36
|
+
"pnpm",
|
|
37
|
+
"composer",
|
|
38
|
+
"pypi",
|
|
39
|
+
"nuget",
|
|
40
|
+
"go",
|
|
41
|
+
"golang",
|
|
42
|
+
"ruby",
|
|
43
|
+
"rubygems",
|
|
44
|
+
"retirejs",
|
|
45
|
+
"polyglot",
|
|
46
|
+
"cli"
|
|
47
|
+
],
|
|
5
48
|
"license": "MIT",
|
|
6
49
|
"repository": "https://github.com/n8tz/fad-checker",
|
|
50
|
+
"homepage": "https://github.com/n8tz/fad-checker#readme",
|
|
51
|
+
"bugs": "https://github.com/n8tz/fad-checker/issues",
|
|
7
52
|
"author": "pp9Ping <pp9Ping@gmail.com>",
|
|
8
53
|
"maintainers": [
|
|
9
54
|
"pp9Ping <pp9Ping@gmail.com>"
|
|
@@ -25,6 +70,8 @@
|
|
|
25
70
|
"dependencies": {
|
|
26
71
|
"chalk": "^4.1.2",
|
|
27
72
|
"commander": "^14.0.1",
|
|
73
|
+
"fflate": "^0.8.3",
|
|
74
|
+
"js-yaml": "^4.1.1",
|
|
28
75
|
"p-limit": "^3.1.0",
|
|
29
76
|
"retire": "^5.4.2",
|
|
30
77
|
"rimraf": "^6.0.1",
|