@cyclonedx/cdxgen 12.3.0 → 12.3.2

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 (121) hide show
  1. package/README.md +15 -5
  2. package/bin/audit.js +7 -0
  3. package/bin/cdxgen.js +241 -81
  4. package/bin/repl.js +138 -0
  5. package/data/rules/ai-agent-governance.yaml +249 -0
  6. package/data/rules/dependency-sources.yaml +41 -0
  7. package/data/rules/mcp-servers.yaml +304 -0
  8. package/data/rules/package-integrity.yaml +123 -0
  9. package/lib/audit/index.js +353 -29
  10. package/lib/audit/index.poku.js +247 -7
  11. package/lib/audit/reporters.js +26 -0
  12. package/lib/audit/scoring.js +262 -13
  13. package/lib/audit/scoring.poku.js +179 -0
  14. package/lib/audit/targets.js +391 -2
  15. package/lib/audit/targets.poku.js +416 -3
  16. package/lib/cli/index.js +588 -45
  17. package/lib/cli/index.poku.js +735 -1
  18. package/lib/evinser/evinser.js +8 -5
  19. package/lib/helpers/agentFormulationParser.js +318 -0
  20. package/lib/helpers/aiInventory.js +262 -0
  21. package/lib/helpers/aiInventory.poku.js +111 -0
  22. package/lib/helpers/analyzer.js +1769 -0
  23. package/lib/helpers/analyzer.poku.js +284 -3
  24. package/lib/helpers/auditCategories.js +76 -0
  25. package/lib/helpers/ciParsers/githubActions.js +140 -16
  26. package/lib/helpers/ciParsers/githubActions.poku.js +110 -0
  27. package/lib/helpers/communityAiConfigParser.js +672 -0
  28. package/lib/helpers/communityAiConfigParser.poku.js +63 -0
  29. package/lib/helpers/depsUtils.js +108 -0
  30. package/lib/helpers/depsUtils.poku.js +72 -1
  31. package/lib/helpers/display.js +325 -3
  32. package/lib/helpers/display.poku.js +301 -0
  33. package/lib/helpers/formulationParsers.js +28 -0
  34. package/lib/helpers/formulationParsers.poku.js +504 -1
  35. package/lib/helpers/jsonLike.js +102 -0
  36. package/lib/helpers/jsonLike.poku.js +34 -0
  37. package/lib/helpers/mcp.js +248 -0
  38. package/lib/helpers/mcp.poku.js +101 -0
  39. package/lib/helpers/mcpConfigParser.js +656 -0
  40. package/lib/helpers/mcpConfigParser.poku.js +126 -0
  41. package/lib/helpers/mcpDiscovery.js +84 -0
  42. package/lib/helpers/mcpDiscovery.poku.js +21 -0
  43. package/lib/helpers/protobom.js +3 -3
  44. package/lib/helpers/provenanceUtils.js +29 -4
  45. package/lib/helpers/provenanceUtils.poku.js +29 -3
  46. package/lib/helpers/registryProvenance.js +210 -0
  47. package/lib/helpers/registryProvenance.poku.js +144 -0
  48. package/lib/helpers/rustFormulationParser.js +330 -0
  49. package/lib/helpers/source.js +21 -2
  50. package/lib/helpers/source.poku.js +38 -0
  51. package/lib/helpers/utils.js +1331 -83
  52. package/lib/helpers/utils.poku.js +599 -188
  53. package/lib/helpers/vsixutils.js +12 -4
  54. package/lib/helpers/vsixutils.poku.js +34 -0
  55. package/lib/managers/binary.js +36 -12
  56. package/lib/managers/binary.poku.js +68 -0
  57. package/lib/managers/docker.js +59 -9
  58. package/lib/managers/docker.poku.js +61 -0
  59. package/lib/managers/piptree.js +12 -7
  60. package/lib/managers/piptree.poku.js +44 -0
  61. package/lib/stages/postgen/annotator.js +2 -1
  62. package/lib/stages/postgen/annotator.poku.js +15 -0
  63. package/lib/stages/postgen/auditBom.js +20 -6
  64. package/lib/stages/postgen/auditBom.poku.js +694 -1
  65. package/lib/stages/postgen/postgen.js +262 -11
  66. package/lib/stages/postgen/postgen.poku.js +306 -2
  67. package/lib/stages/postgen/ruleEngine.js +49 -1
  68. package/lib/stages/postgen/spdxConverter.poku.js +70 -0
  69. package/lib/stages/pregen/pregen.js +6 -4
  70. package/package.json +1 -1
  71. package/types/bin/repl.d.ts.map +1 -1
  72. package/types/lib/audit/index.d.ts.map +1 -1
  73. package/types/lib/audit/reporters.d.ts.map +1 -1
  74. package/types/lib/audit/scoring.d.ts.map +1 -1
  75. package/types/lib/audit/targets.d.ts +12 -0
  76. package/types/lib/audit/targets.d.ts.map +1 -1
  77. package/types/lib/cli/index.d.ts +2 -8
  78. package/types/lib/cli/index.d.ts.map +1 -1
  79. package/types/lib/evinser/evinser.d.ts.map +1 -1
  80. package/types/lib/helpers/agentFormulationParser.d.ts +19 -0
  81. package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -0
  82. package/types/lib/helpers/aiInventory.d.ts +23 -0
  83. package/types/lib/helpers/aiInventory.d.ts.map +1 -0
  84. package/types/lib/helpers/analyzer.d.ts +10 -0
  85. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  86. package/types/lib/helpers/auditCategories.d.ts +12 -0
  87. package/types/lib/helpers/auditCategories.d.ts.map +1 -0
  88. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  89. package/types/lib/helpers/communityAiConfigParser.d.ts +29 -0
  90. package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -0
  91. package/types/lib/helpers/depsUtils.d.ts +8 -0
  92. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  93. package/types/lib/helpers/display.d.ts +17 -1
  94. package/types/lib/helpers/display.d.ts.map +1 -1
  95. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  96. package/types/lib/helpers/jsonLike.d.ts +4 -0
  97. package/types/lib/helpers/jsonLike.d.ts.map +1 -0
  98. package/types/lib/helpers/mcp.d.ts +29 -0
  99. package/types/lib/helpers/mcp.d.ts.map +1 -0
  100. package/types/lib/helpers/mcpConfigParser.d.ts +30 -0
  101. package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -0
  102. package/types/lib/helpers/mcpDiscovery.d.ts +5 -0
  103. package/types/lib/helpers/mcpDiscovery.d.ts.map +1 -0
  104. package/types/lib/helpers/provenanceUtils.d.ts +5 -3
  105. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -1
  106. package/types/lib/helpers/registryProvenance.d.ts +9 -0
  107. package/types/lib/helpers/registryProvenance.d.ts.map +1 -1
  108. package/types/lib/helpers/rustFormulationParser.d.ts +17 -0
  109. package/types/lib/helpers/rustFormulationParser.d.ts.map +1 -0
  110. package/types/lib/helpers/source.d.ts.map +1 -1
  111. package/types/lib/helpers/utils.d.ts +31 -1
  112. package/types/lib/helpers/utils.d.ts.map +1 -1
  113. package/types/lib/helpers/vsixutils.d.ts.map +1 -1
  114. package/types/lib/managers/binary.d.ts.map +1 -1
  115. package/types/lib/managers/docker.d.ts.map +1 -1
  116. package/types/lib/managers/piptree.d.ts.map +1 -1
  117. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  118. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  119. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  120. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  121. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
