@cyclonedx/cdxgen 12.3.2 → 12.3.3

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 (53) hide show
  1. package/README.md +6 -0
  2. package/data/rules/ci-permissions.yaml +132 -0
  3. package/data/rules/dependency-sources.yaml +65 -5
  4. package/data/rules/package-integrity.yaml +22 -0
  5. package/lib/cli/index.js +141 -39
  6. package/lib/cli/index.poku.js +579 -1
  7. package/lib/helpers/agentFormulationParser.js +6 -2
  8. package/lib/helpers/agentFormulationParser.poku.js +42 -0
  9. package/lib/helpers/analyzer.js +38 -9
  10. package/lib/helpers/analyzer.poku.js +67 -0
  11. package/lib/helpers/chromextutils.js +25 -3
  12. package/lib/helpers/chromextutils.poku.js +68 -0
  13. package/lib/helpers/ciParsers/githubActions.js +79 -0
  14. package/lib/helpers/ciParsers/githubActions.poku.js +103 -0
  15. package/lib/helpers/communityAiConfigParser.js +15 -5
  16. package/lib/helpers/communityAiConfigParser.poku.js +71 -0
  17. package/lib/helpers/depsUtils.js +5 -0
  18. package/lib/helpers/depsUtils.poku.js +55 -0
  19. package/lib/helpers/display.js +45 -22
  20. package/lib/helpers/display.poku.js +47 -60
  21. package/lib/helpers/mcpConfigParser.js +21 -5
  22. package/lib/helpers/mcpConfigParser.poku.js +39 -2
  23. package/lib/helpers/propertySanitizer.js +121 -0
  24. package/lib/helpers/utils.js +951 -40
  25. package/lib/helpers/utils.poku.js +882 -0
  26. package/lib/managers/binary.js +16 -0
  27. package/lib/managers/binary.poku.js +1 -0
  28. package/lib/managers/docker.js +240 -16
  29. package/lib/managers/docker.poku.js +1142 -2
  30. package/lib/server/server.js +7 -4
  31. package/lib/server/server.poku.js +36 -1
  32. package/lib/stages/postgen/auditBom.poku.js +644 -2
  33. package/package.json +2 -1
  34. package/types/lib/cli/index.d.ts.map +1 -1
  35. package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -1
  36. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  37. package/types/lib/helpers/chromextutils.d.ts.map +1 -1
  38. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  39. package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -1
  40. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  41. package/types/lib/helpers/display.d.ts +1 -0
  42. package/types/lib/helpers/display.d.ts.map +1 -1
  43. package/types/lib/helpers/mcpConfigParser.d.ts +1 -1
  44. package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -1
  45. package/types/lib/helpers/propertySanitizer.d.ts +3 -0
  46. package/types/lib/helpers/propertySanitizer.d.ts.map +1 -0
  47. package/types/lib/helpers/utils.d.ts +29 -0
  48. package/types/lib/helpers/utils.d.ts.map +1 -1
  49. package/types/lib/managers/binary.d.ts.map +1 -1
  50. package/types/lib/managers/docker.d.ts +3 -0
  51. package/types/lib/managers/docker.d.ts.map +1 -1
  52. package/types/lib/server/server.d.ts +1 -0
  53. package/types/lib/server/server.d.ts.map +1 -1
@@ -208,6 +208,61 @@ describe("trimComponents()", () => {
208
208
  { alg: "SHA-256", content: "def456" },
209
209
  ]);
210
210
  });
