@cyclonedx/cdxgen 12.3.1 → 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 (87) hide show
  1. package/README.md +6 -0
  2. package/bin/cdxgen.js +1 -2
  3. package/data/rules/ai-agent-governance.yaml +43 -0
  4. package/data/rules/ci-permissions.yaml +132 -0
  5. package/data/rules/dependency-sources.yaml +65 -5
  6. package/data/rules/mcp-servers.yaml +36 -2
  7. package/data/rules/package-integrity.yaml +22 -0
  8. package/lib/cli/index.js +436 -56
  9. package/lib/cli/index.poku.js +875 -2
  10. package/lib/helpers/agentFormulationParser.js +10 -3
  11. package/lib/helpers/agentFormulationParser.poku.js +42 -0
  12. package/lib/helpers/aiInventory.js +262 -0
  13. package/lib/helpers/aiInventory.poku.js +111 -0
  14. package/lib/helpers/analyzer.js +413 -54
  15. package/lib/helpers/analyzer.poku.js +117 -0
  16. package/lib/helpers/auditCategories.js +76 -0
  17. package/lib/helpers/chromextutils.js +25 -3
  18. package/lib/helpers/chromextutils.poku.js +68 -0
  19. package/lib/helpers/ciParsers/githubActions.js +79 -0
  20. package/lib/helpers/ciParsers/githubActions.poku.js +103 -0
  21. package/lib/helpers/communityAiConfigParser.js +15 -5
  22. package/lib/helpers/communityAiConfigParser.poku.js +71 -0
  23. package/lib/helpers/depsUtils.js +5 -0
  24. package/lib/helpers/depsUtils.poku.js +55 -0
  25. package/lib/helpers/display.js +50 -24
  26. package/lib/helpers/display.poku.js +70 -58
  27. package/lib/helpers/formulationParsers.js +26 -6
  28. package/lib/helpers/jsonLike.js +21 -20
  29. package/lib/helpers/jsonLike.poku.js +34 -0
  30. package/lib/helpers/mcpConfigParser.js +32 -16
  31. package/lib/helpers/mcpConfigParser.poku.js +104 -0
  32. package/lib/helpers/mcpDiscovery.js +13 -23
  33. package/lib/helpers/mcpDiscovery.poku.js +21 -0
  34. package/lib/helpers/propertySanitizer.js +121 -0
  35. package/lib/helpers/utils.js +953 -41
  36. package/lib/helpers/utils.poku.js +901 -1
  37. package/lib/managers/binary.js +16 -0
  38. package/lib/managers/binary.poku.js +1 -0
  39. package/lib/managers/docker.js +240 -16
  40. package/lib/managers/docker.poku.js +1142 -2
  41. package/lib/server/server.js +7 -4
  42. package/lib/server/server.poku.js +36 -1
  43. package/lib/stages/postgen/annotator.js +2 -1
  44. package/lib/stages/postgen/annotator.poku.js +15 -0
  45. package/lib/stages/postgen/auditBom.js +12 -6
  46. package/lib/stages/postgen/auditBom.poku.js +755 -6
  47. package/lib/stages/postgen/postgen.js +229 -6
  48. package/lib/stages/postgen/postgen.poku.js +180 -0
  49. package/package.json +2 -1
  50. package/types/lib/cli/index.d.ts +1 -0
  51. package/types/lib/cli/index.d.ts.map +1 -1
  52. package/types/lib/helpers/agentFormulationParser.d.ts +19 -0
  53. package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -0
  54. package/types/lib/helpers/aiInventory.d.ts +23 -0
  55. package/types/lib/helpers/aiInventory.d.ts.map +1 -0
  56. package/types/lib/helpers/analyzer.d.ts +5 -0
  57. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  58. package/types/lib/helpers/auditCategories.d.ts +12 -0
  59. package/types/lib/helpers/auditCategories.d.ts.map +1 -0
  60. package/types/lib/helpers/chromextutils.d.ts.map +1 -1
  61. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  62. package/types/lib/helpers/communityAiConfigParser.d.ts +29 -0
  63. package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -0
  64. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  65. package/types/lib/helpers/display.d.ts +1 -0
  66. package/types/lib/helpers/display.d.ts.map +1 -1
  67. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  68. package/types/lib/helpers/jsonLike.d.ts +4 -0
  69. package/types/lib/helpers/jsonLike.d.ts.map +1 -0
  70. package/types/lib/helpers/mcp.d.ts +29 -0
  71. package/types/lib/helpers/mcp.d.ts.map +1 -0
  72. package/types/lib/helpers/mcpConfigParser.d.ts +30 -0
  73. package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -0
  74. package/types/lib/helpers/mcpDiscovery.d.ts +5 -0
  75. package/types/lib/helpers/mcpDiscovery.d.ts.map +1 -0
  76. package/types/lib/helpers/propertySanitizer.d.ts +3 -0
  77. package/types/lib/helpers/propertySanitizer.d.ts.map +1 -0
  78. package/types/lib/helpers/utils.d.ts +31 -0
  79. package/types/lib/helpers/utils.d.ts.map +1 -1
  80. package/types/lib/managers/binary.d.ts.map +1 -1
  81. package/types/lib/managers/docker.d.ts +3 -0
  82. package/types/lib/managers/docker.d.ts.map +1 -1
  83. package/types/lib/server/server.d.ts +1 -0
  84. package/types/lib/server/server.d.ts.map +1 -1
  85. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  86. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  87. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
