@cyclonedx/cdxgen 12.1.5 → 12.2.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 (181) hide show
  1. package/README.md +47 -39
  2. package/bin/cdxgen.js +175 -96
  3. package/bin/evinse.js +4 -4
  4. package/bin/repl.js +1 -1
  5. package/bin/sign.js +102 -0
  6. package/bin/validate.js +233 -0
  7. package/bin/verify.js +69 -28
  8. package/data/queries.json +1 -1
  9. package/data/rules/ci-permissions.yaml +186 -0
  10. package/data/rules/dependency-sources.yaml +123 -0
  11. package/data/rules/package-integrity.yaml +135 -0
  12. package/data/rules/vscode-extensions.yaml +228 -0
  13. package/lib/cli/index.js +327 -372
  14. package/lib/evinser/db.js +137 -0
  15. package/lib/{helpers → evinser}/db.poku.js +2 -6
  16. package/lib/evinser/evinser.js +2 -14
  17. package/lib/helpers/bomSigner.js +312 -0
  18. package/lib/helpers/bomSigner.poku.js +156 -0
  19. package/lib/helpers/ciParsers/azurePipelines.js +295 -0
  20. package/lib/helpers/ciParsers/azurePipelines.poku.js +253 -0
  21. package/lib/helpers/ciParsers/circleCi.js +286 -0
  22. package/lib/helpers/ciParsers/circleCi.poku.js +230 -0
  23. package/lib/helpers/ciParsers/common.js +24 -0
  24. package/lib/helpers/ciParsers/githubActions.js +636 -0
  25. package/lib/helpers/ciParsers/githubActions.poku.js +802 -0
  26. package/lib/helpers/ciParsers/gitlabCi.js +213 -0
  27. package/lib/helpers/ciParsers/gitlabCi.poku.js +247 -0
  28. package/lib/helpers/ciParsers/jenkins.js +181 -0
  29. package/lib/helpers/ciParsers/jenkins.poku.js +197 -0
  30. package/lib/helpers/depsUtils.js +203 -0
  31. package/lib/helpers/depsUtils.poku.js +150 -0
  32. package/lib/helpers/display.js +423 -4
  33. package/lib/helpers/envcontext.js +18 -3
  34. package/lib/helpers/formulationParsers.js +351 -0
  35. package/lib/helpers/logger.js +14 -0
  36. package/lib/helpers/protobom.js +9 -9
  37. package/lib/helpers/pythonutils.js +9 -0
  38. package/lib/helpers/utils.js +681 -406
  39. package/lib/helpers/utils.poku.js +55 -255
  40. package/lib/helpers/versutils.js +202 -0
  41. package/lib/helpers/versutils.poku.js +315 -0
  42. package/lib/helpers/vsixutils.js +1061 -0
  43. package/lib/helpers/vsixutils.poku.js +2247 -0
  44. package/lib/managers/binary.js +19 -19
  45. package/lib/managers/docker.js +108 -1
  46. package/lib/managers/oci.js +10 -0
  47. package/lib/managers/piptree.js +3 -9
  48. package/lib/parsers/npmrc.js +17 -13
  49. package/lib/parsers/npmrc.poku.js +41 -5
  50. package/lib/server/openapi.yaml +1 -1
  51. package/lib/server/server.js +40 -11
  52. package/lib/server/server.poku.js +123 -144
  53. package/lib/stages/postgen/annotator.js +1 -1
  54. package/lib/stages/postgen/auditBom.js +197 -0
  55. package/lib/stages/postgen/auditBom.poku.js +378 -0
  56. package/lib/stages/postgen/postgen.js +54 -1
  57. package/lib/stages/postgen/postgen.poku.js +90 -1
  58. package/lib/stages/postgen/ruleEngine.js +369 -0
  59. package/lib/stages/pregen/envAudit.js +299 -0
  60. package/lib/stages/pregen/envAudit.poku.js +572 -0
  61. package/lib/stages/pregen/pregen.js +12 -8
  62. package/lib/{helpers/validator.js → validator/bomValidator.js} +107 -47
  63. package/lib/validator/complianceEngine.js +241 -0
  64. package/lib/validator/complianceEngine.poku.js +168 -0
  65. package/lib/validator/complianceRules.js +1610 -0
  66. package/lib/validator/complianceRules.poku.js +328 -0
  67. package/lib/validator/index.js +222 -0
  68. package/lib/validator/index.poku.js +144 -0
  69. package/lib/validator/reporters/annotations.js +121 -0
  70. package/lib/validator/reporters/console.js +149 -0
  71. package/lib/validator/reporters/index.js +41 -0
  72. package/lib/validator/reporters/json.js +37 -0
  73. package/lib/validator/reporters/sarif.js +184 -0
  74. package/lib/validator/reporters.poku.js +150 -0
  75. package/package.json +8 -8
  76. package/types/bin/sign.d.ts +3 -0
  77. package/types/bin/sign.d.ts.map +1 -0
  78. package/types/bin/validate.d.ts +3 -0
  79. package/types/bin/validate.d.ts.map +1 -0
  80. package/types/helpers/utils.d.ts +0 -1
  81. package/types/lib/cli/index.d.ts +49 -52
  82. package/types/lib/cli/index.d.ts.map +1 -1
  83. package/types/lib/evinser/db.d.ts +34 -0
  84. package/types/lib/evinser/db.d.ts.map +1 -0
  85. package/types/lib/evinser/evinser.d.ts +63 -16
  86. package/types/lib/evinser/evinser.d.ts.map +1 -1
  87. package/types/lib/helpers/bomSigner.d.ts +27 -0
  88. package/types/lib/helpers/bomSigner.d.ts.map +1 -0
  89. package/types/lib/helpers/ciParsers/azurePipelines.d.ts +17 -0
  90. package/types/lib/helpers/ciParsers/azurePipelines.d.ts.map +1 -0
  91. package/types/lib/helpers/ciParsers/circleCi.d.ts +17 -0
  92. package/types/lib/helpers/ciParsers/circleCi.d.ts.map +1 -0
  93. package/types/lib/helpers/ciParsers/common.d.ts +11 -0
  94. package/types/lib/helpers/ciParsers/common.d.ts.map +1 -0
  95. package/types/lib/helpers/ciParsers/githubActions.d.ts +34 -0
  96. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -0
  97. package/types/lib/helpers/ciParsers/gitlabCi.d.ts +17 -0
  98. package/types/lib/helpers/ciParsers/gitlabCi.d.ts.map +1 -0
  99. package/types/lib/helpers/ciParsers/jenkins.d.ts +17 -0
  100. package/types/lib/helpers/ciParsers/jenkins.d.ts.map +1 -0
  101. package/types/lib/helpers/depsUtils.d.ts +21 -0
  102. package/types/lib/helpers/depsUtils.d.ts.map +1 -0
  103. package/types/lib/helpers/display.d.ts +111 -11
  104. package/types/lib/helpers/display.d.ts.map +1 -1
  105. package/types/lib/helpers/envcontext.d.ts +19 -7
  106. package/types/lib/helpers/envcontext.d.ts.map +1 -1
  107. package/types/lib/helpers/formulationParsers.d.ts +50 -0
  108. package/types/lib/helpers/formulationParsers.d.ts.map +1 -0
  109. package/types/lib/helpers/logger.d.ts +15 -1
  110. package/types/lib/helpers/logger.d.ts.map +1 -1
  111. package/types/lib/helpers/protobom.d.ts +2 -2
  112. package/types/lib/helpers/pythonutils.d.ts +10 -1
  113. package/types/lib/helpers/pythonutils.d.ts.map +1 -1
  114. package/types/lib/helpers/utils.d.ts +532 -128
  115. package/types/lib/helpers/utils.d.ts.map +1 -1
  116. package/types/lib/helpers/versutils.d.ts +8 -0
  117. package/types/lib/helpers/versutils.d.ts.map +1 -0
  118. package/types/lib/helpers/vsixutils.d.ts +130 -0
  119. package/types/lib/helpers/vsixutils.d.ts.map +1 -0
  120. package/types/lib/managers/docker.d.ts +12 -31
  121. package/types/lib/managers/docker.d.ts.map +1 -1
  122. package/types/lib/managers/oci.d.ts +11 -1
  123. package/types/lib/managers/oci.d.ts.map +1 -1
  124. package/types/lib/managers/piptree.d.ts.map +1 -1
  125. package/types/lib/parsers/npmrc.d.ts +4 -1
  126. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  127. package/types/lib/server/server.d.ts +21 -2
  128. package/types/lib/server/server.d.ts.map +1 -1
  129. package/types/lib/stages/postgen/auditBom.d.ts +20 -0
  130. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -0
  131. package/types/lib/stages/postgen/postgen.d.ts +8 -1
  132. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  133. package/types/lib/stages/postgen/ruleEngine.d.ts +18 -0
  134. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -0
  135. package/types/lib/stages/pregen/envAudit.d.ts +8 -0
  136. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -0
  137. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  138. package/types/lib/{helpers/validator.d.ts → validator/bomValidator.d.ts} +1 -1
  139. package/types/lib/validator/bomValidator.d.ts.map +1 -0
  140. package/types/lib/validator/complianceEngine.d.ts +66 -0
  141. package/types/lib/validator/complianceEngine.d.ts.map +1 -0
  142. package/types/lib/validator/complianceRules.d.ts +70 -0
  143. package/types/lib/validator/complianceRules.d.ts.map +1 -0
  144. package/types/lib/validator/index.d.ts +70 -0
  145. package/types/lib/validator/index.d.ts.map +1 -0
  146. package/types/lib/validator/reporters/annotations.d.ts +31 -0
  147. package/types/lib/validator/reporters/annotations.d.ts.map +1 -0
  148. package/types/lib/validator/reporters/console.d.ts +30 -0
  149. package/types/lib/validator/reporters/console.d.ts.map +1 -0
  150. package/types/lib/validator/reporters/index.d.ts +21 -0
  151. package/types/lib/validator/reporters/index.d.ts.map +1 -0
  152. package/types/lib/validator/reporters/json.d.ts +11 -0
  153. package/types/lib/validator/reporters/json.d.ts.map +1 -0
  154. package/types/lib/validator/reporters/sarif.d.ts +16 -0
  155. package/types/lib/validator/reporters/sarif.d.ts.map +1 -0
  156. package/lib/helpers/db.js +0 -162
  157. package/lib/stages/pregen/env-audit.js +0 -34
  158. package/lib/stages/pregen/env-audit.poku.js +0 -290
  159. package/types/helpers/db.d.ts +0 -35
  160. package/types/helpers/db.d.ts.map +0 -1
  161. package/types/lib/helpers/db.d.ts +0 -35
  162. package/types/lib/helpers/db.d.ts.map +0 -1
  163. package/types/lib/helpers/validator.d.ts.map +0 -1
  164. package/types/lib/stages/pregen/env-audit.d.ts +0 -2
  165. package/types/lib/stages/pregen/env-audit.d.ts.map +0 -1
  166. package/types/managers/binary.d.ts +0 -37
  167. package/types/managers/binary.d.ts.map +0 -1
  168. package/types/managers/docker.d.ts +0 -56
  169. package/types/managers/docker.d.ts.map +0 -1
  170. package/types/managers/oci.d.ts +0 -2
  171. package/types/managers/oci.d.ts.map +0 -1
  172. package/types/managers/piptree.d.ts +0 -2
  173. package/types/managers/piptree.d.ts.map +0 -1
  174. package/types/server/server.d.ts +0 -34
  175. package/types/server/server.d.ts.map +0 -1
  176. package/types/stages/postgen/annotator.d.ts +0 -27
  177. package/types/stages/postgen/annotator.d.ts.map +0 -1
  178. package/types/stages/postgen/postgen.d.ts +0 -51
  179. package/types/stages/postgen/postgen.d.ts.map +0 -1
  180. package/types/stages/pregen/pregen.d.ts +0 -59
  181. package/types/stages/pregen/pregen.d.ts.map +0 -1
