fad-checker 2.0.1 → 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.
Files changed (66) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +90 -16
  3. package/completions/fad-checker.bash +1 -1
  4. package/completions/fad-checker.zsh +14 -1
  5. package/data/license-policy.json +97 -0
  6. package/fad-checker-report/cve-report.doc +630 -0
  7. package/fad-checker-report/cve-report.html +748 -0
  8. package/fad-checker.js +278 -53
  9. package/lib/cache-archive.js +3 -0
  10. package/lib/codecs/codec.interface.js +3 -0
  11. package/lib/codecs/composer/parse.js +3 -0
  12. package/lib/codecs/composer/registry.js +13 -5
  13. package/lib/codecs/composer.codec.js +3 -0
  14. package/lib/codecs/go/parse.js +65 -0
  15. package/lib/codecs/go/registry.js +74 -0
  16. package/lib/codecs/go.codec.js +76 -0
  17. package/lib/codecs/index.js +7 -2
  18. package/lib/codecs/maven/jar-scan.js +199 -0
  19. package/lib/codecs/maven.codec.js +17 -1
  20. package/lib/codecs/npm/collect.js +11 -0
  21. package/lib/codecs/npm/parse.js +3 -0
  22. package/lib/codecs/npm/registry.js +10 -2
  23. package/lib/codecs/npm.codec.js +3 -0
  24. package/lib/codecs/nuget/parse.js +3 -0
  25. package/lib/codecs/nuget/registry.js +11 -4
  26. package/lib/codecs/nuget.codec.js +3 -0
  27. package/lib/codecs/pypi/parse.js +7 -2
  28. package/lib/codecs/pypi/registry.js +24 -5
  29. package/lib/codecs/pypi.codec.js +3 -0
  30. package/lib/codecs/recipes.js +28 -1
  31. package/lib/codecs/ruby/parse.js +42 -0
  32. package/lib/codecs/ruby/registry.js +76 -0
  33. package/lib/codecs/ruby.codec.js +66 -0
  34. package/lib/codecs/select.js +3 -0
  35. package/lib/codecs/yarn.codec.js +3 -0
  36. package/lib/config.js +3 -0
  37. package/lib/core.js +3 -0
  38. package/lib/cpe.js +30 -5
  39. package/lib/csaf-export.js +159 -0
  40. package/lib/cve-download.js +3 -0
  41. package/lib/cve-match.js +12 -1
  42. package/lib/cve-report.js +157 -28
  43. package/lib/dep-record.js +15 -2
  44. package/lib/deps-descriptor.js +3 -0
  45. package/lib/epss.js +115 -0
  46. package/lib/gate.js +45 -0
  47. package/lib/json-export.js +110 -0
  48. package/lib/kev.js +88 -0
  49. package/lib/license-policy.js +110 -0
  50. package/lib/maven-license.js +52 -0
  51. package/lib/maven-repo.js +3 -0
  52. package/lib/maven-version.js +8 -3
  53. package/lib/nvd.js +13 -3
  54. package/lib/osv.js +68 -19
  55. package/lib/outdated.js +3 -0
  56. package/lib/priority.js +90 -0
  57. package/lib/purl.js +77 -0
  58. package/lib/retire.js +3 -0
  59. package/lib/sarif-export.js +134 -0
  60. package/lib/sbom-export.js +153 -0
  61. package/lib/scan-completeness.js +3 -0
  62. package/lib/snyk.js +3 -0
  63. package/lib/suppress.js +113 -0
  64. package/lib/transitive.js +3 -0
  65. package/lib/ui.js +3 -0
  66. package/package.json +48 -2
