@cyclonedx/cdxgen 12.2.0 → 12.3.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 +242 -90
  2. package/bin/audit.js +191 -0
  3. package/bin/cdxgen.js +532 -168
  4. package/bin/convert.js +99 -0
  5. package/bin/evinse.js +23 -0
  6. package/bin/repl.js +339 -8
  7. package/bin/sign.js +8 -0
  8. package/bin/validate.js +8 -0
  9. package/bin/verify.js +8 -0
  10. package/data/container-knowledge-index.json +125 -0
  11. package/data/gtfobins-index.json +6296 -0
  12. package/data/lolbas-index.json +150 -0
  13. package/data/queries-darwin.json +63 -3
  14. package/data/queries-win.json +45 -3
  15. package/data/queries.json +74 -2
  16. package/data/rules/chrome-extensions.yaml +240 -0
  17. package/data/rules/ci-permissions.yaml +478 -18
  18. package/data/rules/container-risk.yaml +270 -0
  19. package/data/rules/obom-runtime.yaml +891 -0
  20. package/data/rules/package-integrity.yaml +49 -0
  21. package/data/spdx-export.schema.json +6794 -0
  22. package/data/spdx-model-v3.0.1.jsonld +15999 -0
  23. package/lib/audit/index.js +1924 -0
  24. package/lib/audit/index.poku.js +1488 -0
  25. package/lib/audit/progress.js +137 -0
  26. package/lib/audit/progress.poku.js +188 -0
  27. package/lib/audit/reporters.js +618 -0
  28. package/lib/audit/scoring.js +310 -0
  29. package/lib/audit/scoring.poku.js +341 -0
  30. package/lib/audit/targets.js +260 -0
  31. package/lib/audit/targets.poku.js +331 -0
  32. package/lib/cli/index.js +276 -68
  33. package/lib/cli/index.poku.js +368 -0
  34. package/lib/helpers/analyzer.js +1052 -5
  35. package/lib/helpers/analyzer.poku.js +301 -0
  36. package/lib/helpers/annotationFormatter.js +49 -0
  37. package/lib/helpers/annotationFormatter.poku.js +44 -0
  38. package/lib/helpers/bomUtils.js +36 -0
  39. package/lib/helpers/bomUtils.poku.js +51 -0
  40. package/lib/helpers/caxa.js +2 -2
  41. package/lib/helpers/chromextutils.js +1153 -0
  42. package/lib/helpers/chromextutils.poku.js +493 -0
  43. package/lib/helpers/ciParsers/githubActions.js +1632 -45
  44. package/lib/helpers/ciParsers/githubActions.poku.js +853 -1
  45. package/lib/helpers/containerRisk.js +186 -0
  46. package/lib/helpers/containerRisk.poku.js +52 -0
  47. package/lib/helpers/depsUtils.js +16 -0
  48. package/lib/helpers/depsUtils.poku.js +58 -1
  49. package/lib/helpers/display.js +245 -61
  50. package/lib/helpers/display.poku.js +162 -2
  51. package/lib/helpers/exportUtils.js +123 -0
  52. package/lib/helpers/exportUtils.poku.js +60 -0
  53. package/lib/helpers/formulationParsers.js +69 -0
  54. package/lib/helpers/formulationParsers.poku.js +44 -0
  55. package/lib/helpers/gtfobins.js +189 -0
  56. package/lib/helpers/gtfobins.poku.js +49 -0
  57. package/lib/helpers/lolbas.js +267 -0
  58. package/lib/helpers/lolbas.poku.js +39 -0
  59. package/lib/helpers/osqueryTransform.js +84 -0
  60. package/lib/helpers/osqueryTransform.poku.js +49 -0
  61. package/lib/helpers/provenanceUtils.js +193 -0
  62. package/lib/helpers/provenanceUtils.poku.js +145 -0
  63. package/lib/helpers/pylockutils.js +281 -0
  64. package/lib/helpers/pylockutils.poku.js +48 -0
  65. package/lib/helpers/registryProvenance.js +793 -0
  66. package/lib/helpers/registryProvenance.poku.js +452 -0
  67. package/lib/helpers/remote/dependency-track.js +84 -0
  68. package/lib/helpers/remote/dependency-track.poku.js +119 -0
  69. package/lib/helpers/source.js +1267 -0
  70. package/lib/helpers/source.poku.js +771 -0
  71. package/lib/helpers/spdxUtils.js +97 -0
  72. package/lib/helpers/spdxUtils.poku.js +70 -0
  73. package/lib/helpers/table.js +384 -0
  74. package/lib/helpers/table.poku.js +186 -0
  75. package/lib/helpers/unicodeScan.js +147 -0
  76. package/lib/helpers/unicodeScan.poku.js +45 -0
  77. package/lib/helpers/utils.js +882 -136
  78. package/lib/helpers/utils.poku.js +995 -91
  79. package/lib/managers/binary.js +29 -5
  80. package/lib/managers/docker.js +179 -52
  81. package/lib/managers/docker.poku.js +327 -28
  82. package/lib/managers/oci.js +107 -23
  83. package/lib/managers/oci.poku.js +132 -0
  84. package/lib/server/openapi.yaml +50 -0
  85. package/lib/server/server.js +228 -331
  86. package/lib/server/server.poku.js +220 -5
  87. package/lib/stages/postgen/annotator.js +7 -0
  88. package/lib/stages/postgen/annotator.poku.js +40 -0
  89. package/lib/stages/postgen/auditBom.js +20 -5
  90. package/lib/stages/postgen/auditBom.poku.js +1729 -67
  91. package/lib/stages/postgen/postgen.js +40 -0
  92. package/lib/stages/postgen/postgen.poku.js +47 -0
  93. package/lib/stages/postgen/ruleEngine.js +80 -2
  94. package/lib/stages/postgen/spdxConverter.js +796 -0
  95. package/lib/stages/postgen/spdxConverter.poku.js +341 -0
  96. package/lib/validator/bomValidator.js +232 -0
  97. package/lib/validator/bomValidator.poku.js +70 -0
  98. package/lib/validator/complianceRules.js +70 -7
  99. package/lib/validator/complianceRules.poku.js +30 -0
  100. package/lib/validator/reporters/annotations.js +2 -2
  101. package/lib/validator/reporters/console.js +13 -2
  102. package/lib/validator/reporters.poku.js +13 -0
  103. package/package.json +10 -8
  104. package/types/bin/audit.d.ts +3 -0
  105. package/types/bin/audit.d.ts.map +1 -0
  106. package/types/bin/convert.d.ts +3 -0
  107. package/types/bin/convert.d.ts.map +1 -0
  108. package/types/bin/repl.d.ts.map +1 -1
  109. package/types/lib/audit/index.d.ts +115 -0
  110. package/types/lib/audit/index.d.ts.map +1 -0
  111. package/types/lib/audit/progress.d.ts +27 -0
  112. package/types/lib/audit/progress.d.ts.map +1 -0
  113. package/types/lib/audit/reporters.d.ts +35 -0
  114. package/types/lib/audit/reporters.d.ts.map +1 -0
  115. package/types/lib/audit/scoring.d.ts +35 -0
  116. package/types/lib/audit/scoring.d.ts.map +1 -0
  117. package/types/lib/audit/targets.d.ts +63 -0
  118. package/types/lib/audit/targets.d.ts.map +1 -0
  119. package/types/lib/cli/index.d.ts +8 -0
  120. package/types/lib/cli/index.d.ts.map +1 -1
  121. package/types/lib/helpers/analyzer.d.ts +13 -0
  122. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  123. package/types/lib/helpers/annotationFormatter.d.ts +23 -0
  124. package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
  125. package/types/lib/helpers/bomUtils.d.ts +5 -0
  126. package/types/lib/helpers/bomUtils.d.ts.map +1 -0
  127. package/types/lib/helpers/chromextutils.d.ts +97 -0
  128. package/types/lib/helpers/chromextutils.d.ts.map +1 -0
  129. package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
  130. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  131. package/types/lib/helpers/containerRisk.d.ts +17 -0
  132. package/types/lib/helpers/containerRisk.d.ts.map +1 -0
  133. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  134. package/types/lib/helpers/display.d.ts +4 -1
  135. package/types/lib/helpers/display.d.ts.map +1 -1
  136. package/types/lib/helpers/exportUtils.d.ts +40 -0
  137. package/types/lib/helpers/exportUtils.d.ts.map +1 -0
  138. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  139. package/types/lib/helpers/gtfobins.d.ts +17 -0
  140. package/types/lib/helpers/gtfobins.d.ts.map +1 -0
  141. package/types/lib/helpers/lolbas.d.ts +16 -0
  142. package/types/lib/helpers/lolbas.d.ts.map +1 -0
  143. package/types/lib/helpers/osqueryTransform.d.ts +7 -0
  144. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
  145. package/types/lib/helpers/provenanceUtils.d.ts +90 -0
  146. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
  147. package/types/lib/helpers/pylockutils.d.ts +51 -0
  148. package/types/lib/helpers/pylockutils.d.ts.map +1 -0
  149. package/types/lib/helpers/registryProvenance.d.ts +17 -0
  150. package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
  151. package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
  152. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
  153. package/types/lib/helpers/source.d.ts +141 -0
  154. package/types/lib/helpers/source.d.ts.map +1 -0
  155. package/types/lib/helpers/spdxUtils.d.ts +2 -0
  156. package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
  157. package/types/lib/helpers/table.d.ts +6 -0
  158. package/types/lib/helpers/table.d.ts.map +1 -0
  159. package/types/lib/helpers/unicodeScan.d.ts +46 -0
  160. package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
  161. package/types/lib/helpers/utils.d.ts +30 -11
  162. package/types/lib/helpers/utils.d.ts.map +1 -1
  163. package/types/lib/managers/binary.d.ts.map +1 -1
  164. package/types/lib/managers/docker.d.ts.map +1 -1
  165. package/types/lib/managers/oci.d.ts.map +1 -1
  166. package/types/lib/server/server.d.ts +0 -35
  167. package/types/lib/server/server.d.ts.map +1 -1
  168. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  169. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  170. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  171. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  172. package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
  173. package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
  174. package/types/lib/validator/bomValidator.d.ts +1 -0
  175. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  176. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  177. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  178. package/types/bin/dependencies.d.ts +0 -3
  179. package/types/bin/dependencies.d.ts.map +0 -1
  180. package/types/bin/licenses.d.ts +0 -3
  181. package/types/bin/licenses.d.ts.map +0 -1