211
+
212
+ it("retains identity tool references when merging duplicate components", () => {
213
+ const components = [
214
+ {
215
+ name: "openssl",
216
+ version: "3.0.0",
217
+ purl: "pkg:rpm/redhat/openssl@3.0.0",
218
+ type: "library",
219
+ evidence: {
220
+ identity: [
221
+ {
222
+ field: "purl",
223
+ confidence: 1,
224
+ methods: [
225
+ {
226
+ technique: "binary-analysis",
227
+ confidence: 1,
228
+ value: "openssl",
229
+ },
230
+ ],
231
+ tools: ["pkg:generic/trivy@0.1.0"],
232
+ },
233
+ ],
234
+ },
235
+ },
236
+ {
237
+ name: "openssl",
238
+ version: "3.0.0",
239
+ purl: "pkg:rpm/redhat/openssl@3.0.0",
240
+ type: "library",
241
+ evidence: {
242
+ identity: [
243
+ {
244
+ field: "purl",
245
+ confidence: 1,
246
+ methods: [
247
+ {
248
+ technique: "binary-analysis",
249
+ confidence: 1,
250
+ value: "openssl",
251
+ },
252
+ ],
253
+ tools: ["pkg:generic/blint@1.2.3"],
254
+ },
255
+ ],
256
+ },
257
+ },
258
+ ];
259
+ const result = trimComponents(components);
260
+ assert.strictEqual(result.length, 1);
261
+ assert.deepStrictEqual(result[0].evidence.identity[0].tools, [
262
+ "pkg:generic/trivy@0.1.0",
263
+ "pkg:generic/blint@1.2.3",
264
+ ]);
265
+ });
211
266
  });
212
267
 