@@ -0,0 +1,121 @@
1
+ /**
2
+ * CycloneDX annotation reporter — embeds findings as `annotations[]` entries
3
+ * on a copy of the input BOM. Can be reused by `bom-audit`.
4
+ *
5
+ * CycloneDX supports the annotation schema from spec version 1.5 onward.
6
+ */
7
+
8
+ import { DEBUG_MODE, getTimestamp } from "../../helpers/utils.js";
9
+
10
+ const SUPPORTED_FROM = 1.5;
11
+ const CODE_BLOCK = "```";
12
+
13
+ /**
14
+ * Render a set of findings into CycloneDX annotations.
15
+ *
16
+ * @param {Array<object>} findings Finding objects emitted by the validator or auditBom engine.
17
+ * @param {object} bomJson Full CycloneDX BOM (needed for annotator/subject wiring).
18
+ * @returns {Array<object>} CycloneDX annotation objects.
19
+ */
20
+ export function buildAnnotations(findings, bomJson) {
21
+ if (!findings?.length || !bomJson) {
22
+ return [];
23
+ }
24
+ const specVersion = Number.parseFloat(bomJson.specVersion);
25
+ if (Number.isNaN(specVersion) || specVersion < SUPPORTED_FROM) {
26
+ return [];
27
+ }
28
+ const cdxgenAnnotator =
29
+ bomJson?.metadata?.tools?.components?.filter((c) => c?.name === "cdxgen") ??
30
+ [];
31
+ if (!cdxgenAnnotator.length) {
32
+ if (DEBUG_MODE) {
33
+ console.warn(
34
+ "Cannot create audit annotations: cdxgen tool component not found in metadata",
35
+ );
36
+ }
37
+ return [];
38
+ }
39
+ const subjects = [bomJson.serialNumber];
40
+ const timestamp = getTimestamp();
41
+ return findings.map((f) => {
42
+ const properties = [
43
+ { name: "cdx:validate:engine", value: f.engine || "compliance" },
44
+ { name: "cdx:validate:ruleId", value: f.ruleId },
45
+ { name: "cdx:validate:status", value: f.status },
46
+ { name: "cdx:validate:severity", value: f.severity },
47
+ ];
48
+ if (f.standard) {
49
+ properties.push({ name: "cdx:validate:standard", value: f.standard });
50
+ }
51
+ if (f.standardRefs?.length) {
52
+ properties.push({
53
+ name: "cdx:validate:standardRefs",
54
+ value: f.standardRefs.join(","),
55
+ });
56
+ }
57
+ if (f.category) {
58
+ properties.push({ name: "cdx:validate:category", value: f.category });
59
+ }
60
+ if (f.mitigation) {
61
+ properties.push({ name: "cdx:validate:mitigation", value: f.mitigation });
62
+ }
63
+ if (f.scvsLevels?.length) {
64
+ properties.push({
65
+ name: "cdx:validate:scvsLevels",
66
+ value: f.scvsLevels.join(","),
67
+ });
68
+ }
69
+ if (f.evidence && typeof f.evidence === "object") {
70
+ for (const [key, value] of Object.entries(f.evidence)) {
71
+ properties.push({
72
+ name: `cdx:validate:evidence:${key}`,
73
+ value:
74
+ typeof value === "object" ? JSON.stringify(value) : String(value),
75
+ });
76
+ }
77
+ }
78
+ return {
79
+ subjects,
80
+ annotator: {
81
+ component: cdxgenAnnotator[0],
82
+ },
83
+ timestamp,
84
+ text: `${f.message}\n${CODE_BLOCK}\n${JSON.stringify(properties)}\n${CODE_BLOCK}`,
85
+ };
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Produce a new BOM object with findings embedded as annotations. The caller
91
+ * is responsible for writing the result to disk.
92
+ *
93
+ * @param {object} bomJson
94
+ * @param {Array<object>} findings
95
+ * @returns {object}
96
+ */
97
+ export function renderBom(bomJson, findings) {
98
+ if (!bomJson) {
99
+ return bomJson;
100
+ }
101
+ const annotations = buildAnnotations(findings, bomJson);
102
+ const next = { ...bomJson };
103
+ next.annotations = [...(bomJson.annotations || []), ...annotations];
104
+ return next;
105
+ }
106
+
107
+ /**
108
+ * Convenience wrapper matching the signature of the other reporters. The
109
+ * second argument expects `{ bomJson }` because annotations are BOM-shaped,
110
+ * not report-shaped.
111
+ *
112
+ * @param {object} report Output of validateBomAdvanced().
113
+ * @param {object} options
114
+ * @param {object} options.bomJson The BOM to annotate.
115
+ * @returns {string} JSON string of the annotated BOM.
116
+ */
117
+ export function render(report, options = {}) {
118
+ const { bomJson } = options;
119
+ const annotated = renderBom(bomJson, report.findings || []);
120
+ return JSON.stringify(annotated, null, null);
121
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Console reporter — renders findings and benchmark scorecards as tables.
3
+ * Uses the existing `table` dependency (already a cdxgen runtime dependency).
4
+ */
5
+
6
+ import { table } from "table";
7
+
8
+ const SEVERITY_ICONS = {
9
+ critical: "⛔",
10
+ high: "🔴",
11
+ medium: "🟠",
12
+ low: "🟡",
13
+ info: "🔵",
14
+ };
15
+
16
+ /**
17
+ * @param {object} f Finding
18
+ * @returns {string}
19
+ */
20
+ function severityIcon(f) {
21
+ return SEVERITY_ICONS[f.severity] || "·";
22
+ }
23
+
24
+ /**
25
+ * Produce a human-readable summary of findings.
26
+ *
27
+ * @param {Array<object>} findings
28
+ * @param {object} [options]
29
+ * @returns {string}
30
+ */
31
+ export function formatFindings(findings, options = {}) {
32
+ if (!findings || findings.length === 0) {
33
+ return "No findings.";
34
+ }
35
+ const data = [["", "Rule", "Status", "Severity", "Standard", "Message"]];
36
+ for (const f of findings) {
37
+ data.push([
38
+ severityIcon(f),
39
+ f.ruleId,
40
+ f.status,
41
+ f.severity,
42
+ (f.standardRefs || []).join(", ") || f.standard || "",
43
+ f.message,
44
+ ]);
45
+ }
46
+ const config = {
47
+ columnDefault: { wrapWord: true },
48
+ columns: [
49
+ { width: 2 },
50
+ { width: 15 },
51
+ { width: 8 },
52
+ { width: 10 },
53
+ { width: 20 },
54
+ { width: 60 },
55
+ ],
56
+ header: {
57
+ alignment: "center",
58
+ content: options.title || "cdx-validate findings",
59
+ },
60
+ };
61
+ return table(data, config);
62
+ }
63
+
64
+ /**
65
+ * Produce a scorecard table for benchmark reports.
66
+ *
67
+ * @param {Array<object>} reports
68
+ * @returns {string}
69
+ */
70
+ export function formatBenchmarks(reports) {
71
+ if (!reports || reports.length === 0) {
72
+ return "";
73
+ }
74
+ const data = [
75
+ ["Benchmark", "Controls", "Pass", "Fail", "Manual", "Automatable score"],
76
+ ];
77
+ for (const r of reports) {
78
+ data.push([
79
+ r.name,
80
+ String(r.totalControls),
81
+ String(r.pass),
82
+ String(r.fail),
83
+ String(r.manual),
84
+ `${r.scorePct}% (${r.pass}/${r.automatable})`,
85
+ ]);
86
+ }
87
+ const config = {
88
+ columnDefault: { wrapWord: true },
89
+ columns: [
90
+ { width: 40 },
91
+ { width: 8 },
92
+ { width: 6 },
93
+ { width: 6 },
94
+ { width: 8 },
95
+ { width: 24 },
96
+ ],
97
+ header: {
98
+ alignment: "center",
99
+ content: "cdx-validate benchmark scorecards",
100
+ },
101
+ };
102
+ return table(data, config);
103
+ }
104
+
105
+ /**
106
+ * Produce a compact one-line summary for CI logs.
107
+ *
108
+ * @param {object} summary
109
+ * @returns {string}
110
+ */
111
+ export function formatSummary(summary) {
112
+ return [
113
+ `schemaValid=${summary.schemaValid}`,
114
+ `deepValid=${summary.deepValid}`,
115
+ `pass=${summary.pass}`,
116
+ `fail=${summary.fail}`,
117
+ `manual=${summary.manual}`,
118
+ `errors=${summary.errors}`,
119
+ ].join(" ");
120
+ }
121
+
122
+ /**
123
+ * Render the full report as a single string.
124
+ *
125
+ * @param {object} report Output of validateBomAdvanced().
126
+ * @returns {string}
127
+ */
128
+ export function render(report) {
129
+ const pieces = [];
130
+ pieces.push(formatSummary(report.summary));
131
+ if (report.benchmarks?.length) {
132
+ pieces.push(formatBenchmarks(report.benchmarks));
133
+ }
134
+ const actionable = report.findings.filter(
135
+ (f) => f.status === "fail" || f.severity === "critical",
136
+ );
137
+ if (actionable.length) {
138
+ pieces.push(formatFindings(actionable, { title: "Failing controls" }));
139
+ }
140
+ const manual = report.findings.filter((f) => f.status === "manual");
141
+ if (manual.length) {
142
+ pieces.push(
143
+ formatFindings(manual, {
144
+ title: `Manual review required (${manual.length})`,
145
+ }),
146
+ );
147
+ }
148
+ return pieces.filter(Boolean).join("\n");
149
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Reporter registry — dispatches to the requested reporter.
3
+ *
4
+ * Reporters are intentionally kept as independent modules so they can also be
5
+ * consumed by the BOM audit engine or future validators.
6
+ */
7
+
8
+ import * as annotations from "./annotations.js";
9
+ import * as consoleReporter from "./console.js";
10
+ import * as json from "./json.js";
11
+ import * as sarif from "./sarif.js";
12
+
13
+ /**
14
+ * Map of reporter name → module.
15
+ */
16
+ export const reporters = {
17
+ console: consoleReporter,
18
+ json,
19
+ sarif,
20
+ annotations,
21
+ };
22
+
23
+ /**
24
+ * Render a validation report using the named reporter.
25
+ *
26
+ * @param {string} name Reporter identifier.
27
+ * @param {object} report Output of validateBomAdvanced().
28
+ * @param {object} [opts] Reporter-specific options.
29
+ * @returns {string}
30
+ */
31
+ export function render(name, report, opts) {
32
+ const reporter = reporters[(name || "console").toLowerCase()];
33
+ if (!reporter) {
34
+ throw new Error(
35
+ `Unknown reporter '${name}'. Expected one of: ${Object.keys(reporters).join(", ")}.`,
36
+ );
37
+ }
38
+ return reporter.render(report, opts);
39
+ }
40
+
41
+ export { annotations, consoleReporter as console, json, sarif };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * JSON reporter — emits a stable, documented structure for programmatic use.
3
+ * No dependencies.
4
+ */
5
+
6
+ /**
7
+ * @param {object} report Output of validateBomAdvanced().
8
+ * @param {object} [_options] Unused
9
+ * @returns {string}
10
+ */
11
+ export function render(report, _options = {}) {
12
+ const payload = {
13
+ schemaValid: report.schemaValid,
14
+ deepValid: report.deepValid,
15
+ signatureVerified: report.signatureVerified ?? null,
16
+ summary: report.summary,
17
+ benchmarks: report.benchmarks || [],
18
+ findings: (report.findings || []).map((f) => ({
19
+ ruleId: f.ruleId,
20
+ name: f.name,
21
+ description: f.description,
22
+ engine: f.engine,
23
+ standard: f.standard,
24
+ standardRefs: f.standardRefs,
25
+ scvsLevels: f.scvsLevels,
26
+ category: f.category,
27
+ status: f.status,
28
+ severity: f.severity,
29
+ automatable: f.automatable,
30
+ message: f.message,
31
+ mitigation: f.mitigation,
32
+ locations: f.locations || [],
33
+ evidence: f.evidence || null,
34
+ })),
35
+ };
36
+ return JSON.stringify(payload, null, null);
37
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * SARIF 2.1.0 reporter — renders findings as a SARIF log suitable for upload
3
+ * to GitHub code scanning or any other SARIF-aware consumer.
4
+ *
5
+ * No external dependencies. Spec: https://docs.oasis-open.org/sarif/sarif/v2.1.0/
6
+ */
7
+
8
+ const SARIF_VERSION = "2.1.0";
9
+ const SARIF_SCHEMA =
10
+ "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/Schemata/sarif-schema-2.1.0.json";
11
+
12
+ /**
13
+ * Map internal severity → SARIF `level` property.
14
+ *
15
+ * @param {string} severity
16
+ * @returns {"error" | "warning" | "note"}
17
+ */
18
+ function severityToLevel(severity) {
19
+ switch (severity) {
20
+ case "critical":
21
+ case "high":
22
+ return "error";
23
+ case "medium":
24
+ return "warning";
25
+ default:
26
+ return "note";
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Convert an internal location to a SARIF physicalLocation block. Returns null
32
+ * when there is no file context — the caller will fall back to a synthetic
33
+ * location.
34
+ *
35
+ * @param {object} loc
36
+ * @returns {object | null}
37
+ */
38
+ function toSarifLocation(loc) {
39
+ if (!loc) return null;
40
+ if (loc.file) {
41
+ return {
42
+ physicalLocation: {
43
+ artifactLocation: { uri: loc.file },
44
+ },
45
+ logicalLocations: loc.bomRef
46
+ ? [{ fullyQualifiedName: loc.bomRef, kind: "package" }]
47
+ : undefined,
48
+ };
49
+ }
50
+ if (loc.purl || loc.bomRef) {
51
+ return {
52
+ logicalLocations: [
53
+ {
54
+ fullyQualifiedName: loc.purl || loc.bomRef,
55
+ kind: "package",
56
+ },
57
+ ],
58
+ };
59
+ }
60
+ return null;
61
+ }
62
+
63
+ /**
64
+ * Build the SARIF `rules` array from a catalogue of rule descriptors.
65
+ *
66
+ * @param {Array<object>} findings
67
+ * @returns {Array<object>}
68
+ */
69
+ function deriveRules(findings) {
70
+ const byId = new Map();
71
+ for (const f of findings) {
72
+ if (byId.has(f.ruleId)) continue;
73
+ byId.set(f.ruleId, {
74
+ id: f.ruleId,
75
+ name: f.name || f.ruleId,
76
+ shortDescription: { text: f.name || f.ruleId },
77
+ fullDescription: {
78
+ text: f.description || f.name || f.ruleId,
79
+ },
80
+ defaultConfiguration: { level: severityToLevel(f.severity) },
81
+ properties: {
82
+ category: f.category,
83
+ standard: f.standard,
84
+ standardRefs: f.standardRefs || [],
85
+ scvsLevels: f.scvsLevels || [],
86
+ automatable: f.automatable !== false,
87
+ engine: f.engine,
88
+ },
89
+ help: f.mitigation
90
+ ? {
91
+ text: f.mitigation,
92
+ markdown: `**Remediation:** ${f.mitigation}`,
93
+ }
94
+ : undefined,
95
+ });
96
+ }
97
+ return [...byId.values()];
98
+ }
99
+
100
+ /**
101
+ * Convert one finding into a SARIF result.
102
+ *
103
+ * @param {object} f
104
+ * @returns {object}
105
+ */
106
+ function toSarifResult(f) {
107
+ const locations = (f.locations || []).map(toSarifLocation).filter(Boolean);
108
+ if (locations.length === 0) {
109
+ locations.push({
110
+ logicalLocations: [{ fullyQualifiedName: f.ruleId, kind: "rule" }],
111
+ });
112
+ }
113
+ const result = {
114
+ ruleId: f.ruleId,
115
+ level: severityToLevel(f.severity),
116
+ message: { text: f.message || f.name || f.ruleId },
117
+ locations,
118
+ properties: {
119
+ status: f.status,
120
+ severity: f.severity,
121
+ standard: f.standard,
122
+ standardRefs: f.standardRefs || [],
123
+ scvsLevels: f.scvsLevels || [],
124
+ automatable: f.automatable !== false,
125
+ engine: f.engine,
126
+ },
127
+ };
128
+ if (f.evidence && typeof f.evidence === "object") {
129
+ result.properties.evidence = f.evidence;
130
+ }
131
+ return result;
132
+ }
133
+
134
+ /**
135
+ * Render a validation report as SARIF.
136
+ *
137
+ * @param {object} report Output of validateBomAdvanced().
138
+ * @param {object} [options]
139
+ * @param {string} [options.toolName] Override driver name.
140
+ * @param {string} [options.toolVersion] Driver version to embed.
141
+ * @param {boolean} [options.includeManual] Include manual-review findings (default false).
142
+ * @returns {string}
143
+ */
144
+ export function render(report, options = {}) {
145
+ const {
146
+ toolName = "cdx-validate",
147
+ toolVersion = "v12",
148
+ includeManual = false,
149
+ } = options;
150
+ const findings = (report.findings || []).filter((f) =>
151
+ includeManual ? true : f.status !== "manual",
152
+ );
153
+ const log = {
154
+ $schema: SARIF_SCHEMA,
155
+ version: SARIF_VERSION,
156
+ runs: [
157
+ {
158
+ tool: {
159
+ driver: {
160
+ name: toolName,
161
+ version: toolVersion,
162
+ informationUri: "https://cdxgen.github.io/cdxgen/",
163
+ rules: deriveRules(findings),
164
+ },
165
+ },
166
+ invocations: [
167
+ {
168
+ executionSuccessful:
169
+ report.summary?.fail === 0 && report.schemaValid !== false,
170
+ },
171
+ ],
172
+ results: findings.map(toSarifResult),
173
+ properties: {
174
+ schemaValid: report.schemaValid,
175
+ deepValid: report.deepValid,
176
+ signatureVerified: report.signatureVerified ?? null,
177
+ summary: report.summary,
178
+ benchmarks: report.benchmarks || [],
179
+ },
180
+ },
181
+ ],
182
+ };
183
+ return JSON.stringify(log, null, null);
184
+ }
@@ -0,0 +1,150 @@
1
+ import { assert, describe, it } from "poku";
2
+
3
+ import { buildAnnotations } from "./reporters/annotations.js";
4
+ import { render } from "./reporters/index.js";
5
+
6
+ function sampleReport() {
7
+ return {
8
+ schemaValid: true,
9
+ deepValid: true,
10
+ signatureVerified: null,
11
+ summary: {
12
+ total: 2,
13
+ pass: 0,
14
+ fail: 1,
15
+ manual: 1,
16
+ errors: 1,
17
+ warnings: 0,
18
+ schemaValid: true,
19
+ deepValid: true,
20
+ },
21
+ benchmarks: [
22
+ {
23
+ id: "scvs-l1",
24
+ name: "OWASP SCVS Level 1",
25
+ standard: "SCVS",
26
+ totalControls: 2,
27
+ pass: 0,
28
+ fail: 1,
29
+ manual: 1,
30
+ automatable: 1,
31
+ scorePct: 0,
32
+ controls: [],
33
+ },
34
+ ],
35
+ findings: [
36
+ {
37
+ engine: "compliance",
38
+ ruleId: "SCVS-2.4",
39
+ name: "SBOM is signed",
40
+ description: "SBOM must be signed.",
41
+ standard: "SCVS",
42
+ standardRefs: ["SCVS-2.4"],
43
+ scvsLevels: ["L2", "L3"],
44
+ category: "compliance-scvs",
45
+ status: "fail",
46
+ severity: "high",
47
+ automatable: true,
48
+ message: "BOM is not signed.",
49
+ mitigation: "Use cdx-sign.",
50
+ locations: [{ file: "bom.json" }],
51
+ evidence: { reason: "no-signature" },
52
+ },
53
+ {
54
+ engine: "compliance",
55
+ ruleId: "SCVS-1.5",
56
+ name: "Manual procurement check",
57
+ description: "Manual.",
58
+ standard: "SCVS",
59
+ standardRefs: ["SCVS-1.5"],
60
+ scvsLevels: ["L2", "L3"],
61
+ category: "compliance-scvs",
62
+ status: "manual",
63
+ severity: "info",
64
+ automatable: false,
65
+ message: "Manual review.",
66
+ locations: [],
67
+ evidence: null,
68
+ },
69
+ ],
70
+ };
71
+ }
72
+
73
+ describe("reporter dispatcher", () => {
74
+ it("throws for unknown reporter", () => {
75
+ assert.throws(() => render("xml", sampleReport()), /Unknown reporter/);
76
+ });
77
+
78
+ it("console reporter renders a non-empty string", () => {
79
+ const out = render("console", sampleReport());
80
+ assert.ok(typeof out === "string");
81
+ assert.match(out, /cdx-validate/);
82
+ assert.match(out, /SCVS-2\.4/);
83
+ });
84
+
85
+ it("json reporter emits stable schema", () => {
86
+ const out = render("json", sampleReport());
87
+ const parsed = JSON.parse(out);
88
+ assert.strictEqual(parsed.schemaValid, true);
89
+ assert.strictEqual(parsed.benchmarks.length, 1);
90
+ assert.strictEqual(parsed.findings.length, 2);
91
+ assert.strictEqual(parsed.findings[0].ruleId, "SCVS-2.4");
92
+ });
93
+
94
+ it("sarif reporter emits valid 2.1.0 structure", () => {
95
+ const out = render("sarif", sampleReport(), {
96
+ toolName: "cdx-validate",
97
+ toolVersion: "1.2.3",
98
+ });
99
+ const parsed = JSON.parse(out);
100
+ assert.strictEqual(parsed.version, "2.1.0");
101
+ assert.strictEqual(parsed.runs[0].tool.driver.name, "cdx-validate");
102
+ assert.strictEqual(parsed.runs[0].tool.driver.version, "1.2.3");
103
+ // Manual findings are hidden by default.
104
+ assert.strictEqual(parsed.runs[0].results.length, 1);
105
+ assert.strictEqual(parsed.runs[0].results[0].ruleId, "SCVS-2.4");
106
+ assert.strictEqual(parsed.runs[0].results[0].level, "error");
107
+ // Driver rules must be unique and reference the only remaining finding.
108
+ assert.strictEqual(parsed.runs[0].tool.driver.rules.length, 1);
109
+ });
110
+
111
+ it("sarif reporter can include manual findings when requested", () => {
112
+ const out = render("sarif", sampleReport(), { includeManual: true });
113
+ const parsed = JSON.parse(out);
114
+ assert.strictEqual(parsed.runs[0].results.length, 2);
115
+ });
116
+
117
+ it("annotations reporter returns the BOM with annotations appended", () => {
118
+ const bomJson = {
119
+ bomFormat: "CycloneDX",
120
+ specVersion: "1.6",
121
+ serialNumber: "urn:uuid:1b671687-395b-41f5-a30f-a58921a69b79",
122
+ metadata: {
123
+ tools: {
124
+ components: [
125
+ { type: "application", name: "cdxgen", version: "12.0.0" },
126
+ ],
127
+ },
128
+ component: { name: "demo", "bom-ref": "demo", type: "application" },
129
+ },
130
+ components: [{ name: "demo", "bom-ref": "demo", type: "library" }],
131
+ };
132
+ const out = render("annotations", sampleReport(), { bomJson });
133
+ const parsed = JSON.parse(out);
134
+ assert.ok(Object.keys(parsed?.annotations));
135
+ assert.strictEqual(parsed.annotations.length, 2);
136
+ const first = parsed.annotations[0];
137
+ assert.ok(first.subjects[0].includes(bomJson.serialNumber));
138
+ assert.ok(first.annotator);
139
+ });
140
+
141
+ it("annotations reporter skips when spec version is below 1.5", () => {
142
+ const bomJson = {
143
+ bomFormat: "CycloneDX",
144
+ specVersion: "1.4",
145
+ metadata: { component: { name: "old" } },
146
+ };
147
+ const ann = buildAnnotations(sampleReport().findings, bomJson);
148
+ assert.strictEqual(Object.keys(ann).length, 0);
149
+ });
150
+ });