package/lib/purl.js ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * lib/purl.js — build Package URL (purl) strings from a depRecord.
3
+ *
4
+ * purl spec: pkg:<type>/<namespace>/<name>@<version>?qualifiers#subpath
5
+ * We only emit type/namespace/name@version — enough for CycloneDX `bom-ref`
6
+ * and CSAF `product_identification_helper.purl`.
7
+ *
8
+ * Pure (no I/O). Shared by lib/sbom-export.js and lib/csaf-export.js.
9
+ *
10
+ * @author: N.BRAUN
11
+ * @email: pp9ping@gmail.com
12
+ */
13
+
14
+ // purl type per ecosystem. Maven keeps the dotted groupId as a single namespace
15
+ // segment (not slash-separated); npm/composer carry a namespace; pypi/nuget don't.
16
+ const TYPE = {
17
+ maven: "maven",
18
+ npm: "npm",
19
+ composer: "composer",
20
+ pypi: "pypi",
21
+ nuget: "nuget",
22
+ go: "golang",
23
+ ruby: "gem",
24
+ };
25
+
26
+ // Per the purl spec, each namespace segment and the name are percent-encoded,
27
+ // but the dots inside a Maven groupId and the separators are preserved by
28
+ // encodeURIComponent (it leaves "." "-" "_" "~" alone), so it is the right tool.
29
+ function enc(s) {
30
+ return encodeURIComponent(String(s));
31
+ }
32
+
33
+ // PEP 503 normalisation for PyPI names (purl mandates the canonical form).
34
+ function normalizePypi(name) {
35
+ return String(name).toLowerCase().replace(/[-_.]+/g, "-");
36
+ }
37
+
38
+ /**
39
+ * Build a purl for a depRecord. Returns null when the dep lacks a usable
40
+ * ecosystem/name. Version is omitted when absent.
41
+ */
42
+ function purlFor(dep) {
43
+ if (!dep || typeof dep !== "object") return null;
44
+ const eco = dep.ecosystem || dep.ecosystemType;
45
+ const type = TYPE[eco];
46
+ if (!type) return null;
47
+
48
+ const rawName = dep.name || dep.artifactId;
49
+ if (!rawName) return null;
50
+ let namespace = dep.namespace || dep.groupId || "";
51
+ let name = rawName;
52
+
53
+ if (eco === "npm" && !namespace && name.startsWith("@") && name.includes("/")) {
54
+ // Scoped package: "@scope/pkg" → namespace "@scope", name "pkg".
55
+ const slash = name.indexOf("/");
56
+ namespace = name.slice(0, slash);
57
+ name = name.slice(slash + 1);
58
+ } else if (eco === "pypi") {
59
+ name = normalizePypi(name);
60
+ namespace = "";
61
+ } else if (eco === "nuget") {
62
+ namespace = "";
63
+ } else if (eco === "go" && !namespace && name.includes("/")) {
64
+ // Go module path: namespace = everything up to the last "/", name = last segment.
65
+ const slash = name.lastIndexOf("/");
66
+ namespace = name.slice(0, slash);
67
+ name = name.slice(slash + 1);
68
+ }
69
+
70
+ // Namespace may itself be slash-separated (Go module paths); encode each
71
+ // segment but keep the separators.
72
+ const nsPart = namespace ? `${String(namespace).split("/").map(enc).join("/")}/` : "";
73
+ const verPart = dep.version ? `@${enc(dep.version)}` : "";
74
+ return `pkg:${type}/${nsPart}${enc(name)}${verPart}`;
75
+ }
76
+
77
+ module.exports = { purlFor };
package/lib/retire.js CHANGED
@@ -12,6 +12,9 @@
12
12
  * The CLI is expected at node_modules/.bin/retire (declared in
13
13
  * package.json deps). When bundled with bun, we also try `retire`
14
14
  * on PATH as a fallback.
15
+ *
16
+ * @author: N.BRAUN
17
+ * @email: pp9ping@gmail.com
15
18
  */
16
19
  const fs = require("fs");
17
20
  const path = require("path");
@@ -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 };
@@ -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");
@@ -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");
package/lib/ui.js CHANGED
@@ -5,6 +5,9 @@
5
5
  * The Progress indicator renders a "[n/N] <spinner> label — summary" checklist.
6
6
  * TTY: one animated line per step, rewritten in place, finalized with ✓/⊘/✗.
7
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
8
11
  */
9
12
  const chalk = require("chalk");
10
13
 
package/package.json CHANGED
@@ -1,9 +1,54 @@
1
1
  {
2
2
  "name": "fad-checker",
3
- "version": "2.0.1",
4
- "description": "Scan ALL Maven, npm, Yarn, Composer, Python & C#/.NET dependencies in a source tree ONE SHOT without mvn/python/etc, deal with private deps, multi mvn modules, EOL, obsolete, outdated deps & give fix recos",
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,7 @@
25
70
  "dependencies": {
26
71
  "chalk": "^4.1.2",
27
72
  "commander": "^14.0.1",
73
+ "fflate": "^0.8.3",
28
74
  "js-yaml": "^4.1.1",
29
75
  "p-limit": "^3.1.0",
30
76
  "retire": "^5.4.2",