213
268
  describe("mergeServices()", () => {
@@ -65,10 +65,44 @@ const formatComponentName = (component, highlight) => {
65
65
  return displayName;
66
66
  };
67
67
 
68
- const printProvenanceLegend = () => {
69
- console.log(
70
- `Legend: ${REGISTRY_PROVENANCE_ICON} = registry provenance or trusted publishing evidence`,
71
- );
68
+ /**
69
+ * Builds the summary and provenance lines printed after the component table.
70
+ *
71
+ * @param {Object} bomJson CycloneDX BOM JSON object
72
+ * @param {string[]|undefined} filterTypes Optional list of component types to include
73
+ * @param {string|undefined} summaryText Optional summary message to print after the table
74
+ * @param {number} displayedProvenanceCount Number of displayed components with registry provenance
75
+ * @returns {string[]} Summary lines to print
76
+ */
77
+ export const buildTableSummaryLines = (
78
+ bomJson,
79
+ filterTypes,
80
+ summaryText,
81
+ displayedProvenanceCount = 0,
82
+ ) => {
83
+ const summaryLines = [];
84
+ if (summaryText) {
85
+ summaryLines.push(summaryText);
86
+ } else if (!filterTypes) {
87
+ summaryLines.push(
88
+ `BOM includes ${bomJson?.components?.length || 0} components and ${
89
+ bomJson?.dependencies?.length || 0
90
+ } dependencies`,
91
+ );
92
+ } else {
93
+ summaryLines.push(
94
+ `Components filtered based on type: ${filterTypes.join(", ")}`,
95
+ );
96
+ }
97
+ if (displayedProvenanceCount > 0) {
98
+ summaryLines.push(
99
+ `Legend: ${REGISTRY_PROVENANCE_ICON} = registry provenance or trusted publishing evidence`,
100
+ );
101
+ summaryLines.push(
102
+ `${REGISTRY_PROVENANCE_ICON} ${displayedProvenanceCount} component(s) include registry provenance or trusted publishing metadata.`,
103
+ );
104
+ }
105
+ return summaryLines;
72
106
  };
73
107
 
74
108
  /**
@@ -247,24 +281,13 @@ export function printTable(
247
281
  }
248
282
  stream.end();
249
283
  console.log();
250
- if (summaryText) {
251
- console.log(summaryText);
252
- } else if (!filterTypes) {
253
- console.log(
254
- "BOM includes",
255
- bomJson?.components?.length || 0,
256
- "components and",
257
- bomJson?.dependencies?.length || 0,
258
- "dependencies",
259
- );
260
- } else {
261
- console.log(`Components filtered based on type: ${filterTypes.join(", ")}`);
262
- }
263
- if (displayedProvenanceCount > 0) {
264
- printProvenanceLegend();
265
- console.log(
266
- `${REGISTRY_PROVENANCE_ICON} ${displayedProvenanceCount} component(s) include registry provenance or trusted publishing metadata.`,
267
- );
284
+ for (const line of buildTableSummaryLines(
285
+ bomJson,
286
+ filterTypes,
287
+ summaryText,
288
+ displayedProvenanceCount,
289
+ )) {
290
+ console.log(line);
268
291
  }
269
292
  }
270
293
  const formatProps = (props) => {
@@ -8,6 +8,7 @@ import {
8
8
  buildActivitySummaryPayload,
9
9
  buildDependencyTreeLegendLines,
10
10
  buildDependencyTreeLines,
11
+ buildTableSummaryLines,
11
12
  printDependencyTree,
12
13
  serializeActivitySummary,
13
14
  } from "./display.js";
@@ -22,75 +23,61 @@ it("print tree test", () => {
22
23
 
23
24
  it("prints a provenance icon for registry-backed components", async () => {
24
25
  const rows = [];
25
- const consoleLogStub = sinon.stub(console, "log");
26
- try {
27
- const { printTable } = await esmock("./display.js", {
28
- "./table.js": {
29
- createStream: () => ({
30
- end() {
31
- // intentional no-op for stream stub
32
- },
33
- write(row) {
34
- rows.push(row);
35
- },
36
- }),
37
- table: sinon.stub().returns(""),
38
- },
39
- "./utils.js": {
40
- isSecureMode: false,
41
- safeExistsSync: sinon.stub(),
42
- toCamel: sinon.stub(),
43
- },
44
- });
45
-
46
- printTable(
26
+ const bomJson = {
27
+ components: [
47
28
  {
48
- components: [
49
- {
50
- group: "",
51
- name: "left-pad",
52
- properties: [
53
- {
54
- name: "cdx:npm:provenanceUrl",
55
- value:
56
- "https://registry.npmjs.org/-/npm/v1/attestations/left-pad",
57
- },
58
- ],
59
- type: "library",
60
- version: "1.3.0",
61
- },
29
+ group: "",
30
+ name: "left-pad",
31
+ properties: [
62
32
  {
63
- group: "",
64
- name: "lodash",
65
- properties: [],
66
- type: "library",
67
- version: "4.17.21",
33
+ name: "cdx:npm:provenanceUrl",
34
+ value: "https://registry.npmjs.org/-/npm/v1/attestations/left-pad",
68
35
  },
69
36
  ],
70
- dependencies: [],
37
+ type: "library",
38
+ version: "1.3.0",
71
39
  },
72
- undefined,
73
- undefined,
74
- "Found 1 trusted component.",
75
- );
40
+ {
41
+ group: "",
42
+ name: "lodash",
43
+ properties: [],
44
+ type: "library",
45
+ version: "4.17.21",
46
+ },
47
+ ],
48
+ dependencies: [],
49
+ };
50
+ const { printTable } = await esmock("./display.js", {
51
+ "./table.js": {
52
+ createStream: () => ({
53
+ end() {
54
+ // intentional no-op for stream stub
55
+ },
56
+ write(row) {
57
+ rows.push(row);
58
+ },
59
+ }),
60
+ table: sinon.stub().returns(""),
61
+ },
62
+ "./utils.js": {
63
+ isSecureMode: false,
64
+ safeExistsSync: sinon.stub(),
65
+ toCamel: sinon.stub(),
66
+ },
67
+ });
76
68
 
77
- assert.strictEqual(rows[1][1], `${REGISTRY_PROVENANCE_ICON} left-pad`);
78
- assert.strictEqual(rows[2][1], "lodash");
79
- sinon.assert.calledWithExactly(
80
- consoleLogStub,
69
+ printTable(bomJson, undefined, undefined, "Found 1 trusted component.");
70
+
71
+ assert.strictEqual(rows[1][1], `${REGISTRY_PROVENANCE_ICON} left-pad`);
72
+ assert.strictEqual(rows[2][1], "lodash");
73
+ assert.deepStrictEqual(
74
+ buildTableSummaryLines(bomJson, undefined, "Found 1 trusted component.", 1),
75
+ [
81
76
  "Found 1 trusted component.",
82
- );
83
- sinon.assert.calledWithExactly(
84
- consoleLogStub,
85
77
  `Legend: ${REGISTRY_PROVENANCE_ICON} = registry provenance or trusted publishing evidence`,
86
- );
87
- sinon.assert.calledWithExactly(
88
- consoleLogStub,
89
78
  `${REGISTRY_PROVENANCE_ICON} 1 component(s) include registry provenance or trusted publishing metadata.`,
90
- );
91
- } finally {
92
- consoleLogStub.restore();
93
- }
79
+ ],
80
+ );
94
81
  });
95
82
 
96
83
  it("displaySelfThreatModel does not assume a default TLP classification", async () => {
@@ -9,6 +9,10 @@ import {
9
9
  providerNamesForText,
10
10
  sanitizeMcpRefToken,
11
11
  } from "./mcpDiscovery.js";
12
+ import {
13
+ sanitizeBomPropertyValue,
14
+ sanitizeBomUrl,
15
+ } from "./propertySanitizer.js";
12
16
  import { scanTextForHiddenUnicode } from "./unicodeScan.js";
13
17
 
14
18
  const MCP_CONFIG_PATTERNS = [
@@ -40,13 +44,22 @@ const SECRET_FIELD_NAME_PATTERN =
40
44
  const ENV_REFERENCE_PATTERN = /(?:\$\{?[A-Z0-9_]+\}?|%[A-Z0-9_]+%)/u;
41
45
 
42
46
  function addUniqueProperty(properties, name, value) {
43
- if (value === undefined || value === null || value === "") {
47
+ const sanitizedValue = sanitizeBomPropertyValue(name, value);
48
+ if (
49
+ sanitizedValue === undefined ||
50
+ sanitizedValue === null ||
51
+ sanitizedValue === ""
52
+ ) {
44
53
  return;
45
54
  }
46
- if (properties.some((prop) => prop.name === name && prop.value === value)) {
55
+ if (
56
+ properties.some(
57
+ (prop) => prop.name === name && prop.value === String(sanitizedValue),
58
+ )
59
+ ) {
47
60
  return;
48
61
  }
49
- properties.push({ name, value: String(value) });
62
+ properties.push({ name, value: String(sanitizedValue) });
50
63
  }
51
64
 
52
65
  function normalizeFilePath(filePath) {
@@ -380,6 +393,9 @@ function createServiceFromConfig(
380
393
  const command = normalized.command;
381
394
  const args = normalized.args;
382
395
  const endpoints = extractEndpoints(serverConfig);
396
+ const sanitizedEndpoints = endpoints.map((endpoint) =>
397
+ sanitizeBomUrl(endpoint),
398
+ );
383
399
  const transport = inferTransport(serverConfig, endpoints);
384
400
  const authHints = authHintsFromValue(serverConfig);
385
401
  const authPosture = authPostureForConfig(serverConfig, endpoints, authHints);
@@ -396,7 +412,7 @@ function createServiceFromConfig(
396
412
  JSON.stringify({
397
413
  args,
398
414
  command,
399
- endpoints,
415
+ endpoints: sanitizedEndpoints,
400
416
  env: serverConfig?.env || serverConfig?.environment || {},
401
417
  }),
402
418
  );
@@ -534,7 +550,7 @@ function createServiceFromConfig(
534
550
  return {
535
551
  "bom-ref": `urn:service:mcp:${sanitizeMcpRefToken(serviceName)}:${sanitizeMcpRefToken(version)}`,
536
552
  authenticated,
537
- endpoints,
553
+ endpoints: sanitizedEndpoints,
538
554
  group: "mcp",
539
555
  name: serviceName,
540
556
  properties,
@@ -69,7 +69,7 @@ describe("mcpConfigParser", () => {
69
69
  args: [
70
70
  "--token",
71
71
  "sk_test_super_secret_value",
72
- "https://docs.example.com/mcp",
72
+ "https://user:pass@docs.example.com/mcp?access_token=secret#frag",
73
73
  ],
74
74
  command: "npx",
75
75
  env: {
@@ -99,7 +99,7 @@ describe("mcpConfigParser", () => {
99
99
  );
100
100
  assert.strictEqual(
101
101
  getProp(service, "cdx:mcp:credentialExposureFieldCount"),
102
- "3",
102
+ "4",
103
103
  );
104
104
  assert.strictEqual(
105
105
  getProp(service, "cdx:mcp:credentialReferenceCount"),
@@ -109,6 +109,12 @@ describe("mcpConfigParser", () => {
109
109
  getProp(component, "cdx:mcp:credentialExposedServiceCount"),
110
110
  "1",
111
111
  );
112
+ assert.strictEqual(getProp(service, "cdx:mcp:command"), "npx");
113
+ assert.deepStrictEqual(service.endpoints, ["https://docs.example.com/mcp"]);
114
+ assert.strictEqual(
115
+ getProp(component, "cdx:mcp:configuredEndpoints"),
116
+ "https://docs.example.com/mcp",
117
+ );
112
118
  assert.strictEqual(
113
119
  getProp(service, "cdx:mcp:credentialRiskIndicators"),
114
120
  undefined,
@@ -123,4 +129,35 @@ describe("mcpConfigParser", () => {
123
129
  undefined,
124
130
  );
125
131
  });
132
+
133
+ it("summarizes Windows executable paths with spaces safely", async () => {
134
+ const readFileSync = sinon.stub();
135
+ const scanTextForHiddenUnicode = sinon.stub().returns({
136
+ hasHiddenUnicode: false,
137
+ });
138
+ readFileSync.withArgs("/repo/.vscode/mcp.json", "utf-8").returns(
139
+ JSON.stringify({
140
+ mcpServers: {
141
+ releaseDocs: {
142
+ args: ["--inspect"],
143
+ command: "C:\\Program Files\\nodejs\\node.exe --inspect",
144
+ mcp: true,
145
+ transport: "stdio",
146
+ },
147
+ },
148
+ }),
149
+ );
150
+ const { mcpConfigParser } = await esmock("./mcpConfigParser.js", {
151
+ "node:fs": { readFileSync },
152
+ "./unicodeScan.js": { scanTextForHiddenUnicode },
153
+ });
154
+
155
+ const result = mcpConfigParser.parse(["/repo/.vscode/mcp.json"]);
156
+
157
+ assert.strictEqual(
158
+ getProp(result.services[0], "cdx:mcp:command") ||
159
+ getProp(result.components[0], "cdx:mcp:command"),
160
+ "node.exe",
161
+ );
162
+ });
126
163
  });
@@ -0,0 +1,121 @@
1
+ import path from "node:path";
2
+
3
+ const DANGEROUS_OBJECT_KEYS = new Set([
4
+ "__proto__",
5
+ "constructor",
6
+ "prototype",
7
+ ]);
8
+ const INLINE_CREDENTIAL_PATTERNS = [
9
+ /\bAKIA[0-9A-Z]{16}\b/gu,
10
+ /\bbearer\s+[a-z0-9._-]{16,}\b/giu,
11
+ /\b(?:sk|rk|pk)_[a-z0-9_-]{8,}\b/giu,
12
+ /\bgh[pousr]_[a-z0-9]{20,}\b/giu,
13
+ /\bAIza[0-9A-Za-z_-]{20,}\b/gu,
14
+ ];
15
+ const JSON_PROPERTY_NAMES = new Set([
16
+ "cdx:agent:permission",
17
+ "cdx:mcp:toolAnnotations",
18
+ "cdx:skill:metadata",
19
+ ]);
20
+ const URL_PATTERN = /https?:\/\/[^\s<>"'),\]}]+/giu;
21
+
22
+ function sanitizeUrlForBom(value) {
23
+ const input = String(value || "").trim();
24
+ if (!input) {
25
+ return input;
26
+ }
27
+ try {
28
+ const parsed = new URL(input);
29
+ parsed.username = "";
30
+ parsed.password = "";
31
+ parsed.search = "";
32
+ parsed.hash = "";
33
+ return parsed.toString();
34
+ } catch {
35
+ return input;
36
+ }
37
+ }
38
+
39
+ function sanitizeTextForBom(value) {
40
+ let sanitized = String(value ?? "");
41
+ sanitized = sanitized.replace(URL_PATTERN, (match) =>
42
+ sanitizeUrlForBom(match),
43
+ );
44
+ for (const pattern of INLINE_CREDENTIAL_PATTERNS) {
45
+ sanitized = sanitized.replace(pattern, "[redacted]");
46
+ }
47
+ return sanitized;
48
+ }
49
+
50
+ function sanitizeStructuredValueForBom(value) {
51
+ if (typeof value === "string") {
52
+ return sanitizeTextForBom(value);
53
+ }
54
+ if (Array.isArray(value)) {
55
+ return value.map((entry) => sanitizeStructuredValueForBom(entry));
56
+ }
57
+ if (value && typeof value === "object") {
58
+ const sanitized = {};
59
+ for (const [key, entryValue] of Object.entries(value)) {
60
+ if (DANGEROUS_OBJECT_KEYS.has(key)) {
61
+ continue;
62
+ }
63
+ sanitized[key] = sanitizeStructuredValueForBom(entryValue);
64
+ }
65
+ return sanitized;
66
+ }
67
+ return value;
68
+ }
69
+
70
+ function extractCommandExecutable(command) {
71
+ const trimmedCommand = String(command || "").trim();
72
+ if (!trimmedCommand) {
73
+ return "";
74
+ }
75
+ const quotedMatch = trimmedCommand.match(/^(['"])(.*?)\1/u);
76
+ if (quotedMatch?.[2]) {
77
+ return quotedMatch[2];
78
+ }
79
+ const absolutePathMatch = trimmedCommand.match(
80
+ /^((?:[A-Za-z]:\\|\/).*?\.(?:bat|bin|cjs|cmd|com|exe|jar|js|mjs|ps1|py|rb|sh|ts|tsx))(?=\s|$)/iu,
81
+ );
82
+ if (absolutePathMatch?.[1]) {
83
+ return absolutePathMatch[1];
84
+ }
85
+ return trimmedCommand.split(/\s+/u)[0];
86
+ }
87
+
88
+ function summarizeExecutable(command) {
89
+ const executable = extractCommandExecutable(command);
90
+ if (!executable) {
91
+ return "configured";
92
+ }
93
+ if (executable.includes("\\")) {
94
+ return path.win32.basename(executable) || "configured";
95
+ }
96
+ return path.posix.basename(executable) || "configured";
97
+ }
98
+
99
+ export function sanitizeBomUrl(value) {
100
+ return sanitizeUrlForBom(value);
101
+ }
102
+
103
+ export function sanitizeBomPropertyValue(name, value) {
104
+ if (value === undefined || value === null || value === "") {
105
+ return value;
106
+ }
107
+ if (name === "cdx:mcp:command") {
108
+ const sanitizedCommand = sanitizeTextForBom(value).trim();
109
+ if (!sanitizedCommand) {
110
+ return sanitizedCommand;
111
+ }
112
+ return summarizeExecutable(sanitizedCommand);
113
+ }
114
+ if (JSON_PROPERTY_NAMES.has(name) || typeof value === "object") {
115
+ return JSON.stringify(sanitizeStructuredValueForBom(value));
116
+ }
117
+ if (typeof value === "string") {
118
+ return sanitizeTextForBom(value);
119
+ }
120
+ return value;
121
+ }