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.
Files changed (139) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +147 -25
  3. package/completions/fad-checker.bash +1 -1
  4. package/completions/fad-checker.zsh +14 -1
  5. package/data/eol-mapping.json +5 -5
  6. package/data/license-policy.json +97 -0
  7. package/fad-checker-report/cve-report.doc +630 -0
  8. package/fad-checker-report/cve-report.html +748 -0
  9. package/fad-checker.js +556 -245
  10. package/lib/cache-archive.js +3 -0
  11. package/lib/codecs/codec.interface.js +3 -0
  12. package/lib/codecs/composer/parse.js +3 -0
  13. package/lib/codecs/composer/registry.js +13 -5
  14. package/lib/codecs/composer.codec.js +3 -0
  15. package/lib/codecs/go/parse.js +65 -0
  16. package/lib/codecs/go/registry.js +74 -0
  17. package/lib/codecs/go.codec.js +76 -0
  18. package/lib/codecs/index.js +7 -2
  19. package/lib/codecs/maven/jar-scan.js +199 -0
  20. package/lib/codecs/maven.codec.js +17 -1
  21. package/lib/codecs/npm/collect.js +37 -17
  22. package/lib/codecs/npm/parse.js +117 -10
  23. package/lib/codecs/npm/registry.js +14 -5
  24. package/lib/codecs/npm.codec.js +4 -1
  25. package/lib/codecs/nuget/parse.js +3 -0
  26. package/lib/codecs/nuget/registry.js +11 -4
  27. package/lib/codecs/nuget.codec.js +7 -2
  28. package/lib/codecs/pypi/parse.js +111 -14
  29. package/lib/codecs/pypi/registry.js +24 -5
  30. package/lib/codecs/pypi.codec.js +30 -6
  31. package/lib/codecs/recipes.js +28 -1
  32. package/lib/codecs/ruby/parse.js +42 -0
  33. package/lib/codecs/ruby/registry.js +76 -0
  34. package/lib/codecs/ruby.codec.js +66 -0
  35. package/lib/codecs/select.js +3 -0
  36. package/lib/codecs/yarn.codec.js +3 -0
  37. package/lib/config.js +3 -0
  38. package/lib/core.js +38 -1
  39. package/lib/cpe.js +30 -5
  40. package/lib/csaf-export.js +159 -0
  41. package/lib/cve-download.js +3 -0
  42. package/lib/cve-match.js +27 -6
  43. package/lib/cve-report.js +157 -28
  44. package/lib/dep-record.js +15 -2
  45. package/lib/deps-descriptor.js +113 -0
  46. package/lib/epss.js +115 -0
  47. package/lib/gate.js +45 -0
  48. package/lib/json-export.js +110 -0
  49. package/lib/kev.js +88 -0
  50. package/lib/license-policy.js +110 -0
  51. package/lib/maven-license.js +52 -0
  52. package/lib/maven-repo.js +3 -0
  53. package/lib/maven-version.js +8 -3
  54. package/lib/nvd.js +17 -6
  55. package/lib/osv.js +75 -23
  56. package/lib/outdated.js +10 -2
  57. package/lib/priority.js +90 -0
  58. package/lib/purl.js +77 -0
  59. package/lib/retire.js +80 -13
  60. package/lib/sarif-export.js +134 -0
  61. package/lib/sbom-export.js +153 -0
  62. package/lib/scan-completeness.js +3 -0
  63. package/lib/snyk.js +3 -0
  64. package/lib/suppress.js +113 -0
  65. package/lib/transitive.js +6 -3
  66. package/lib/ui.js +90 -0
  67. package/package.json +49 -2
  68. package/CLAUDE.md +0 -147
  69. package/docs/ARCHITECTURE.md +0 -182
  70. package/docs/USAGE.md +0 -187
  71. package/docs/superpowers/plans/2026-05-29-codec-composer.md +0 -556
  72. package/docs/superpowers/plans/2026-05-29-codec-foundation.md +0 -851
  73. package/docs/superpowers/plans/2026-05-29-codec-nuget.md +0 -432
  74. package/docs/superpowers/plans/2026-05-29-codec-pypi.md +0 -450
  75. package/docs/superpowers/specs/2026-05-29-codecs-multi-ecosystem-design.md +0 -251
  76. package/test/cli-ecosystem.test.js +0 -30
  77. package/test/codec-edge-cases.test.js +0 -131
  78. package/test/codec-integration.test.js +0 -119
  79. package/test/codecs.test.js +0 -92
  80. package/test/composer.test.js +0 -71
  81. package/test/core.test.js +0 -153
  82. package/test/cpe.test.js +0 -166
  83. package/test/cve-download.test.js +0 -39
  84. package/test/cve-match.test.js +0 -217
  85. package/test/cve-report.test.js +0 -180
  86. package/test/dep-record.test.js +0 -31
  87. package/test/fixtures/complex-enterprise/api/pom.xml +0 -32
  88. package/test/fixtures/complex-enterprise/build/pom.xml +0 -46
  89. package/test/fixtures/complex-enterprise/dao/pom.xml +0 -37
  90. package/test/fixtures/complex-enterprise/pom.xml +0 -66
  91. package/test/fixtures/complex-enterprise/web/pom.xml +0 -77
  92. package/test/fixtures/csharp-config/packages.config +0 -4
  93. package/test/fixtures/csharp-csproj/Directory.Packages.props +0 -5
  94. package/test/fixtures/csharp-csproj/app.csproj +0 -7
  95. package/test/fixtures/csharp-lock/packages.lock.json +0 -6
  96. package/test/fixtures/cve-samples/cve-non-java.json +0 -19
  97. package/test/fixtures/cve-samples/cve-product-only.json +0 -31
  98. package/test/fixtures/cve-samples/cve-with-packagename.json +0 -37
  99. package/test/fixtures/cve-samples/nvd-log4shell.json +0 -40
  100. package/test/fixtures/cve-samples/nvd-npm-lodash.json +0 -22
  101. package/test/fixtures/monorepo-mixed/libs/common-bom/pom.xml +0 -26
  102. package/test/fixtures/monorepo-mixed/packages/cli/package.json +0 -14
  103. package/test/fixtures/monorepo-mixed/packages/cli/yarn.lock +0 -41
  104. package/test/fixtures/monorepo-mixed/packages/no-lock/package.json +0 -10
  105. package/test/fixtures/monorepo-mixed/packages/web-app/package-lock.json +0 -71
  106. package/test/fixtures/monorepo-mixed/packages/web-app/package.json +0 -17
  107. package/test/fixtures/monorepo-mixed/pom.xml +0 -29
  108. package/test/fixtures/monorepo-mixed/services/api/pom.xml +0 -27
  109. package/test/fixtures/monorepo-mixed/services/worker/pom.xml +0 -28
  110. package/test/fixtures/php-app/composer.json +0 -5
  111. package/test/fixtures/php-app/composer.lock +0 -10
  112. package/test/fixtures/polyglot/cs/packages.lock.json +0 -1
  113. package/test/fixtures/polyglot/js/package-lock.json +0 -1
  114. package/test/fixtures/polyglot/js/package.json +0 -1
  115. package/test/fixtures/polyglot/mvn/pom.xml +0 -7
  116. package/test/fixtures/polyglot/php/composer.lock +0 -1
  117. package/test/fixtures/polyglot/py/poetry.lock +0 -3
  118. package/test/fixtures/private-lib-detection/core/pom.xml +0 -26
  119. package/test/fixtures/private-lib-detection/plugin/pom.xml +0 -23
  120. package/test/fixtures/private-lib-detection/pom.xml +0 -35
  121. package/test/fixtures/python-pipenv/Pipfile.lock +0 -1
  122. package/test/fixtures/python-poetry/poetry.lock +0 -7
  123. package/test/fixtures/python-reqs/requirements.txt +0 -5
  124. package/test/fixtures/python-uv/uv.lock +0 -3
  125. package/test/fixtures/simple/app/pom.xml +0 -28
  126. package/test/fixtures/simple/lib/pom.xml +0 -18
  127. package/test/fixtures/simple/pom.xml +0 -24
  128. package/test/maven-repo.test.js +0 -111
  129. package/test/maven-version.test.js +0 -57
  130. package/test/monorepo.test.js +0 -132
  131. package/test/npm-registry.test.js +0 -64
  132. package/test/npm.test.js +0 -150
  133. package/test/nuget.test.js +0 -66
  134. package/test/osv.test.js +0 -62
  135. package/test/outdated.test.js +0 -101
  136. package/test/pypi.test.js +0 -72
  137. package/test/snyk.test.js +0 -64
  138. package/test/transitive.test.js +0 -305
  139. 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 };
@@ -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");
@@ -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.0.0",
4
- "description": "Scans ALL Maven, npm, Yarn, Composer, Python, C#/.NET in a source tree ONE SHOT Alone like a big boy, 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,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",