@@ -0,0 +1,63 @@
1
+ import esmock from "esmock";
2
+ import { assert, describe, it } from "poku";
3
+ import sinon from "sinon";
4
+
5
+ function getProp(obj, name) {
6
+ return obj?.properties?.find((property) => property.name === name)?.value;
7
+ }
8
+
9
+ describe("communityAiConfigParser", () => {
10
+ it("normalizes Windows paths for community ecosystem discovery", async () => {
11
+ const readFileSync = sinon.stub();
12
+ readFileSync
13
+ .withArgs("C:\\repo\\.opencode\\agents\\review.md", "utf-8")
14
+ .returns(
15
+ [
16
+ "---",
17
+ "description: Reviews code for bugs and quality",
18
+ "mode: subagent",
19
+ "model: anthropic/claude-sonnet-4-20250514",
20
+ "---",
21
+ "Focus on code review findings.",
22
+ ].join("\n"),
23
+ );
24
+ readFileSync
25
+ .withArgs("C:\\repo\\.nanocoder\\commands\\fix.md", "utf-8")
26
+ .returns(
27
+ [
28
+ "---",
29
+ "description: Apply the standard fix workflow",
30
+ "category: engineering",
31
+ "tags: [bugfix, workflow]",
32
+ "---",
33
+ "1. Reproduce the issue",
34
+ ].join("\n"),
35
+ );
36
+ const { communityAiConfigParser } = await esmock(
37
+ "./communityAiConfigParser.js",
38
+ {
39
+ "node:fs": { readFileSync },
40
+ },
41
+ );
42
+
43
+ const result = communityAiConfigParser.parse([
44
+ "C:\\repo\\.opencode\\agents\\review.md",
45
+ "C:\\repo\\.nanocoder\\commands\\fix.md",
46
+ ]);
47
+
48
+ assert.ok(
49
+ result.components.some(
50
+ (component) =>
51
+ getProp(component, "cdx:agent:framework") === "opencode" &&
52
+ getProp(component, "cdx:file:kind") === "agent-definition",
53
+ ),
54
+ );
55
+ assert.ok(
56
+ result.components.some(
57
+ (component) =>
58
+ getProp(component, "cdx:agent:framework") === "nanocoder" &&
59
+ getProp(component, "cdx:file:kind") === "custom-command",
60
+ ),
61
+ );
62
+ });
63
+ });
@@ -82,6 +82,114 @@ export function mergeDependencies(
82
82
  return retlist;
83
83
  }