@@ -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) => {
@@ -1103,7 +1126,7 @@ export function displaySelfThreatModel(
1103
1126
  options,
1104
1127
  envAuditFindings,
1105
1128
  ) {
1106
- const TLP = options.tlpClassification || "CLEAR";
1129
+ const TLP = options.tlpClassification;
1107
1130
  const risks = [];
1108
1131
  let riskScore = 0;
1109
1132
 
@@ -1242,8 +1265,11 @@ export function displaySelfThreatModel(
1242
1265
  AMBER_AND_STRICT: "Organisation only. No external sharing.",
1243
1266
  RED: "Named recipients only. Do not forward or store beyond session.",
1244
1267
  };
1268
+ const tlpValue = TLP
1269
+ ? `${TLP} — ${tlpGuidance[TLP]}`
1270
+ : "Not set — no distribution constraints recorded.";
1245
1271
  const headerData = [
1246
- ["TLP Classification", `${TLP} — ${tlpGuidance[TLP]}`],
1272
+ ["TLP Classification", tlpValue],
1247
1273
  ["Risk Score", `${riskScore}/10`],
1248
1274
  ["Risk Level", `${riskColor[riskLevel]}${riskLevel}${reset}`],
1249
1275
  ];
@@ -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,74 +23,85 @@ 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: [
29
+ group: "",
30
+ name: "left-pad",
31
+ properties: [
49
32
  {
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
- },
62
- {
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
- );
79
+ ],
80
+ );
81
+ });
82
+
83
+ it("displaySelfThreatModel does not assume a default TLP classification", async () => {
84
+ const tableStub = sinon.stub().returns("table-output");
85
+ try {
86
+ const { displaySelfThreatModel } = await esmock("./display.js", {
87
+ "./table.js": {
88
+ createStream: sinon.stub(),
89
+ table: tableStub,
90
+ },
91
+ "./utils.js": {
92
+ isSecureMode: false,
93
+ safeExistsSync: sinon.stub(),
94
+ toCamel: sinon.stub().callsFake((value) => value),
95
+ },
96
+ });
97
+ displaySelfThreatModel("/workspace/project", {}, {}, []);
98
+ const [headerData] = tableStub.firstCall.args;
99
+ assert.deepStrictEqual(headerData[0], [
100
+ "TLP Classification",
101
+ "Not set — no distribution constraints recorded.",
102
+ ]);
91
103
  } finally {
92
- consoleLogStub.restore();
104
+ sinon.restore();
93
105
  }
94
106
  });
95
107
 
@@ -4,14 +4,17 @@ import process from "node:process";
4
4
 
5
5
  import { v4 as uuidv4 } from "uuid";
6
6
 
7
- import { agentFormulationParser } from "./agentFormulationParser.js";
7
+ import {
8
+ AI_INVENTORY_PROJECT_TYPES,
9
+ collectAiInventory,
10
+ optionIncludesAiInventoryProjectType,
11
+ } from "./aiInventory.js";
8
12
  import { collectOSCryptoLibs } from "./cbomutils.js";
9
13
  import { azurePipelinesParser } from "./ciParsers/azurePipelines.js";
10
14
  import { circleCiParser } from "./ciParsers/circleCi.js";
11
15
  import { githubActionsParser } from "./ciParsers/githubActions.js";