@@ -0,0 +1,618 @@
1
+ import { buildAnnotationText } from "../helpers/annotationFormatter.js";
2
+ import { table } from "../helpers/table.js";
3
+ import { getTimestamp } from "../helpers/utils.js";
4
+ import { severityMeetsThreshold } from "./scoring.js";
5
+
6
+ const SARIF_VERSION = "2.1.0";
7
+ const SARIF_SCHEMA =
8
+ "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/Schemata/sarif-schema-2.1.0.json";
9
+ const AUDIT_ERROR_RULE_ID = "AUDIT-ERROR";
10
+
11
+ /**
12
+ * Filter results by final severity threshold.
13
+ *
14
+ * @param {object[]} results results list
15
+ * @param {string} minSeverity threshold severity
16
+ * @returns {object[]} filtered results
17
+ */
18
+ function filterResults(results, minSeverity) {
19
+ return results.filter((result) =>
20
+ severityMeetsThreshold(result?.assessment?.severity || "none", minSeverity),
21
+ );
22
+ }
23
+
24
+ function effectiveResults(report) {
25
+ return report.groupedResults?.length
26
+ ? report.groupedResults
27
+ : report.results || [];
28
+ }
29
+
30
+ function severityToSarifLevel(severity) {
31
+ switch (severity) {
32
+ case "critical":
33
+ case "high":
34
+ return "error";
35
+ case "medium":
36
+ return "warning";
37
+ default:
38
+ return "note";
39
+ }
40
+ }
41
+
42
+ function splitCsv(value) {
43
+ return String(value || "")
44
+ .split(",")
45
+ .map((entry) => entry.trim())
46
+ .filter(Boolean);
47
+ }
48
+
49
+ function extractLocalDispatchEdge(finding) {
50
+ const senderFile = finding?.location?.file;
51
+ const receiverFiles = splitCsv(finding?.evidence?.localReceiverWorkflowFiles);
52
+ const receiverNames = splitCsv(finding?.evidence?.localReceiverWorkflowNames);
53
+ const matchBasis = splitCsv(finding?.evidence?.localReceiverMatchBasis);
54
+ const hasLocalDispatchReceiver =
55
+ finding?.evidence?.hasLocalDispatchReceiver === "true" ||
56
+ receiverFiles.length > 0 ||
57
+ receiverNames.length > 0;
58
+ if (!senderFile || !hasLocalDispatchReceiver) {
59
+ return undefined;
60
+ }
61
+ return {
62
+ matchBasis,
63
+ receiverFiles,
64
+ receiverNames,
65
+ senderFile,
66
+ };
67
+ }
68
+
69
+ function formatLocalDispatchEdge(edge) {
70
+ if (!edge) {
71
+ return undefined;
72
+ }
73
+ const receiverLabel = edge.receiverNames[0] || edge.receiverFiles[0];
74
+ if (!receiverLabel) {
75
+ return undefined;
76
+ }
77
+ return `${edge.senderFile} -> ${receiverLabel}`;
78
+ }
79
+
80
+ function targetSarifLocations(result, findingLocation) {
81
+ const bomRef =
82
+ findingLocation?.bomRef ||
83
+ result?.target?.bomRefs?.[0] ||
84
+ result?.target?.purl ||
85
+ result?.grouping?.label;
86
+ if (findingLocation?.file) {
87
+ return [
88
+ {
89
+ physicalLocation: {
90
+ artifactLocation: {
91
+ uri: findingLocation.file,
92
+ },
93
+ },
94
+ logicalLocations: bomRef
95
+ ? [{ fullyQualifiedName: bomRef, kind: "package" }]
96
+ : undefined,
97
+ },
98
+ ];
99
+ }
100
+ if (bomRef) {
101
+ return [
102
+ {
103
+ logicalLocations: [{ fullyQualifiedName: bomRef, kind: "package" }],
104
+ },
105
+ ];
106
+ }
107
+ return [
108
+ {
109
+ logicalLocations: [{ fullyQualifiedName: "cdx-audit", kind: "tool" }],
110
+ },
111
+ ];
112
+ }
113
+
114
+ function resultProperties(result) {
115
+ const properties = {
116
+ auditSeverity: result?.assessment?.severity || "none",
117
+ confidence: result?.assessment?.confidenceLabel,
118
+ reasons: result?.assessment?.reasons || [],
119
+ score: result?.assessment?.score,
120
+ status: result?.status,
121
+ target: {
122
+ bomRefs: result?.target?.bomRefs || [],
123
+ name: result?.target?.name,
124
+ namespace: result?.target?.namespace,
125
+ purl: result?.target?.purl,
126
+ type: result?.target?.type,
127
+ version: result?.target?.version,
128
+ },
129
+ };
130
+ if (result?.grouping) {
131
+ properties.grouping = result.grouping;
132
+ }
133
+ if (result?.repoUrl) {
134
+ properties.repoUrl = result.repoUrl;
135
+ }
136
+ if (result?.sourceDirectoryConfidence) {
137
+ properties.sourceDirectoryConfidence = result.sourceDirectoryConfidence;
138
+ }
139
+ return properties;
140
+ }
141
+
142
+ function findingProperties(finding) {
143
+ const properties = {
144
+ attackTactics: finding?.attackTactics,
145
+ attackTechniques: finding?.attackTechniques,
146
+ category: finding?.category,
147
+ mitigation: finding?.mitigation,
148
+ severity: finding?.severity,
149
+ tags: attackTags(finding),
150
+ };
151
+ const localDispatchEdge = extractLocalDispatchEdge(finding);
152
+ if (localDispatchEdge) {
153
+ properties.localDispatchEdge = formatLocalDispatchEdge(localDispatchEdge);
154
+ properties.localDispatchReceiverFiles = localDispatchEdge.receiverFiles;
155
+ properties.localDispatchReceiverNames = localDispatchEdge.receiverNames;
156
+ properties.localDispatchMatchBasis = localDispatchEdge.matchBasis;
157
+ }
158
+ return properties;
159
+ }
160
+
161
+ function sarifRelatedLocations(finding) {
162
+ const localDispatchEdge = extractLocalDispatchEdge(finding);
163
+ if (!localDispatchEdge?.receiverFiles?.length) {
164
+ return undefined;
165
+ }
166
+ return localDispatchEdge.receiverFiles.map((receiverFile, index) => ({
167
+ id: index + 1,
168
+ logicalLocations: localDispatchEdge.receiverNames[index]
169
+ ? [
170
+ {
171
+ fullyQualifiedName: localDispatchEdge.receiverNames[index],
172
+ kind: "function",
173
+ },
174
+ ]
175
+ : undefined,
176
+ message: {
177
+ text: `Local dispatch receiver: ${
178
+ localDispatchEdge.receiverNames[index] || receiverFile
179
+ }`,
180
+ },
181
+ physicalLocation: {
182
+ artifactLocation: {
183
+ uri: receiverFile,
184
+ },
185
+ },
186
+ }));
187
+ }
188
+
189
+ function sarifHelp(finding, result) {
190
+ const helpText = [];
191
+ if (finding?.mitigation) {
192
+ helpText.push(finding.mitigation);
193
+ }
194
+ const upstreamEscalation = summarizeUpstreamEscalation(result);
195
+ if (upstreamEscalation) {
196
+ helpText.push(upstreamEscalation);
197
+ }
198
+ if (!helpText.length) {
199
+ return undefined;
200
+ }
201
+ return {
202
+ markdown: helpText
203
+ .map((entry, index) =>
204
+ index === 0
205
+ ? `**Remediation:** ${entry}`
206
+ : `**External maintainer path:** ${entry}`,
207
+ )
208
+ .join("\n\n"),
209
+ text: helpText.join(" "),
210
+ };
211
+ }
212
+
213
+ function attackTags(finding) {
214
+ return [
215
+ ...(finding?.attackTactics || []),
216
+ ...(finding?.attackTechniques || []),
217
+ ].map((id) => `ATT&CK:${id}`);
218
+ }
219
+
220
+ function deriveSarifRules(entries) {
221
+ const rulesById = new Map();
222
+ for (const entry of entries) {
223
+ const finding = entry.finding;
224
+ const result = entry.result;
225
+ if (rulesById.has(finding.ruleId)) {
226
+ continue;
227
+ }
228
+ rulesById.set(finding.ruleId, {
229
+ id: finding.ruleId,
230
+ name: finding.name || finding.ruleId,
231
+ shortDescription: {
232
+ text: finding.name || finding.ruleId,
233
+ },
234
+ fullDescription: {
235
+ text: finding.description || finding.name || finding.ruleId,
236
+ },
237
+ defaultConfiguration: {
238
+ level: severityToSarifLevel(finding.severity),
239
+ },
240
+ properties: {
241
+ attackTactics: finding.attackTactics,
242
+ attackTechniques: finding.attackTechniques,
243
+ category: finding.category,
244
+ engine: finding.engine || "cdx-audit",
245
+ tags: attackTags(finding),
246
+ },
247
+ help: sarifHelp(finding, result),
248
+ });
249
+ }
250
+ return [...rulesById.values()];
251
+ }
252
+
253
+ function findingToSarifResult(finding, result) {
254
+ const nextAction = summarizeNextAction(result);
255
+ const upstreamEscalation = summarizeUpstreamEscalation(result);
256
+ return {
257
+ level: severityToSarifLevel(
258
+ finding?.severity || result?.assessment?.severity,
259
+ ),
260
+ locations: targetSarifLocations(result, finding?.location),
261
+ message: {
262
+ text: finding?.message || finding?.description || finding?.ruleId,
263
+ },
264
+ properties: {
265
+ ...resultProperties(result),
266
+ ...findingProperties(finding),
267
+ nextAction,
268
+ upstreamEscalation,
269
+ },
270
+ relatedLocations: sarifRelatedLocations(finding),
271
+ ruleId: finding?.ruleId || AUDIT_ERROR_RULE_ID,
272
+ };
273
+ }
274
+
275
+ function errorToSarifEntry(result) {
276
+ const severity = result?.assessment?.severity || "high";
277
+ return {
278
+ category: result?.errorType || "runtime",
279
+ description:
280
+ "cdx-audit could not complete predictive analysis for the resolved target.",
281
+ message: result?.error || "cdx-audit failed to analyze the target.",
282
+ name: "Target analysis error",
283
+ ruleId: AUDIT_ERROR_RULE_ID,
284
+ severity,
285
+ };
286
+ }
287
+
288
+ function consoleTargetLabel(result) {
289
+ if (result?.grouping?.label) {
290
+ return result.grouping.label;
291
+ }
292
+ if (result?.target?.purl) {
293
+ return result.target.purl;
294
+ }
295
+ const namespacePrefix = result?.target?.namespace
296
+ ? `${result.target.namespace}/`
297
+ : "";
298
+ const versionSuffix = result?.target?.version
299
+ ? `@${result.target.version}`
300
+ : "";
301
+ return `${result?.target?.type || "pkg"}:${namespacePrefix}${result?.target?.name || "unknown"}${versionSuffix}`;
302
+ }
303
+
304
+ function topFinding(result) {
305
+ return result?.findings?.[0];
306
+ }
307
+
308
+ function summarizeWhy(result) {
309
+ const finding = topFinding(result);
310
+ const localDispatchEdge = extractLocalDispatchEdge(finding);
311
+ if (finding?.message && localDispatchEdge) {
312
+ return `${finding.ruleId} — ${finding.message} (${formatLocalDispatchEdge(localDispatchEdge)})`;
313
+ }
314
+ if (finding?.message) {
315
+ return `${finding.ruleId} — ${finding.message}`;
316
+ }
317
+ if (result?.error) {
318
+ return result.error;
319
+ }
320
+ return (
321
+ result?.assessment?.reasons?.[0] || "Review the predictive audit details."
322
+ );
323
+ }
324
+
325
+ function groupedPurlPreview(result) {
326
+ if (!result?.grouping?.groupedPurls?.length) {
327
+ return undefined;
328
+ }
329
+ const preview = result.grouping.groupedPurls.slice(0, 2).join(", ");
330
+ return result.grouping.groupedPurls.length > 2 ? `${preview}, …` : preview;
331
+ }
332
+
333
+ function summarizeReviewFocus(result) {
334
+ const finding = topFinding(result);
335
+ const localDispatchEdge = extractLocalDispatchEdge(finding);
336
+ if (localDispatchEdge?.receiverFiles?.length) {
337
+ return `Review sender '${localDispatchEdge.senderFile}' together with receiver '${localDispatchEdge.receiverFiles[0]}' for the flagged workflow-dispatch chain.`;
338
+ }
339
+ if (finding?.location?.file && result?.repoUrl) {
340
+ return `Review '${finding.location.file}' in ${result.repoUrl}.`;
341
+ }
342
+ if (finding?.location?.file) {
343
+ return `Review '${finding.location.file}' for the flagged workflow or release step.`;
344
+ }
345
+ if (result?.grouping?.memberCount > 1) {
346
+ return `Start with ${groupedPurlPreview(result) || result.grouping.label} and inspect the shared repository or workflow pattern.`;
347
+ }
348
+ if (result?.repoUrl) {
349
+ return `Review ${result.repoUrl} for the flagged release workflow, provenance, or publish behavior.`;
350
+ }
351
+ if (finding?.location?.purl) {
352
+ return `Inspect ${finding.location.purl} in your dependency tree and verify its source and release posture.`;
353
+ }
354
+ if (result?.target?.purl) {
355
+ return `Inspect ${result.target.purl} and verify its source repository, release workflow, and provenance signals.`;
356
+ }
357
+ return "Review the reported target and verify the associated repository, workflow, or package metadata.";
358
+ }
359
+
360
+ function summarizeUpstreamEscalation(result) {
361
+ const finding = topFinding(result);
362
+ if (finding?.location?.file && result?.repoUrl) {
363
+ return `If you do not maintain this repository, open an issue or discussion with the upstream maintainers and reference '${finding.location.file}'.`;
364
+ }
365
+ if (result?.grouping?.memberCount > 1) {
366
+ return `If these dependencies are maintained externally, open an issue or discussion with the upstream maintainers and reference ${result.grouping.label}.`;
367
+ }
368
+ if (result?.target?.purl) {
369
+ return `If this dependency is maintained externally, open an issue or discussion with the upstream maintainers and reference ${result.target.purl}.`;
370
+ }
371
+ if (result?.repoUrl) {
372
+ return "If you do not maintain this repository, open an issue or discussion with the upstream maintainers and share the predictive audit finding.";
373
+ }
374
+ return undefined;
375
+ }
376
+
377
+ function summarizeNextAction(result) {
378
+ const finding = topFinding(result);
379
+ if (result?.error) {
380
+ return `${summarizeReviewFocus(result)} Verify repository access, source resolution, and clone permissions before re-running the audit.`;
381
+ }
382
+ const nextSteps = [summarizeReviewFocus(result)];
383
+ if (finding?.mitigation) {
384
+ nextSteps.push(finding.mitigation);
385
+ }
386
+ const upstreamEscalation = summarizeUpstreamEscalation(result);
387
+ if (upstreamEscalation) {
388
+ nextSteps.push(upstreamEscalation);
389
+ }
390
+ return nextSteps.join(" ");
391
+ }
392
+
393
+ function renderActionTable(results) {
394
+ const rows = [
395
+ ["Severity", "Target", "Why this needs action", "What to do next"],
396
+ ];
397
+ results.forEach((result) => {
398
+ rows.push([
399
+ result?.assessment?.severity?.toUpperCase() || "NONE",
400
+ consoleTargetLabel(result),
401
+ summarizeWhy(result),
402
+ summarizeNextAction(result),
403
+ ]);
404
+ });
405
+ return table(rows, {
406
+ columns: [{ width: 10 }, { width: 36 }, { width: 52 }, { width: 68 }],
407
+ columnDefault: { wrapWord: false },
408
+ });
409
+ }
410
+
411
+ export function renderSarifReport(report, options = {}) {
412
+ const minSeverity = options.minSeverity || "low";
413
+ const visibleResults = filterResults(effectiveResults(report), minSeverity);
414
+ const entries = [];
415
+ const sarifResults = [];
416
+ for (const result of visibleResults) {
417
+ if (result?.findings?.length) {
418
+ for (const finding of result.findings) {
419
+ entries.push({ finding, result });
420
+ sarifResults.push(findingToSarifResult(finding, result));
421
+ }
422
+ continue;
423
+ }
424
+ if (result?.error) {
425
+ const errorEntry = errorToSarifEntry(result);
426
+ entries.push({ finding: errorEntry, result });
427
+ sarifResults.push(findingToSarifResult(errorEntry, result));
428
+ }
429
+ }
430
+ const toolName = report?.tool?.name || "cdx-audit";
431
+ const toolVersion = report?.tool?.version || "v12";
432
+ const log = {
433
+ $schema: SARIF_SCHEMA,
434
+ version: SARIF_VERSION,
435
+ runs: [
436
+ {
437
+ tool: {
438
+ driver: {
439
+ informationUri: "https://cdxgen.github.io/cdxgen/",
440
+ name: toolName,
441
+ rules: deriveSarifRules(entries),
442
+ version: toolVersion,
443
+ },
444
+ },
445
+ invocations: [
446
+ {
447
+ executionSuccessful: report?.summary?.erroredTargets === 0,
448
+ },
449
+ ],
450
+ properties: {
451
+ aggregateReportFile: report?.aggregateReportFile,
452
+ generatedAt: report?.generatedAt,
453
+ inputs: report?.inputs || [],
454
+ summary: report?.summary,
455
+ },
456
+ results: sarifResults,
457
+ },
458
+ ],
459
+ };
460
+ return `${JSON.stringify(log, null, 2)}\n`;
461
+ }
462
+
463
+ /**
464
+ * Render an audit report as pretty JSON.
465
+ *
466
+ * @param {object} report aggregate report
467
+ * @returns {string} JSON output
468
+ */
469
+ export function renderJsonReport(report) {
470
+ return `${JSON.stringify(report, null, 2)}\n`;
471
+ }
472
+
473
+ /**
474
+ * Render an audit report for terminal output.
475
+ *
476
+ * @param {object} report aggregate report
477
+ * @param {object} options render options
478
+ * @returns {string} console report text
479
+ */
480
+ export function renderConsoleReport(report, options = {}) {
481
+ const minSeverity = options.minSeverity || "low";
482
+ const visibleResults = filterResults(effectiveResults(report), minSeverity);
483
+ const lines = [];
484
+ lines.push("cdx-audit — predictive supply-chain exposure audit");
485
+ lines.push("");
486
+ lines.push(`Input BOMs: ${report.summary.inputBomCount}`);
487
+ lines.push(`Candidate targets: ${report.summary.totalTargets}`);
488
+ lines.push(`Scanned targets: ${report.summary.scannedTargets}`);
489
+ lines.push(`Errored targets: ${report.summary.erroredTargets}`);
490
+ lines.push(`Skipped targets: ${report.summary.skippedTargets}`);
491
+ if (report.summary.groupedResultCount) {
492
+ lines.push(
493
+ `Consolidated alert groups: ${report.summary.groupedResultCount}`,
494
+ );
495
+ }
496
+ lines.push("");
497
+ if (!visibleResults.length) {
498
+ lines.push("No dependencies require your attention right now.");
499
+ lines.push(
500
+ `No predictive findings met or exceeded the configured severity threshold ('${minSeverity}').`,
501
+ );
502
+ return `${lines.join("\n")}\n`;
503
+ }
504
+ lines.push("Dependencies requiring your attention:");
505
+ lines.push("");
506
+ lines.push(renderActionTable(visibleResults));
507
+ lines.push("");
508
+ lines.push(
509
+ "Next step: review the file, repository, or package listed in 'What to do next'. If you maintain it, make the remediation directly; otherwise, open an upstream issue or discussion with the relevant maintainers, then re-run cdx-audit or cdxgen --bom-audit.",
510
+ );
511
+ return `${lines.join("\n")}\n`;
512
+ }
513
+
514
+ /**
515
+ * Render the requested report format.
516
+ *
517
+ * @param {string} reportType format name
518
+ * @param {object} report aggregate report
519
+ * @param {object} options render options
520
+ * @returns {string} rendered report
521
+ */
522
+ export function renderAuditReport(reportType, report, options = {}) {
523
+ if ((reportType || "console") === "json") {
524
+ return renderJsonReport(report);
525
+ }
526
+ if ((reportType || "console") === "sarif") {
527
+ return renderSarifReport(report, options);
528
+ }
529
+ return renderConsoleReport(report, options);
530
+ }
531
+
532
+ /**
533
+ * Convert predictive audit results into CycloneDX annotations.
534
+ *
535
+ * @param {object} report aggregate audit report
536
+ * @param {object} bomJson root CycloneDX BOM
537
+ * @param {object} [options] annotation options
538
+ * @returns {object[]} annotations
539
+ */
540
+ export function formatPredictiveAnnotations(report, bomJson, options = {}) {
541
+ const cdxgenAnnotator = bomJson?.metadata?.tools?.components?.find(
542
+ (component) => component.name === "cdxgen",
543
+ );
544
+ if (!cdxgenAnnotator) {
545
+ return [];
546
+ }
547
+ const minSeverity = options.minSeverity || "low";
548
+ const actionableResults = filterResults(
549
+ report.results || [],
550
+ minSeverity,
551
+ ).filter((result) => (result?.assessment?.severity || "none") !== "none");
552
+ return actionableResults.map((result) => {
553
+ const nextAction = summarizeNextAction(result);
554
+ const upstreamEscalation = summarizeUpstreamEscalation(result);
555
+ const properties = [
556
+ { name: "cdx:audit:engine", value: "cdx-audit" },
557
+ { name: "cdx:audit:severity", value: result.assessment.severity },
558
+ {
559
+ name: "cdx:audit:confidence",
560
+ value: result.assessment.confidenceLabel,
561
+ },
562
+ { name: "cdx:audit:score", value: String(result.assessment.score) },
563
+ { name: "cdx:audit:nextAction", value: nextAction },
564
+ { name: "cdx:audit:target:purl", value: result.target.purl },
565
+ ];
566
+ if (upstreamEscalation) {
567
+ properties.push({
568
+ name: "cdx:audit:upstreamGuidance",
569
+ value: upstreamEscalation,
570
+ });
571
+ }
572
+ if (result.repoUrl) {
573
+ properties.push({
574
+ name: "cdx:audit:target:repoUrl",
575
+ value: result.repoUrl,
576
+ });
577
+ }
578
+ if (result.findings?.length) {
579
+ const localDispatchEdge = extractLocalDispatchEdge(result.findings[0]);
580
+ properties.push({
581
+ name: "cdx:audit:topFinding:ruleId",
582
+ value: result.findings[0].ruleId,
583
+ });
584
+ if (localDispatchEdge) {
585
+ properties.push({
586
+ name: "cdx:audit:dispatch:edge",
587
+ value: formatLocalDispatchEdge(localDispatchEdge),
588
+ });
589
+ if (localDispatchEdge.receiverFiles.length) {
590
+ properties.push({
591
+ name: "cdx:audit:dispatch:receiverFiles",
592
+ value: localDispatchEdge.receiverFiles.join(","),
593
+ });
594
+ }
595
+ if (localDispatchEdge.receiverNames.length) {
596
+ properties.push({
597
+ name: "cdx:audit:dispatch:receiverNames",
598
+ value: localDispatchEdge.receiverNames.join(","),
599
+ });
600
+ }
601
+ }
602
+ }
603
+ return {
604
+ annotator: {
605
+ component: cdxgenAnnotator,
606
+ },
607
+ subjects: result.target.bomRefs?.length
608
+ ? result.target.bomRefs
609
+ : [bomJson.serialNumber],
610
+ text: buildAnnotationText(
611
+ `Predictive audit score ${result.assessment.score} (${result.assessment.severity}) for ${result.target.purl}.`,
612
+ properties,
613
+ [result.assessment.reasons?.[0] || "", `Next action: ${nextAction}`],
614
+ ),
615
+ timestamp: getTimestamp(),
616
+ };
617
+ });
618
+ }