84
84
 
85
+ function serviceIdentityKey(service) {
86
+ if (service?.["bom-ref"]) {
87
+ return service["bom-ref"].toLowerCase();
88
+ }
89
+ return `${service?.group || ""}:${service?.name || ""}:${service?.version || ""}`.toLowerCase();
90
+ }
91
+
92
+ function mergeServiceProperties(existingProps = [], newProps = []) {
93
+ const merged = [...existingProps];
94
+ for (const newProp of newProps) {
95
+ if (
96
+ !merged.find(
97
+ (prop) =>
98
+ prop?.name === newProp?.name && prop?.value === newProp?.value,
99
+ )
100
+ ) {
101
+ merged.push(newProp);
102
+ }
103
+ }
104
+ return merged;
105
+ }
106
+
107
+ function normalizeServiceEndpoints(endpoints) {
108
+ if (Array.isArray(endpoints)) {
109
+ return endpoints.filter(
110
+ (endpoint) => typeof endpoint === "string" && endpoint,
111
+ );
112
+ }
113
+ if (typeof endpoints === "string" && endpoints) {
114
+ return [endpoints];
115
+ }
116
+ return [];
117
+ }
118
+
119
+ /**
120
+ * Merge CycloneDX services using bom-ref or group/name/version identity.
121
+ *
122
+ * @param {Object[]|Object} services Existing service list
123
+ * @param {Object[]|Object} newServices New service list
124
+ * @returns {Object[]} Merged and deduplicated services
125
+ */
126
+ export function mergeServices(services, newServices) {
127
+ const combined = []
128
+ .concat(services || [])
129
+ .concat(newServices || [])
130
+ .filter(Boolean);
131
+ const serviceMap = new Map();
132
+ for (const service of combined) {
133
+ const key = serviceIdentityKey(service);
134
+ if (!serviceMap.has(key)) {
135
+ serviceMap.set(key, {
136
+ ...service,
137
+ endpoints: Array.from(
138
+ new Set(normalizeServiceEndpoints(service.endpoints)),
139
+ ),
140
+ properties: mergeServiceProperties([], service.properties || []),
141
+ services: Array.isArray(service.services)
142
+ ? mergeServices([], service.services)
143
+ : undefined,
144
+ });
145
+ continue;
146
+ }
147
+ const existing = serviceMap.get(key);
148
+ existing.description = existing.description || service.description;
149
+ existing.group = existing.group || service.group;
150
+ existing.name = existing.name || service.name;
151
+ existing.version = existing.version || service.version;
152
+ existing.provider = existing.provider || service.provider;
153
+ existing.trustZone = existing.trustZone || service.trustZone;
154
+ if (service.authenticated === true) {
155
+ existing.authenticated = true;
156
+ } else if (
157
+ typeof existing.authenticated === "undefined" &&
158
+ typeof service.authenticated !== "undefined"
159
+ ) {
160
+ existing.authenticated = service.authenticated;
161
+ }
162
+ if (service["x-trust-boundary"] === true) {
163
+ existing["x-trust-boundary"] = true;
164
+ } else if (
165
+ typeof existing["x-trust-boundary"] === "undefined" &&
166
+ typeof service["x-trust-boundary"] !== "undefined"
167
+ ) {
168
+ existing["x-trust-boundary"] = service["x-trust-boundary"];
169
+ }
170
+ const incomingEndpoints = normalizeServiceEndpoints(service.endpoints);
171
+ if (incomingEndpoints.length) {
172
+ existing.endpoints = Array.from(
173
+ new Set([
174
+ ...normalizeServiceEndpoints(existing.endpoints),
175
+ ...incomingEndpoints,
176
+ ]),
177
+ );
178
+ }
179
+ existing.properties = mergeServiceProperties(
180
+ existing.properties || [],
181
+ service.properties || [],
182
+ );
183
+ if (Array.isArray(service.services) && service.services.length) {
184
+ existing.services = mergeServices(
185
+ existing.services || [],
186
+ service.services,
187
+ );
188
+ }
189
+ }
190
+ return Array.from(serviceMap.values());
191
+ }
192
+
85
193
  /**
86
194
  * Trim duplicate components by retaining all the properties
87
195
  *
@@ -1,6 +1,10 @@
1
1
  import { assert, describe, it } from "poku";
2
2
 
3
- import { mergeDependencies, trimComponents } from "./depsUtils.js";
3
+ import {
4
+ mergeDependencies,
5
+ mergeServices,
6
+ trimComponents,
7
+ } from "./depsUtils.js";
4
8
 
5
9
  describe("mergeDependencies()", () => {
6
10
  it("merges two non-overlapping dependency arrays", () => {
@@ -205,3 +209,70 @@ describe("trimComponents()", () => {
205
209
  ]);
206
210
  });
207
211
  });
212
+
213
+ describe("mergeServices()", () => {
214
+ it("merges matching services and deduplicates endpoints and properties", () => {
215
+ const result = mergeServices(
216
+ [
217
+ {
218
+ "bom-ref": "urn:service:mcp:demo:1.0.0",
219
+ name: "demo",
220
+ version: "1.0.0",
221
+ endpoints: ["/mcp"],
222
+ authenticated: false,
223
+ properties: [{ name: "cdx:mcp:transport", value: "streamable-http" }],
224
+ },
225
+ ],
226
+ [
227
+ {
228
+ "bom-ref": "urn:service:mcp:demo:1.0.0",
229
+ name: "demo",
230
+ version: "1.0.0",
231
+ endpoints: ["/mcp", "/.well-known/oauth-authorization-server"],
232
+ authenticated: true,
233
+ "x-trust-boundary": true,
234
+ properties: [
235
+ { name: "cdx:mcp:transport", value: "streamable-http" },
236
+ { name: "cdx:mcp:capabilities:tools", value: "true" },
237
+ ],
238
+ },
239
+ ],
240
+ );
241
+ assert.strictEqual(result.length, 1);
242
+ assert.deepStrictEqual(result[0].endpoints, [
243
+ "/mcp",
244
+ "/.well-known/oauth-authorization-server",
245
+ ]);
246
+ assert.strictEqual(result[0].authenticated, true);
247
+ assert.strictEqual(result[0]["x-trust-boundary"], true);
248
+ assert.strictEqual(result[0].properties.length, 2);
249
+ });
250
+
251
+ it("retains distinct services", () => {
252
+ const result = mergeServices(
253
+ [{ "bom-ref": "urn:service:mcp:stdio:1.0.0", name: "stdio" }],
254
+ [{ "bom-ref": "urn:service:mcp:http:1.0.0", name: "http" }],
255
+ );
256
+ assert.strictEqual(result.length, 2);
257
+ });
258
+
259
+ it("normalizes string endpoints when merging matching services", () => {
260
+ const result = mergeServices(
261
+ [
262
+ {
263
+ "bom-ref": "urn:service:mcp:demo:1.0.0",
264
+ endpoints: "/mcp",
265
+ name: "demo",
266
+ },
267
+ ],
268
+ [
269
+ {
270
+ "bom-ref": "urn:service:mcp:demo:1.0.0",
271
+ endpoints: ["/mcp", "/health"],
272
+ name: "demo",
273
+ },
274
+ ],
275
+ );
276
+ assert.deepStrictEqual(result[0].endpoints, ["/mcp", "/health"]);
277
+ });
278
+ });
@@ -7,7 +7,13 @@ import {
7
7
  REGISTRY_PROVENANCE_ICON,
8
8
  } from "./provenanceUtils.js";
9
9
  import { createStream, table } from "./table.js";
10
- import { isSecureMode, safeExistsSync, toCamel } from "./utils.js";
10
+ import {
11
+ getRecordedActivities,
12
+ isDryRun,
13
+ isSecureMode,
14
+ safeExistsSync,
15
+ toCamel,
16
+ } from "./utils.js";
11
17
 
12
18
  // https://github.com/yangshun/tree-node-cli/blob/master/src/index.js
13
19
  const SYMBOLS_ANSI = {
@@ -21,6 +27,29 @@ const SYMBOLS_ANSI = {
21
27
  const MAX_TREE_DEPTH = 6;
22
28
  const CYCLE_NODE_ICON = "↺";
23
29
  const REPEATED_NODE_ICON = "⤴";
30
+ const MULTIVALUE_ACTIVITY_TARGET_KEYS = new Set([
31
+ "LockFiles",
32
+ "ManifestFiles",
33
+ "PkgFiles",
34
+ "SrcFiles",
35
+ ]);
36
+ const PATH_SEPARATOR_REGEX = /[\\/]+/;
37
+ const ENV_AUDIT_SEVERITY_RANK = {
38
+ low: 1,
39
+ medium: 2,
40
+ high: 3,
41
+ critical: 4,
42
+ };
43
+ const ENV_AUDIT_TYPE_LABELS = {
44
+ "code-execution": "Code Execution",
45
+ "credential-exposure": "Credential Exposure",
46
+ "debug-exposure": "Debug Exposure",
47
+ "environment-variable": "Environment Variable",
48
+ "network-interception": "Network Interception",
49
+ "permission-misuse": "Permission Misuse",
50
+ privilege: "Privilege",
51
+ };
52
+
24
53
  const highlightStr = (s, highlight) => {
25
54
  if (highlight && s?.includes(highlight)) {
26
55
  s = s.replaceAll(highlight, `\x1b[1;33m${highlight}\x1b[0m`);
@@ -61,6 +90,86 @@ export const buildDependencyTreeLegendLines = (treeGraphics) => {
61
90
  }
62
91
  return [`Legend: ${legendLines.join("; ")}`];
63
92
  };
93
+
94
+ export function buildActivitySummaryPayload(activities, dryRunMode = isDryRun) {
95
+ const completedCount = activities.filter(
96
+ ({ status }) => status === "completed",
97
+ ).length;
98
+ const blockedCount = activities.filter(
99
+ ({ status }) => status === "blocked",
100
+ ).length;
101
+ const failedCount = activities.filter(
102
+ ({ status }) => status === "failed",
103
+ ).length;
104
+ return {
105
+ activities,
106
+ mode: dryRunMode ? "dry-run" : "debug",
107
+ summary: {
108
+ blocked: blockedCount,
109
+ completed: completedCount,
110
+ failed: failedCount,
111
+ total: activities.length,
112
+ },
113
+ };
114
+ }
115
+
116
+ export function serializeActivitySummary(
117
+ activities,
118
+ reportType = "json",
119
+ dryRunMode = isDryRun,
120
+ ) {
121
+ const activitySummaryPayload = buildActivitySummaryPayload(
122
+ activities,
123
+ dryRunMode,
124
+ );
125
+ if (reportType === "json") {
126
+ return [JSON.stringify(activitySummaryPayload, null, 2)];
127
+ }
128
+ if (reportType === "jsonl") {
129
+ return [
130
+ JSON.stringify({
131
+ mode: activitySummaryPayload.mode,
132
+ recordType: "summary",
133
+ ...activitySummaryPayload.summary,
134
+ }),
135
+ ...activities.map((activity) =>
136
+ JSON.stringify({
137
+ recordType: "activity",
138
+ ...activity,
139
+ }),
140
+ ),
141
+ ];
142
+ }
143
+ return [];
144
+ }
145
+
146
+ const splitCommaSeparatedActivityEntries = (value) =>
147
+ value
148
+ .split(",")
149
+ .map((entry) => entry.trim())
150
+ .filter(Boolean);
151
+
152
+ const activityPathDepth = (entry) =>
153
+ entry.split(PATH_SEPARATOR_REGEX).filter(Boolean).length;
154
+
155
+ const sortActivityTargetEntries = (entries) =>
156
+ [...entries].sort((left, right) => {
157
+ const depthDiff = activityPathDepth(left) - activityPathDepth(right);
158
+ if (depthDiff !== 0) {
159
+ return depthDiff;
160
+ }
161
+ const lengthDiff = left.length - right.length;
162
+ if (lengthDiff !== 0) {
163
+ return lengthDiff;
164
+ }
165
+ return left.localeCompare(right);
166
+ });
167
+
168
+ const isLikelyActivityPathList = (entries) =>
169
+ entries.length > 1 &&
170
+ entries.every(
171
+ (entry) => PATH_SEPARATOR_REGEX.test(entry) && !entry.includes("://"),
172
+ );
64
173
  /**
65
174
  * Prints the BOM components as a streaming table to the console.
66
175
  * Delegates to {@link printOSTable} automatically when the BOM metadata indicates
@@ -765,10 +874,220 @@ export function printSummary(bomJson) {
765
874
  console.log(table(data, config));
766
875
  }
767
876
 
877
+ export function printActivitySummary(reportType = undefined) {
878
+ const activities = getRecordedActivities();
879
+ if (!activities.length) {
880
+ return;
881
+ }
882
+ const activitySummaryPayload = buildActivitySummaryPayload(activities);
883
+ const completedCount = activitySummaryPayload.summary.completed;
884
+ const blockedCount = activitySummaryPayload.summary.blocked;
885
+ const failedCount = activitySummaryPayload.summary.failed;
886
+ const formatStatus = (status) => {
887
+ if (status === "completed") {
888
+ return "completed";
889
+ }
890
+ if (status === "blocked") {
891
+ return "blocked";
892
+ }
893
+ if (status === "failed") {
894
+ return "failed";
895
+ }
896
+ return status || "";
897
+ };
898
+ if (reportType === "json") {
899
+ for (const line of serializeActivitySummary(activities, reportType)) {
900
+ console.log(line);
901
+ }
902
+ return;
903
+ }
904
+ if (reportType === "jsonl") {
905
+ for (const line of serializeActivitySummary(activities, reportType)) {
906
+ console.log(line);
907
+ }
908
+ return;
909
+ }
910
+ const formatActivityTarget = (target) => {
911
+ if (typeof target !== "string" || !target.includes(",")) {
912
+ return target || "";
913
+ }
914
+ const targetEntries = splitCommaSeparatedActivityEntries(target);
915
+ if (isLikelyActivityPathList(targetEntries)) {
916
+ return sortActivityTargetEntries(targetEntries).join("\n");
917
+ }
918
+ if (!(target.includes(":") || target.includes("="))) {
919
+ return target || "";
920
+ }
921
+ const targetSegments = target.split(/,\s*(?=[A-Za-z][\w-]*\s*[:=])/);
922
+ let didFormat = false;
923
+ const renderedSegments = targetSegments.map((segment) => {
924
+ const segmentMatch = segment.match(/^([A-Za-z][\w-]*)\s*([:=])\s*(.*)$/);
925
+ if (!segmentMatch) {
926
+ return segment;
927
+ }
928
+ const [, key, separator, value] = segmentMatch;
929
+ if (!MULTIVALUE_ACTIVITY_TARGET_KEYS.has(key) || !value.includes(",")) {
930
+ return segment;
931
+ }
932
+ didFormat = true;
933
+ return `${key}${separator}\n${sortActivityTargetEntries(
934
+ splitCommaSeparatedActivityEntries(value),
935
+ )
936
+ .map((entry) => `- ${entry}`)
937
+ .join("\n")}`;
938
+ });
939
+ return didFormat ? renderedSegments.join("\n") : target;
940
+ };
941
+ const formatActivityType = (type) => {
942
+ if (typeof type !== "string" || !type.includes(",")) {
943
+ return type || "";
944
+ }
945
+ return splitCommaSeparatedActivityEntries(type)
946
+ .sort((left, right) => left.localeCompare(right))
947
+ .join("\n");
948
+ };
949
+ const data = [
950
+ [
951
+ "Identifier",
952
+ "Type",
953
+ "Package Type",
954
+ "Activity",
955
+ "Target",
956
+ "Outcome / Why",
957
+ ],
958
+ ];
959
+ for (const activity of activities) {
960
+ data.push([
961
+ activity.identifier,
962
+ formatActivityType(activity.projectType),
963
+ activity.packageType || "",
964
+ activity.kind || "",
965
+ formatActivityTarget(activity.target),
966
+ activity.reason
967
+ ? `${formatStatus(activity.status)}\n${activity.reason}`.trim()
968
+ : formatStatus(activity.status),
969
+ ]);
970
+ }
971
+ const config = {
972
+ header: {
973
+ alignment: "center",
974
+ content: `${
975
+ isDryRun
976
+ ? "cdxgen dry-run activity summary"
977
+ : "cdxgen debug activity summary"
978
+ }\n${completedCount} completed ${blockedCount} blocked ${failedCount} failed`,
979
+ },
980
+ columns: [
981
+ { width: 14 },
982
+ { width: 14 },
983
+ { width: 14 },
984
+ { width: 12 },
985
+ { width: 48, wrapWord: true },
986
+ { width: 28, wrapWord: true },
987
+ ],
988
+ };
989
+ console.log(table(data, config));
990
+ }
991
+
768
992
  /**
769
993
  * @typedef {{type: string, variable: string, severity: string, message: string, mitigation: string}} EnvAuditFinding
770
994
  */
