@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
@@ -7,6 +7,7 @@ import {
7
7
  providerNamesForText,
8
8
  sanitizeMcpRefToken,
9
9
  } from "./mcpDiscovery.js";
10
+ import { sanitizeBomUrl } from "./propertySanitizer.js";
10
11
  import { scanTextForHiddenUnicode } from "./unicodeScan.js";
11
12
 
12
13
  const AGENT_FILE_PATTERNS = [
@@ -125,7 +126,7 @@ function buildInferredMcpServices(filePath, mcpUrls, authHints, providerNames) {
125
126
  "bom-ref": `urn:service:agent-mcp:${sanitizeMcpRefToken(hostname || basename(filePath))}:${index + 1}`,
126
127
  group: "mcp",
127
128
  name: hostname || `${basename(filePath)}-mcp-endpoint`,
128
- endpoints: [urlValue],
129
+ endpoints: [sanitizeBomUrl(urlValue)],
129
130
  properties,
130
131
  version: "inferred",
131
132
  };
@@ -167,7 +168,10 @@ export const agentFormulationParser = {
167
168
  packageRefs.length > 0 ||
168
169
  /\bmcp\b/i.test(raw) ||
169
170
  /modelcontextprotocol/i.test(raw);
170
- if (!hiddenUnicodeScan.hasHiddenUnicode && !hasMcpReferences) {
171
+ // Inventory all matched agent/instruction files, even when they do not
172
+ // yet contain hidden Unicode or explicit MCP references, so shipped files
173
+ // such as CLAUDE.md and AGENTS.md still surface in build/post-build BOMs.
174
+ if (!raw.trim().length) {
171
175
  continue;
172
176
  }
173
177
  const hiddenComponentKinds = [];
@@ -230,10 +234,13 @@ export const agentFormulationParser = {
230
234
  }
231
235
  }
232
236
  if (mcpUrls.length) {
237
+ const sanitizedMcpUrls = mcpUrls.map((urlValue) =>
238
+ sanitizeBomUrl(urlValue),
239
+ );
233
240
  properties.push(
234
241
  {
235
242
  name: "cdx:agent:hiddenMcpUrls",
236
- value: mcpUrls.join(","),
243
+ value: sanitizedMcpUrls.join(","),
237
244
  },
238
245
  {
239
246
  name: "cdx:agent:hiddenMcpHosts",
@@ -0,0 +1,42 @@
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("agentFormulationParser", () => {
10
+ it("sanitizes inferred MCP URLs before emitting them", async () => {
11
+ const readFileSync = sinon.stub();
12
+ const scanTextForHiddenUnicode = sinon.stub().returns({
13
+ hasHiddenUnicode: false,
14
+ });
15
+ readFileSync
16
+ .withArgs("/repo/AGENTS.md", "utf-8")
17
+ .returns(
18
+ [
19
+ "Use the remote MCP endpoint at",
20
+ "https://user:pass@example.com/mcp?access_token=abc#frag",
21
+ "during release preparation.",
22
+ ].join(" "),
23
+ );
24
+ const { agentFormulationParser } = await esmock(
25
+ "./agentFormulationParser.js",
26
+ {
27
+ "node:fs": { readFileSync },
28
+ "./unicodeScan.js": { scanTextForHiddenUnicode },
29
+ },
30
+ );
31
+
32
+ const result = agentFormulationParser.parse(["/repo/AGENTS.md"]);
33
+
34
+ assert.strictEqual(
35
+ getProp(result.components[0], "cdx:agent:hiddenMcpUrls"),
36
+ "https://example.com/mcp",
37
+ );
38
+ assert.deepStrictEqual(result.services[0].endpoints, [
39
+ "https://example.com/mcp",
40
+ ]);
41
+ });
42
+ });
@@ -0,0 +1,262 @@
1
+ import { agentFormulationParser } from "./agentFormulationParser.js";
2
+ import { communityAiConfigParser } from "./communityAiConfigParser.js";
3
+ import { mergeServices, trimComponents } from "./depsUtils.js";
4
+ import { classifyMcpReference } from "./mcp.js";
5
+ import { mcpConfigParser } from "./mcpConfigParser.js";
6
+ import { getAllFiles } from "./utils.js";
7
+
8
+ export const AI_INVENTORY_PROJECT_TYPES = ["mcp", "ai-skill"];
9
+ export const AI_INSTRUCTION_FILE_KINDS = new Set([
10
+ "agent-config",
11
+ "agent-definition",
12
+ "agent-instructions",
13
+ "ai-agent-file",
14
+ "copilot-instructions",
15
+ "copilot-setup-workflow",
16
+ "crew-agent",
17
+ "crew-task",
18
+ "crew-tool",
19
+ "custom-command",
20
+ "custom-tool",
21
+ "graph-definition",
22
+ ]);
23
+ export const AI_SKILL_FILE_KIND = "skill-file";
24
+ export const MCP_CONFIG_FILE_KIND = "mcp-config";
25
+
26
+ const AI_INVENTORY_FILE_KINDS = new Set([
27
+ "agent-config",
28
+ "agent-definition",
29
+ "agent-instructions",
30
+ "ai-agent-file",
31
+ "copilot-instructions",
32
+ "copilot-setup-workflow",
33
+ "crew-agent",
34
+ "crew-task",
35
+ "crew-tool",
36
+ "custom-command",
37
+ "custom-tool",
38
+ "graph-definition",
39
+ AI_SKILL_FILE_KIND,
40
+ ]);
41
+
42
+ const AI_INVENTORY_PARSERS = [
43
+ {
44
+ id: agentFormulationParser.id,
45
+ parser: agentFormulationParser,
46
+ types: ["mcp", "ai-skill"],
47
+ },
48
+ {
49
+ id: mcpConfigParser.id,
50
+ parser: mcpConfigParser,
51
+ types: ["mcp"],
52
+ },
53
+ {
54
+ id: communityAiConfigParser.id,
55
+ parser: communityAiConfigParser,
56
+ types: ["ai-skill"],
57
+ },
58
+ ];
59
+
60
+ export function inventoryPropertyValue(subject, name) {
61
+ return subject?.properties?.find((property) => property.name === name)?.value;
62
+ }
63
+
64
+ function hasPropertyPrefix(subject, prefix) {
65
+ return (subject?.properties || []).some((property) =>
66
+ property?.name?.startsWith(prefix),
67
+ );
68
+ }
69
+
70
+ function uniqueNonEmptyTypes(types) {
71
+ return [...new Set((types || []).filter(Boolean))];
72
+ }
73
+
74
+ export function optionIncludesAiInventoryProjectType(optionValue, type) {
75
+ const values = Array.isArray(optionValue)
76
+ ? optionValue
77
+ : optionValue
78
+ ? [optionValue]
79
+ : [];
80
+ return values.some((value) => {
81
+ const normalizedValue = String(value).toLowerCase();
82
+ if (type === "ai-skill") {
83
+ return ["ai-skill", "skill", "skills"].includes(normalizedValue);
84
+ }
85
+ return normalizedValue === type;
86
+ });
87
+ }
88
+
89
+ export function inventoryTypesForSubject(subject) {
90
+ const types = new Set();
91
+ const fileKind = inventoryPropertyValue(subject, "cdx:file:kind");
92
+ if (
93
+ subject?.group === "mcp" ||
94
+ classifyMcpReference(subject).isMcp ||
95
+ hasPropertyPrefix(subject, "cdx:mcp:") ||
96
+ (subject?.tags || []).some((tag) => String(tag || "").startsWith("mcp"))
97
+ ) {
98
+ types.add("mcp");
99
+ }
100
+ if (
101
+ AI_INVENTORY_FILE_KINDS.has(fileKind) ||
102
+ hasPropertyPrefix(subject, "cdx:agent:") ||
103
+ hasPropertyPrefix(subject, "cdx:skill:") ||
104
+ hasPropertyPrefix(subject, "cdx:tool:") ||
105
+ hasPropertyPrefix(subject, "cdx:langgraph:") ||
106
+ hasPropertyPrefix(subject, "cdx:crewai:")
107
+ ) {
108
+ types.add("ai-skill");
109
+ }
110
+ if (
111
+ inventoryPropertyValue(subject, "cdx:mcp:inventorySource") === "agent-file"
112
+ ) {
113
+ types.add("ai-skill");
114
+ }
115
+ return Array.from(types);
116
+ }
117
+
118
+ export function matchesAiInventoryType(subject, type) {
119
+ return inventoryTypesForSubject(subject).includes(type);
120
+ }
121
+
122
+ export function matchesAiInventoryExcludeType(subject, type) {
123
+ if (type === "mcp") {
124
+ const fileKind = inventoryPropertyValue(subject, "cdx:file:kind");
125
+ return (
126
+ fileKind === MCP_CONFIG_FILE_KIND ||
127
+ subject?.group === "mcp" ||
128
+ inventoryPropertyValue(subject, "cdx:mcp:inventorySource") !==
129
+ undefined ||
130
+ inventoryPropertyValue(subject, "cdx:mcp:role") !== undefined
131
+ );
132
+ }
133
+ return matchesAiInventoryType(subject, type);
134
+ }
135
+
136
+ export function filterInventorySubjectsByTypes(subjects, types) {
137
+ const allowedTypes = uniqueNonEmptyTypes(types);
138
+ if (!allowedTypes.length) {
139
+ return [];
140
+ }
141
+ return (subjects || []).filter((subject) =>
142
+ inventoryTypesForSubject(subject).some((type) =>
143
+ allowedTypes.includes(type),
144
+ ),
145
+ );
146
+ }
147
+
148
+ export function filterInventoryDependencies(
149
+ dependencies,
150
+ components,
151
+ services,
152
+ ) {
153
+ const allowedRefs = new Set(
154
+ []
155
+ .concat(components || [])
156
+ .concat(services || [])
157
+ .map((subject) => subject?.["bom-ref"])
158
+ .filter(Boolean),
159
+ );
160
+ return (dependencies || [])
161
+ .filter((dependency) => allowedRefs.has(dependency.ref))
162
+ .map((dependency) => {
163
+ const filteredDependency = {
164
+ ref: dependency.ref,
165
+ };
166
+ if (dependency.dependsOn?.length) {
167
+ filteredDependency.dependsOn = dependency.dependsOn.filter((ref) =>
168
+ allowedRefs.has(ref),
169
+ );
170
+ }
171
+ if (dependency.provides?.length) {
172
+ filteredDependency.provides = dependency.provides.filter((ref) =>
173
+ allowedRefs.has(ref),
174
+ );
175
+ }
176
+ return filteredDependency;
177
+ });
178
+ }
179
+
180
+ export function collectAiInventory(discoveryPath, options, types) {
181
+ const requestedTypes = uniqueNonEmptyTypes(types);
182
+ if (!requestedTypes.length) {
183
+ return { components: [], dependencies: [], services: [] };
184
+ }
185
+ let components = [];
186
+ const dependencies = [];
187
+ let services = [];
188
+ for (const parserEntry of AI_INVENTORY_PARSERS) {
189
+ if (!parserEntry.types.some((type) => requestedTypes.includes(type))) {
190
+ continue;
191
+ }
192
+ const matchedFiles = [];
193
+ for (const pattern of parserEntry.parser.patterns) {
194
+ const found = getAllFiles(discoveryPath, pattern, options);
195
+ if (found?.length) {
196
+ matchedFiles.push(...found);
197
+ }
198
+ }
199
+ const uniqueMatchedFiles = [...new Set(matchedFiles)];
200
+ if (!uniqueMatchedFiles.length) {
201
+ continue;
202
+ }
203
+ let result;
204
+ try {
205
+ result = parserEntry.parser.parse(uniqueMatchedFiles, options);
206
+ } catch (err) {
207
+ console.warn(
208
+ `[aiInventory] Parser "${parserEntry.id}" threw an error:`,
209
+ err.message,
210
+ );
211
+ continue;
212
+ }
213
+ if (result?.components?.length) {
214
+ components = components.concat(result.components);
215
+ }
216
+ if (result?.services?.length) {
217
+ services = mergeServices(services, result.services);
218
+ }
219
+ if (result?.dependencies?.length) {
220
+ dependencies.push(...result.dependencies);
221
+ }
222
+ }
223
+ components = trimComponents(
224
+ filterInventorySubjectsByTypes(components, requestedTypes),
225
+ );
226
+ services = mergeServices(
227
+ [],
228
+ filterInventorySubjectsByTypes(services, requestedTypes),
229
+ );
230
+ return {
231
+ components,
232
+ dependencies: filterInventoryDependencies(
233
+ dependencies,
234
+ components,
235
+ services,
236
+ ),
237
+ services,
238
+ };
239
+ }
240
+
241
+ export function summarizeAiInventory(inventory) {
242
+ const components = inventory?.components || [];
243
+ const services = inventory?.services || [];
244
+ return {
245
+ instructionCount: components.filter((component) =>
246
+ AI_INSTRUCTION_FILE_KINDS.has(
247
+ inventoryPropertyValue(component, "cdx:file:kind"),
248
+ ),
249
+ ).length,
250
+ mcpConfigCount: components.filter(
251
+ (component) =>
252
+ inventoryPropertyValue(component, "cdx:file:kind") ===
253
+ MCP_CONFIG_FILE_KIND,
254
+ ).length,
255
+ mcpServiceCount: services.length,
256
+ skillCount: components.filter(
257
+ (component) =>
258
+ inventoryPropertyValue(component, "cdx:file:kind") ===
259
+ AI_SKILL_FILE_KIND,
260
+ ).length,
261
+ };
262
+ }
@@ -0,0 +1,111 @@
1
+ import { assert, describe, it } from "poku";
2
+
3
+ import {
4
+ filterInventoryDependencies,
5
+ inventoryTypesForSubject,
6
+ matchesAiInventoryExcludeType,
7
+ matchesAiInventoryType,
8
+ summarizeAiInventory,
9
+ } from "./aiInventory.js";
10
+
11
+ describe("aiInventory", () => {
12
+ it("classifies agent-derived MCP services as both mcp and ai-skill", () => {
13
+ const service = {
14
+ "bom-ref": "urn:service:agent-mcp:demo:1",
15
+ group: "mcp",
16
+ properties: [
17
+ { name: "cdx:mcp:inventorySource", value: "agent-file" },
18
+ { name: "cdx:mcp:serviceType", value: "inferred-endpoint" },
19
+ ],
20
+ };
21
+ assert.deepStrictEqual(inventoryTypesForSubject(service).sort(), [
22
+ "ai-skill",
23
+ "mcp",
24
+ ]);
25
+ assert.strictEqual(matchesAiInventoryType(service, "mcp"), true);
26
+ assert.strictEqual(matchesAiInventoryType(service, "ai-skill"), true);
27
+ });
28
+
29
+ it("limits MCP exclusion matching to AI inventory services, files, and primitives", () => {
30
+ const mcpPackage = {
31
+ "bom-ref": "pkg:npm/@modelcontextprotocol/server-filesystem@1.0.0",
32
+ name: "@modelcontextprotocol/server-filesystem",
33
+ purl: "pkg:npm/%40modelcontextprotocol/server-filesystem@1.0.0",
34
+ };
35
+ const mcpPrimitive = {
36
+ "bom-ref": "urn:mcp:tool:docs:search",
37
+ properties: [{ name: "cdx:mcp:role", value: "tool" }],
38
+ tags: ["mcp", "mcp-tool"],
39
+ };
40
+ const mcpConfig = {
41
+ "bom-ref": "file:/repo/.vscode/mcp.json",
42
+ properties: [{ name: "cdx:file:kind", value: "mcp-config" }],
43
+ type: "file",
44
+ };
45
+ const mcpService = {
46
+ "bom-ref": "urn:service:mcp:docs:latest",
47
+ group: "mcp",
48
+ properties: [{ name: "cdx:mcp:inventorySource", value: "config-file" }],
49
+ };
50
+ assert.strictEqual(matchesAiInventoryExcludeType(mcpPackage, "mcp"), false);
51
+ assert.strictEqual(
52
+ matchesAiInventoryExcludeType(mcpPrimitive, "mcp"),
53
+ true,
54
+ );
55
+ assert.strictEqual(matchesAiInventoryExcludeType(mcpConfig, "mcp"), true);
56
+ assert.strictEqual(matchesAiInventoryExcludeType(mcpService, "mcp"), true);
57
+ });
58
+
59
+ it("filters dependencies to retained component and service refs", () => {
60
+ const components = [{ "bom-ref": "file:/repo/CLAUDE.md" }];
61
+ const services = [{ "bom-ref": "urn:service:mcp:docs:latest" }];
62
+ const filtered = filterInventoryDependencies(
63
+ [
64
+ {
65
+ ref: "urn:service:mcp:docs:latest",
66
+ provides: ["file:/repo/CLAUDE.md", "urn:service:mcp:other:latest"],
67
+ },
68
+ {
69
+ ref: "urn:service:mcp:missing:latest",
70
+ provides: ["file:/repo/CLAUDE.md"],
71
+ },
72
+ ],
73
+ components,
74
+ services,
75
+ );
76
+ assert.deepStrictEqual(filtered, [
77
+ {
78
+ ref: "urn:service:mcp:docs:latest",
79
+ provides: ["file:/repo/CLAUDE.md"],
80
+ },
81
+ ]);
82
+ });
83
+
84
+ it("summarizes AI inventory counts for instructions, skills, configs, and services", () => {
85
+ const summary = summarizeAiInventory({
86
+ components: [
87
+ {
88
+ properties: [{ name: "cdx:file:kind", value: "agent-instructions" }],
89
+ },
90
+ {
91
+ properties: [
92
+ { name: "cdx:file:kind", value: "copilot-instructions" },
93
+ ],
94
+ },
95
+ {
96
+ properties: [{ name: "cdx:file:kind", value: "skill-file" }],
97
+ },
98
+ {
99
+ properties: [{ name: "cdx:file:kind", value: "mcp-config" }],
100
+ },
101
+ ],
102
+ services: [{ name: "releaseDocs" }, { name: "deployBot" }],
103
+ });
104
+ assert.deepStrictEqual(summary, {
105
+ instructionCount: 2,
106
+ mcpConfigCount: 1,
107
+ mcpServiceCount: 2,
108
+ skillCount: 1,
109
+ });
110
+ });
111
+ });