12
16
  import { gitlabCiParser } from "./ciParsers/gitlabCi.js";
13
17
  import { jenkinsParser } from "./ciParsers/jenkins.js";
14
- import { communityAiConfigParser } from "./communityAiConfigParser.js";
15
18
  import { trimComponents } from "./depsUtils.js";
16
19
  import {
17
20
  collectEnvInfo,
@@ -20,7 +23,6 @@ import {
20
23
  gitTreeHashes,
21
24
  listFiles,
22
25
  } from "./envcontext.js";
23
- import { mcpConfigParser } from "./mcpConfigParser.js";
24
26
  import { rustFormulationParser } from "./rustFormulationParser.js";
25
27
  import { scanTextForHiddenUnicode } from "./unicodeScan.js";
26
28
  import { getAllFiles } from "./utils.js";
@@ -100,9 +102,6 @@ function buildReadmeSecurityComponents(discoveryPath, options) {
100
102
  */
101
103
  const _parsers = [
102
104
  rustFormulationParser,
103
- agentFormulationParser,
104
- mcpConfigParser,
105
- communityAiConfigParser,
106
105
  githubActionsParser,
107
106
  gitlabCiParser,
108
107
  jenkinsParser,
@@ -312,6 +311,12 @@ export function addFormulationSection(filePath, options, context = {}) {
312
311
  const ciProperties = [];
313
312
 
314
313
  const discoveryPath = projectPath || ".";
314
+ const excludedInventoryTypes = AI_INVENTORY_PROJECT_TYPES.filter((type) => {
315
+ return optionIncludesAiInventoryProjectType(options?.excludeType, type);
316
+ });
317
+ const includedInventoryTypes = AI_INVENTORY_PROJECT_TYPES.filter(
318
+ (type) => !excludedInventoryTypes.includes(type),
319
+ );
315
320
 
316
321
  for (const parser of _parsers) {
317
322
  const matchedFiles = [];
@@ -355,6 +360,21 @@ export function addFormulationSection(filePath, options, context = {}) {
355
360
  }
356
361
  }
357
362
 
363
+ const aiInventory = collectAiInventory(
364
+ discoveryPath,
365
+ options,
366
+ includedInventoryTypes,
367
+ );
368
+ if (aiInventory.components.length) {
369
+ ciComponents.push(...aiInventory.components);
370
+ }
371
+ if (aiInventory.services.length) {
372
+ ciServices.push(...aiInventory.services);
373
+ }
374
+ if (aiInventory.dependencies.length) {
375
+ dependencies.push(...aiInventory.dependencies);
376
+ }
377
+
358
378
  // Merge CI components into the formulation component list
359
379
  if (ciComponents.length) {
360
380
  components = components.concat(ciComponents);
@@ -1,22 +1,32 @@
1
+ /**
2
+ * Returns whether the quote at `index` is escaped by an odd-length run of
3
+ * backslashes immediately preceding it.
4
+ *
5
+ * @param {string} raw Raw JSON-like text being scanned
6
+ * @param {number} index Index of the quote character to evaluate
7
+ * @returns {boolean} `true` when the quote is escaped and should not terminate
8
+ * the current string literal
9
+ */
10
+ function isEscapedQuote(raw, index) {
11
+ let backslashCount = 0;
12
+ let lookBehind = index - 1;
13
+ while (lookBehind >= 0 && raw[lookBehind] === "\\") {
14
+ backslashCount += 1;
15
+ lookBehind -= 1;
16
+ }
17
+ return backslashCount % 2 === 1;
18
+ }
19
+
1
20
  export function stripJsonComments(raw) {
2
21
  let output = "";
3
22
  let inString = false;
4
23
  let stringQuote = "";
5
- let escaped = false;
6
24
  for (let index = 0; index < raw.length; index++) {
7
25
  const char = raw[index];
8
26
  const nextChar = raw[index + 1];
9
27
  if (inString) {
10
28
  output += char;
11
- if (escaped) {
12
- escaped = false;
13
- continue;
14
- }
15
- if (char === "\\") {
16
- escaped = true;
17
- continue;
18
- }
19
- if (char === stringQuote) {
29
+ if (char === stringQuote && !isEscapedQuote(raw, index)) {
20
30
  inString = false;
21
31
  stringQuote = "";
22
32
  }
@@ -57,20 +67,11 @@ export function stripJsonTrailingCommas(raw) {
57
67
  let output = "";
58
68
  let inString = false;
59
69
  let stringQuote = "";
60
- let escaped = false;
61
70
  for (let index = 0; index < raw.length; index++) {
62
71
  const char = raw[index];
63
72
  if (inString) {
64
73
  output += char;
65
- if (escaped) {
66
- escaped = false;
67
- continue;
68
- }
69
- if (char === "\\") {
70
- escaped = true;
71
- continue;
72
- }
73
- if (char === stringQuote) {
74
+ if (char === stringQuote && !isEscapedQuote(raw, index)) {
74
75
  inString = false;
75
76
  stringQuote = "";
76
77
  }
@@ -0,0 +1,34 @@
1
+ import { assert, describe, it } from "poku";
2
+
3
+ import { parseJsonLike, stripJsonComments } from "./jsonLike.js";
4
+
5
+ describe("jsonLike", () => {
6
+ it("preserves escaped quotes while stripping comments", () => {
7
+ const parsedMessage = 'escaped quote: " // not a comment';
8
+ const rawMessage = String.raw`escaped quote: \" // not a comment`;
9
+ const raw = String.raw`{
10
+ "message": "escaped quote: \" // not a comment",
11
+ // trailing comment
12
+ "enabled": true
13
+ }`;
14
+ const stripped = stripJsonComments(raw);
15
+ assert.ok(stripped.includes(rawMessage));
16
+ assert.ok(!stripped.includes("trailing comment"));
17
+ assert.deepStrictEqual(parseJsonLike(raw), {
18
+ enabled: true,
19
+ message: parsedMessage,
20
+ });
21
+ });
22
+
23
+ it("preserves comment markers after escaped backslashes inside strings", () => {
24
+ const raw = `{
25
+ "path": "C:\\\\\\\\temp\\\\\\\\file // keep",
26
+ /* block comment */
27
+ "count": 1
28
+ }`;
29
+ assert.deepStrictEqual(parseJsonLike(raw), {
30
+ count: 1,
31
+ path: "C:\\\\temp\\\\file // keep",
32
+ });
33
+ });
34
+ });
@@ -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) {
@@ -214,6 +227,9 @@ function detectConfigCredentialSignals(serverConfig) {
214
227
  }
215
228
  }
216
229
  return {
230
+ credentialIndicatorCount: inlineIndicators.size,
231
+ credentialReferenceCount: credentialRefs.size,
232
+ exposureFieldCount: exposureFields.size,
217
233
  credentialRefs: Array.from(credentialRefs).sort(),
218
234
  exposureFields: Array.from(exposureFields).sort(),
219
235
  inlineIndicators: Array.from(inlineIndicators).sort(),
@@ -377,6 +393,9 @@ function createServiceFromConfig(
377
393
  const command = normalized.command;
378
394
  const args = normalized.args;
379
395
  const endpoints = extractEndpoints(serverConfig);
396
+ const sanitizedEndpoints = endpoints.map((endpoint) =>
397
+ sanitizeBomUrl(endpoint),
398
+ );
380
399
  const transport = inferTransport(serverConfig, endpoints);
381
400
  const authHints = authHintsFromValue(serverConfig);
382
401
  const authPosture = authPostureForConfig(serverConfig, endpoints, authHints);
@@ -393,7 +412,7 @@ function createServiceFromConfig(
393
412
  JSON.stringify({
394
413
  args,
395
414
  command,
396
- endpoints,
415
+ endpoints: sanitizedEndpoints,
397
416
  env: serverConfig?.env || serverConfig?.environment || {},
398
417
  }),
399
418
  );
@@ -468,22 +487,22 @@ function createServiceFromConfig(
468
487
  addUniqueProperty(properties, "cdx:mcp:credentialExposure", "true");
469
488
  addUniqueProperty(
470
489
  properties,
471
- "cdx:mcp:credentialRiskIndicators",
472
- credentialSignals.inlineIndicators.join(","),
490
+ "cdx:mcp:credentialIndicatorCount",
491
+ String(credentialSignals.credentialIndicatorCount),
473
492
  );
474
493
  }
475
494
  if (credentialSignals.exposureFields.length) {
476
495
  addUniqueProperty(
477
496
  properties,
478
- "cdx:mcp:credentialExposureFields",
479
- credentialSignals.exposureFields.join(","),
497
+ "cdx:mcp:credentialExposureFieldCount",
498
+ String(credentialSignals.exposureFieldCount),
480
499
  );
481
500
  }
482
501
  if (credentialSignals.credentialRefs.length) {
483
502
  addUniqueProperty(
484
503
  properties,
485
- "cdx:mcp:credentialRefs",
486
- credentialSignals.credentialRefs.join(","),
504
+ "cdx:mcp:credentialReferenceCount",
505
+ String(credentialSignals.credentialReferenceCount),
487
506
  );
488
507
  }
489
508
  if (supportsDcr) {
@@ -531,7 +550,7 @@ function createServiceFromConfig(
531
550
  return {
532
551
  "bom-ref": `urn:service:mcp:${sanitizeMcpRefToken(serviceName)}:${sanitizeMcpRefToken(version)}`,
533
552
  authenticated,
534
- endpoints,
553
+ endpoints: sanitizedEndpoints,
535
554
  group: "mcp",
536
555
  name: serviceName,
537
556
  properties,
@@ -593,11 +612,8 @@ function createConfigComponent(filePath, format, raw, services) {
593
612
  addUniqueProperty(properties, "cdx:mcp:credentialExposure", "true");
594
613
  addUniqueProperty(
595
614
  properties,
596
- "cdx:mcp:credentialExposedServices",
597
- credentialServices
598
- .map((service) => service.name)
599
- .sort()
600
- .join(","),
615
+ "cdx:mcp:credentialExposedServiceCount",
616
+ String(credentialServices.length),
601
617
  );
602
618
  }
603
619
  return {
@@ -56,4 +56,108 @@ describe("mcpConfigParser", () => {
56
56
  syntax: "json",
57
57
  });
58
58
  });
59
+
60
+ it("records credential exposure without embedding raw secret metadata", async () => {
61
+ const readFileSync = sinon.stub();
62
+ const scanTextForHiddenUnicode = sinon.stub().returns({
63
+ hasHiddenUnicode: false,
64
+ });
65
+ readFileSync.withArgs("/repo/.vscode/mcp.json", "utf-8").returns(
66
+ JSON.stringify({
67
+ mcpServers: {
68
+ releaseDocs: {
69
+ args: [
70
+ "--token",
71
+ "sk_test_super_secret_value",
72
+ "https://user:pass@docs.example.com/mcp?access_token=secret#frag",
73
+ ],
74
+ command: "npx",
75
+ env: {
76
+ API_KEY: "$" + "{API_KEY}",
77
+ },
78
+ headers: {
79
+ Authorization: "Bearer sk_test_another_secret_value",
80
+ },
81
+ transport: "http",
82
+ },
83
+ },
84
+ }),
85
+ );
86
+ const { mcpConfigParser } = await esmock("./mcpConfigParser.js", {
87
+ "node:fs": { readFileSync },
88
+ "./unicodeScan.js": { scanTextForHiddenUnicode },
89
+ });
90
+
91
+ const result = mcpConfigParser.parse(["/repo/.vscode/mcp.json"]);
92
+ const service = result.services[0];
93
+ const component = result.components[0];
94
+
95
+ assert.strictEqual(getProp(service, "cdx:mcp:credentialExposure"), "true");
96
+ assert.strictEqual(
97
+ getProp(service, "cdx:mcp:credentialIndicatorCount"),
98
+ "3",
99
+ );
100
+ assert.strictEqual(
101
+ getProp(service, "cdx:mcp:credentialExposureFieldCount"),
102
+ "4",
103
+ );
104
+ assert.strictEqual(
105
+ getProp(service, "cdx:mcp:credentialReferenceCount"),
106
+ "1",
107
+ );
108
+ assert.strictEqual(
109
+ getProp(component, "cdx:mcp:credentialExposedServiceCount"),
110
+ "1",
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
+ );
118
+ assert.strictEqual(
119
+ getProp(service, "cdx:mcp:credentialRiskIndicators"),
120
+ undefined,
121
+ );
122
+ assert.strictEqual(
123
+ getProp(service, "cdx:mcp:credentialExposureFields"),
124
+ undefined,
125
+ );
126
+ assert.strictEqual(getProp(service, "cdx:mcp:credentialRefs"), undefined);
127
+ assert.strictEqual(
128
+ getProp(component, "cdx:mcp:credentialExposedServices"),
129
+ undefined,
130
+ );
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
+ });
59
163
  });