771
995
 
996
+ const summarizeEnvAuditSeverities = (envAuditFindings) => {
997
+ const counts = { critical: 0, high: 0, medium: 0, low: 0 };
998
+ for (const finding of envAuditFindings) {
999
+ if (counts[finding.severity] !== undefined) {
1000
+ counts[finding.severity] += 1;
1001
+ }
1002
+ }
1003
+ return ["critical", "high", "medium", "low"]
1004
+ .filter((severity) => counts[severity] > 0)
1005
+ .map((severity) => `${counts[severity]} ${severity}`)
1006
+ .join(" ");
1007
+ };
1008
+
1009
+ const buildEnvironmentAuditGroups = (envAuditFindings) => {
1010
+ const groups = new Map();
1011
+ for (const finding of envAuditFindings) {
1012
+ const isCredentialExposure = finding.type === "credential-exposure";
1013
+ const groupKey = isCredentialExposure
1014
+ ? "credential-exposure"
1015
+ : JSON.stringify([
1016
+ finding.type,
1017
+ finding.severity,
1018
+ finding.message,
1019
+ finding.mitigation,
1020
+ ]);
1021
+ if (!groups.has(groupKey)) {
1022
+ groups.set(groupKey, {
1023
+ details: isCredentialExposure
1024
+ ? "Credential-like environment variables are set. Build tools or install scripts invoked during SBOM generation may read inherited environment variables."
1025
+ : finding.message,
1026
+ mitigation: isCredentialExposure
1027
+ ? "Unset unneeded secrets when scanning untrusted repositories. Prefer ephemeral, scoped CI credentials injected only for the step that needs them."
1028
+ : finding.mitigation,
1029
+ severity: finding.severity,
1030
+ title:
1031
+ ENV_AUDIT_TYPE_LABELS[finding.type] ||
1032
+ toCamel(finding.type || "Finding"),
1033
+ variables: new Set(),
1034
+ });
1035
+ }
1036
+ groups.get(groupKey).variables.add(finding.variable);
1037
+ }
1038
+ return [...groups.values()]
1039
+ .map((group) => ({
1040
+ ...group,
1041
+ variables: [...group.variables].filter(Boolean).sort(),
1042
+ }))
1043
+ .sort((left, right) => {
1044
+ const severityDiff =
1045
+ (ENV_AUDIT_SEVERITY_RANK[right.severity] || 0) -
1046
+ (ENV_AUDIT_SEVERITY_RANK[left.severity] || 0);
1047
+ if (severityDiff !== 0) {
1048
+ return severityDiff;
1049
+ }
1050
+ return left.title.localeCompare(right.title);
1051
+ });
1052
+ };
1053
+
1054
+ /**
1055
+ * Prints a grouped secure-mode environment audit call-out panel.
1056
+ *
1057
+ * @param {EnvAuditFinding[]} envAuditFindings Audit findings to display
1058
+ * @returns {void}
1059
+ */
1060
+ export function printEnvironmentAuditFindings(envAuditFindings = []) {
1061
+ if (!envAuditFindings.length) {
1062
+ return;
1063
+ }
1064
+ const groupedFindings = buildEnvironmentAuditGroups(envAuditFindings);
1065
+ const severitySummary = summarizeEnvAuditSeverities(envAuditFindings);
1066
+ const data = [["Category", "Severity", "Variable(s)", "Details"]];
1067
+ for (const finding of groupedFindings) {
1068
+ data.push([
1069
+ finding.title,
1070
+ finding.severity.toUpperCase(),
1071
+ finding.variables.join("\n"),
1072
+ `${finding.details}\nMitigation: ${finding.mitigation}`,
1073
+ ]);
1074
+ }
1075
+ const config = {
1076
+ header: {
1077
+ alignment: "center",
1078
+ content: `SECURE MODE: Environment audit\n${severitySummary || `${envAuditFindings.length} finding(s)`}`,
1079
+ },
1080
+ columns: [
1081
+ { width: 22 },
1082
+ { width: 10 },
1083
+ { width: 24, wrapWord: true },
1084
+ { width: 50, wrapWord: true },
1085
+ ],
1086
+ columnDefault: { wrapWord: true },
1087
+ };
1088
+ console.log(table(data, config));
1089
+ }
1090
+
772
1091
  /**
773
1092
  * Runs the pre-generation environment audit and renders the results as formatted
774
1093
  * tables to the console. Called when the --env-audit CLI flag is set.
@@ -784,7 +1103,7 @@ export function displaySelfThreatModel(
784
1103
  options,
785
1104
  envAuditFindings,
786
1105
  ) {
787
- const TLP = options.tlpClassification || "CLEAR";
1106
+ const TLP = options.tlpClassification;
788
1107
  const risks = [];
789
1108
  let riskScore = 0;
790
1109
 
@@ -923,8 +1242,11 @@ export function displaySelfThreatModel(
923
1242
  AMBER_AND_STRICT: "Organisation only. No external sharing.",
924
1243
  RED: "Named recipients only. Do not forward or store beyond session.",
925
1244
  };
1245
+ const tlpValue = TLP
1246
+ ? `${TLP} — ${tlpGuidance[TLP]}`
1247
+ : "Not set — no distribution constraints recorded.";
926
1248
  const headerData = [
927
- ["TLP Classification", `${TLP} — ${tlpGuidance[TLP]}`],
1249
+ ["TLP Classification", tlpValue],
928
1250
  ["Risk Score", `${riskScore}/10`],
929
1251
  ["Risk Level", `${riskColor[riskLevel]}${riskLevel}${reset}`],
930
1252
  ];