@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
@@ -24,7 +24,12 @@ const WORKFLOWS_DIR = join(
24
24
  "workflows",
25
25
  );
26
26
 
27
- function makeBom(components = [], workflows = [], formulationComponents = []) {
27
+ function makeBom(
28
+ components = [],
29
+ workflows = [],
30
+ formulationComponents = [],
31
+ services = [],
32
+ ) {
28
33
  const formulationEntry = {};
29
34
  if (formulationComponents.length) {
30
35
  formulationEntry.components = formulationComponents;
@@ -54,6 +59,7 @@ function makeBom(components = [], workflows = [], formulationComponents = []) {
54
59
  },
55
60
  },
56
61
  components,
62
+ services,
57
63
  formulation:
58
64
  workflows.length || formulationComponents.length
59
65
  ? [formulationEntry]
@@ -136,6 +142,10 @@ describe("loadRules", () => {
136
142
  containerRiskRules.length > 0,
137
143
  "Should have container risk rules",
138
144
  );
145
+ const mcpRules = rules.filter((r) => r.category === "mcp-server");
146
+ assert.ok(mcpRules.length > 0, "Should have MCP server rules");
147
+ const agentRules = rules.filter((r) => r.category === "ai-agent");
148
+ assert.ok(agentRules.length > 0, "Should have AI agent rules");
139
149
  });
140
150
  });
141
151
 
@@ -231,6 +241,418 @@ describe("evaluateRule", () => {
231
241
  assert.deepStrictEqual(findings[0].attackTechniques, ["T1528"]);
232
242
  });
233
243
 
244
+ it("should detect unauthenticated MCP tool exposure (MCP-001)", async () => {
245
+ const rules = await loadRules(RULES_DIR);
246
+ const rule = rules.find((r) => r.id === "MCP-001");
247
+ assert.ok(rule, "MCP-001 rule should exist");
248
+
249
+ const bom = makeBom(
250
+ [],
251
+ [],
252
+ [],
253
+ [
254
+ {
255
+ "bom-ref": "urn:service:mcp:unsafe-http:1.0.0",
256
+ name: "unsafe-http",
257
+ version: "1.0.0",
258
+ endpoints: ["/mcp-unsafe"],
259
+ authenticated: false,
260
+ properties: [
261
+ { name: "SrcFile", value: "src/unsafe.js" },
262
+ { name: "cdx:mcp:transport", value: "streamable-http" },
263
+ { name: "cdx:mcp:capabilities:tools", value: "true" },
264
+ { name: "cdx:mcp:toolCount", value: "1" },
265
+ { name: "cdx:mcp:officialSdk", value: "false" },
266
+ ],
267
+ },
268
+ ],
269
+ );
270
+
271
+ const findings = await evaluateRule(rule, bom);
272
+ assert.ok(findings.length > 0, "Should detect unauthenticated MCP tools");
273
+ assert.strictEqual(findings[0].severity, "critical");
274
+ });
275
+
276
+ it("should detect a network-exposed non-official MCP server (MCP-003)", async () => {
277
+ const rules = await loadRules(RULES_DIR);
278
+ const rule = rules.find((r) => r.id === "MCP-003");
279
+ assert.ok(rule, "MCP-003 rule should exist");
280
+
281
+ const bom = makeBom(
282
+ [],
283
+ [],
284
+ [],
285
+ [
286
+ {
287
+ "bom-ref": "urn:service:mcp:custom-wrapper:0.1.0",
288
+ name: "custom-wrapper",
289
+ version: "0.1.0",
290
+ endpoints: ["http://localhost:4000/mcp"],
291
+ authenticated: true,
292
+ properties: [
293
+ { name: "SrcFile", value: "src/custom.js" },
294
+ { name: "cdx:mcp:transport", value: "streamable-http" },
295
+ { name: "cdx:mcp:officialSdk", value: "false" },
296
+ { name: "cdx:mcp:toolCount", value: "2" },
297
+ { name: "cdx:mcp:sdkImports", value: "@acme/mcp-server" },
298
+ ],
299
+ },
300
+ ],
301
+ );
302
+
303
+ const findings = await evaluateRule(rule, bom);
304
+ assert.ok(findings.length > 0, "Should detect non-official MCP wrapper");
305
+ assert.strictEqual(findings[0].severity, "medium");
306
+ });
307
+
308
+ it("should detect hidden Unicode in AI agent files (AGT-001)", async () => {
309
+ const rules = await loadRules(RULES_DIR);
310
+ const rule = rules.find((r) => r.id === "AGT-001");
311
+ assert.ok(rule, "AGT-001 rule should exist");
312
+
313
+ const bom = makeBom(
314
+ [],
315
+ [],
316
+ [
317
+ {
318
+ "bom-ref": "file:/repo/AGENTS.md",
319
+ name: "AGENTS.md",
320
+ type: "file",
321
+ properties: [
322
+ { name: "SrcFile", value: "/repo/AGENTS.md" },
323
+ { name: "cdx:agent:inventorySource", value: "agent-file" },
324
+ { name: "cdx:file:hasHiddenUnicode", value: "true" },
325
+ { name: "cdx:file:hiddenUnicodeCodePoints", value: "U+200B" },
326
+ { name: "cdx:file:hiddenUnicodeLineNumbers", value: "4" },
327
+ ],
328
+ },
329
+ ],
330
+ );
331
+
332
+ const findings = await evaluateRule(rule, bom);
333
+ assert.ok(
334
+ findings.length > 0,
335
+ "Should detect hidden Unicode in agent file",
336
+ );
337
+ assert.ok(findings[0].standards?.["owasp-ai-top-10"]?.length);
338
+ });
339
+
340
+ it("should detect public MCP endpoint references in AI agent files (AGT-002)", async () => {
341
+ const rules = await loadRules(RULES_DIR);
342
+ const rule = rules.find((r) => r.id === "AGT-002");
343
+ assert.ok(rule, "AGT-002 rule should exist");
344
+
345
+ const bom = makeBom(
346
+ [],
347
+ [],
348
+ [
349
+ {
350
+ "bom-ref": "file:/repo/.github/copilot-instructions.md",
351
+ name: "copilot-instructions.md",
352
+ type: "file",
353
+ properties: [
354
+ {
355
+ name: "SrcFile",
356
+ value: "/repo/.github/copilot-instructions.md",
357
+ },
358
+ { name: "cdx:agent:inventorySource", value: "agent-file" },
359
+ { name: "cdx:agent:hasPublicMcpEndpoint", value: "true" },
360
+ {
361
+ name: "cdx:agent:hiddenMcpUrls",
362
+ value: "https://demo.ngrok-free.app/mcp",
363
+ },
364
+ {
365
+ name: "cdx:agent:hiddenMcpHosts",
366
+ value: "demo.ngrok-free.app",
367
+ },
368
+ ],
369
+ },
370
+ ],
371
+ );
372
+
373
+ const findings = await evaluateRule(rule, bom);
374
+ assert.ok(findings.length > 0, "Should detect public MCP endpoint risk");
375
+ assert.strictEqual(findings[0].severity, "high");
376
+ });
377
+
378
+ it("should detect undeclared MCP references in AI agent files (AGT-003)", async () => {
379
+ const rules = await loadRules(RULES_DIR);
380
+ const rule = rules.find((r) => r.id === "AGT-003");
381
+ assert.ok(rule, "AGT-003 rule should exist");
382
+
383
+ const bom = makeBom(
384
+ [],
385
+ [],
386
+ [
387
+ {
388
+ "bom-ref": "file:/repo/AGENTS.md",
389
+ name: "AGENTS.md",
390
+ type: "file",
391
+ properties: [
392
+ { name: "SrcFile", value: "/repo/AGENTS.md" },
393
+ { name: "cdx:agent:inventorySource", value: "agent-file" },
394
+ { name: "cdx:agent:hasMcpReferences", value: "true" },
395
+ {
396
+ name: "cdx:agent:mcpPackageRefs",
397
+ value: "@acme/mcp-server",
398
+ },
399
+ {
400
+ name: "cdx:agent:hiddenMcpUrls",
401
+ value: "http://localhost:3000/mcp",
402
+ },
403
+ ],
404
+ },
405
+ ],
406
+ );
407
+
408
+ const findings = await evaluateRule(rule, bom);
409
+ assert.ok(findings.length > 0, "Should detect undeclared MCP references");
410
+ });
411
+
412
+ it("should detect tunneled MCP references in AI agent files (AGT-004)", async () => {
413
+ const rules = await loadRules(RULES_DIR);
414
+ const rule = rules.find((r) => r.id === "AGT-004");
415
+ assert.ok(rule, "AGT-004 rule should exist");
416
+
417
+ const bom = makeBom(
418
+ [],
419
+ [],
420
+ [
421
+ {
422
+ "bom-ref": "file:/repo/AGENTS.md",
423
+ name: "AGENTS.md",
424
+ type: "file",
425
+ properties: [
426
+ { name: "SrcFile", value: "/repo/AGENTS.md" },
427
+ { name: "cdx:agent:inventorySource", value: "agent-file" },
428
+ { name: "cdx:agent:hasTunnelReference", value: "true" },
429
+ {
430
+ name: "cdx:agent:hiddenMcpUrls",
431
+ value: "https://demo.ngrok-free.app/mcp",
432
+ },
433
+ ],
434
+ },
435
+ ],
436
+ );
437
+
438
+ const findings = await evaluateRule(rule, bom);
439
+ assert.ok(findings.length > 0, "Should detect tunnel exposure");
440
+ });
441
+
442
+ it("should detect inline credentials in AI agent files (AGT-006)", async () => {
443
+ const rules = await loadRules(RULES_DIR);
444
+ const rule = rules.find((r) => r.id === "AGT-006");
445
+ assert.ok(rule, "AGT-006 rule should exist");
446
+
447
+ const bom = makeBom(
448
+ [],
449
+ [],
450
+ [
451
+ {
452
+ "bom-ref": "file:/repo/AGENTS.md",
453
+ name: "AGENTS.md",
454
+ type: "file",
455
+ properties: [
456
+ { name: "SrcFile", value: "/repo/AGENTS.md" },
457
+ { name: "cdx:agent:inventorySource", value: "agent-file" },
458
+ { name: "cdx:agent:credentialExposure", value: "true" },
459
+ {
460
+ name: "cdx:agent:credentialRiskIndicators",
461
+ value: "generic-secret,bearer-token",
462
+ },
463
+ ],
464
+ },
465
+ ],
466
+ );
467
+
468
+ const findings = await evaluateRule(rule, bom);
469
+ assert.ok(findings.length > 0, "Should detect inline credentials");
470
+ assert.strictEqual(findings[0].severity, "critical");
471
+ });
472
+
473
+ it("should detect unauthenticated configured MCP endpoints (MCP-004)", async () => {
474
+ const rules = await loadRules(RULES_DIR);
475
+ const rule = rules.find((r) => r.id === "MCP-004");
476
+ assert.ok(rule, "MCP-004 rule should exist");
477
+
478
+ const bom = makeBom(
479
+ [],
480
+ [],
481
+ [],
482
+ [
483
+ {
484
+ "bom-ref": "urn:service:mcp:gateway:latest",
485
+ name: "gateway",
486
+ version: "latest",
487
+ endpoints: ["https://demo.ngrok-free.app/mcp"],
488
+ authenticated: false,
489
+ properties: [
490
+ { name: "SrcFile", value: "/repo/.vscode/mcp.json" },
491
+ { name: "cdx:mcp:inventorySource", value: "config-file" },
492
+ { name: "cdx:mcp:transport", value: "streamable-http" },
493
+ { name: "cdx:mcp:configFormat", value: "vscode" },
494
+ { name: "cdx:mcp:configKey", value: "mcpServers.gateway" },
495
+ { name: "cdx:mcp:trustProfile", value: "review-needed" },
496
+ ],
497
+ },
498
+ ],
499
+ );
500
+
501
+ const findings = await evaluateRule(rule, bom);
502
+ assert.ok(
503
+ findings.length > 0,
504
+ "Should detect unauthenticated config endpoint",
505
+ );
506
+ });
507
+
508
+ it("should detect inline credential exposure in MCP config services (MCP-005)", async () => {
509
+ const rules = await loadRules(RULES_DIR);
510
+ const rule = rules.find((r) => r.id === "MCP-005");
511
+ assert.ok(rule, "MCP-005 rule should exist");
512
+
513
+ const bom = makeBom(
514
+ [],
515
+ [],
516
+ [],
517
+ [
518
+ {
519
+ "bom-ref": "urn:service:mcp:gateway:latest",
520
+ name: "gateway",
521
+ version: "latest",
522
+ properties: [
523
+ { name: "SrcFile", value: "/repo/.vscode/mcp.json" },
524
+ { name: "cdx:mcp:inventorySource", value: "config-file" },
525
+ { name: "cdx:mcp:credentialExposure", value: "true" },
526
+ {
527
+ name: "cdx:mcp:credentialExposureFieldCount",
528
+ value: "2",
529
+ },
530
+ {
531
+ name: "cdx:mcp:credentialIndicatorCount",
532
+ value: "2",
533
+ },
534
+ { name: "cdx:mcp:credentialReferenceCount", value: "1" },
535
+ ],
536
+ },
537
+ ],
538
+ );
539
+
540
+ const findings = await evaluateRule(rule, bom);
541
+ assert.ok(findings.length > 0, "Should detect config credential exposure");
542
+ assert.strictEqual(findings[0].severity, "critical");
543
+ });
544
+
545
+ it("should detect confused-deputy risk in MCP config services (MCP-006)", async () => {
546
+ const rules = await loadRules(RULES_DIR);
547
+ const rule = rules.find((r) => r.id === "MCP-006");
548
+ assert.ok(rule, "MCP-006 rule should exist");
549
+
550
+ const bom = makeBom(
551
+ [],
552
+ [],
553
+ [],
554
+ [
555
+ {
556
+ "bom-ref": "urn:service:mcp:gateway:latest",
557
+ name: "gateway",
558
+ version: "latest",
559
+ properties: [
560
+ { name: "SrcFile", value: "/repo/.vscode/mcp.json" },
561
+ { name: "cdx:mcp:inventorySource", value: "config-file" },
562
+ { name: "cdx:mcp:security:confusedDeputyRisk", value: "high" },
563
+ { name: "cdx:mcp:auth:supportsDCR", value: "true" },
564
+ { name: "cdx:mcp:authPosture", value: "oauth" },
565
+ ],
566
+ },
567
+ ],
568
+ );
569
+
570
+ const findings = await evaluateRule(rule, bom);
571
+ assert.ok(findings.length > 0, "Should detect confused-deputy risk");
572
+ });
573
+
574
+ it("should detect token passthrough risk in MCP config services (MCP-007)", async () => {
575
+ const rules = await loadRules(RULES_DIR);
576
+ const rule = rules.find((r) => r.id === "MCP-007");
577
+ assert.ok(rule, "MCP-007 rule should exist");
578
+
579
+ const bom = makeBom(
580
+ [],
581
+ [],
582
+ [],
583
+ [
584
+ {
585
+ "bom-ref": "urn:service:mcp:gateway:latest",
586
+ name: "gateway",
587
+ version: "latest",
588
+ properties: [
589
+ { name: "SrcFile", value: "/repo/.vscode/mcp.json" },
590
+ { name: "cdx:mcp:inventorySource", value: "config-file" },
591
+ { name: "cdx:mcp:security:tokenPassthroughRisk", value: "high" },
592
+ { name: "cdx:mcp:authPosture", value: "bearer" },
593
+ {
594
+ name: "cdx:mcp:trustProfile",
595
+ value: "official-sdk+networked+auth",
596
+ },
597
+ ],
598
+ },
599
+ ],
600
+ );
601
+
602
+ const findings = await evaluateRule(rule, bom);
603
+ assert.ok(findings.length > 0, "Should detect token passthrough risk");
604
+ });
605
+
606
+ it("should flag shipped AI instruction files in build/post-build BOMs (AGT-007)", async () => {
607
+ const rules = await loadRules(RULES_DIR);
608
+ const rule = rules.find((r) => r.id === "AGT-007");
609
+ assert.ok(rule, "AGT-007 rule should exist");
610
+
611
+ const bom = makeBom([
612
+ {
613
+ "bom-ref": "file:/repo/CLAUDE.md",
614
+ name: "CLAUDE.md",
615
+ type: "file",
616
+ properties: [
617
+ { name: "SrcFile", value: "/repo/CLAUDE.md" },
618
+ { name: "cdx:file:kind", value: "agent-instructions" },
619
+ { name: "cdx:agent:inventorySource", value: "agent-file" },
620
+ ],
621
+ },
622
+ ]);
623
+ bom.metadata.lifecycles = [{ phase: "build" }, { phase: "post-build" }];
624
+
625
+ const findings = await evaluateRule(rule, bom);
626
+ assert.ok(findings.length > 0, "Should detect shipped AI instructions");
627
+ assert.strictEqual(findings[0].severity, "medium");
628
+ });
629
+
630
+ it("should flag shipped MCP config files in build/post-build BOMs (MCP-008)", async () => {
631
+ const rules = await loadRules(RULES_DIR);
632
+ const rule = rules.find((r) => r.id === "MCP-008");
633
+ assert.ok(rule, "MCP-008 rule should exist");
634
+
635
+ const bom = makeBom([
636
+ {
637
+ "bom-ref": "file:/repo/.vscode/mcp.json",
638
+ name: "mcp.json",
639
+ type: "file",
640
+ properties: [
641
+ { name: "SrcFile", value: "/repo/.vscode/mcp.json" },
642
+ { name: "cdx:file:kind", value: "mcp-config" },
643
+ { name: "cdx:mcp:configFormat", value: "vscode" },
644
+ { name: "cdx:mcp:configuredServiceCount", value: "1" },
645
+ { name: "cdx:mcp:configuredServiceNames", value: "releaseDocs" },
646
+ ],
647
+ },
648
+ ]);
649
+ bom.metadata.lifecycles = [{ phase: "build" }];
650
+
651
+ const findings = await evaluateRule(rule, bom);
652
+ assert.ok(findings.length > 0, "Should detect shipped MCP config");
653
+ assert.strictEqual(findings[0].severity, "medium");
654
+ });
655
+
234
656
  it("should detect npm name mismatch (INT-002)", async () => {
235
657
  const rules = await loadRules(RULES_DIR);
236
658
  const rule = rules.find((r) => r.id === "INT-002");
@@ -271,6 +693,216 @@ describe("evaluateRule", () => {
271
693
  assert.strictEqual(findings[0].severity, "high");
272
694
  });
273
695
 
696
+ it("should detect Cargo git dependency without immutable pin (PKG-007)", async () => {
697
+ const rules = await loadRules(RULES_DIR);
698
+ const rule = rules.find((r) => r.id === "PKG-007");
699
+ assert.ok(rule, "PKG-007 rule should exist");
700
+
701
+ const bom = makeBom([
702
+ {
703
+ type: "library",
704
+ name: "git-crate",
705
+ version: "git+https://example.com/git-crate",
706
+ purl: "pkg:cargo/git-crate@git+https://example.com/git-crate",
707
+ "bom-ref": "pkg:cargo/git-crate@git+https://example.com/git-crate",
708
+ properties: [
709
+ { name: "cdx:cargo:git", value: "https://example.com/git-crate" },
710
+ { name: "cdx:cargo:dependencyKind", value: "runtime" },
711
+ ],
712
+ },
713
+ ]);
714
+
715
+ const findings = await evaluateRule(rule, bom);
716
+ assert.ok(findings.length > 0, "Should detect mutable Cargo git source");
717
+ assert.strictEqual(findings[0].severity, "high");
718
+ });
719
+
720
+ it("should detect Cargo local path dependency (PKG-008)", async () => {
721
+ const rules = await loadRules(RULES_DIR);
722
+ const rule = rules.find((r) => r.id === "PKG-008");
723
+ assert.ok(rule, "PKG-008 rule should exist");
724
+
725
+ const bom = makeBom([
726
+ {
727
+ type: "library",
728
+ name: "path-crate",
729
+ version: "path+../path-crate",
730
+ purl: "pkg:cargo/path-crate@path+../path-crate",
731
+ "bom-ref": "pkg:cargo/path-crate@path+../path-crate",
732
+ properties: [
733
+ { name: "cdx:cargo:path", value: "../path-crate" },
734
+ { name: "cdx:cargo:dependencyKind", value: "build" },
735
+ ],
736
+ },
737
+ ]);
738
+
739
+ const findings = await evaluateRule(rule, bom);
740
+ assert.ok(findings.length > 0, "Should detect Cargo path dependency");
741
+ assert.strictEqual(findings[0].severity, "high");
742
+ });
743
+
744
+ it("should detect yanked Cargo crate (INT-010)", async () => {
745
+ const rules = await loadRules(RULES_DIR);
746
+ const rule = rules.find((r) => r.id === "INT-010");
747
+ assert.ok(rule, "INT-010 rule should exist");
748
+
749
+ const bom = makeBom([
750
+ {
751
+ type: "library",
752
+ name: "yanked-crate",
753
+ version: "1.2.3",
754
+ purl: "pkg:cargo/yanked-crate@1.2.3",
755
+ "bom-ref": "pkg:cargo/yanked-crate@1.2.3",
756
+ properties: [
757
+ { name: "cdx:cargo:yanked", value: "true" },
758
+ { name: "cdx:cargo:publisher", value: "publisher" },
759
+ ],
760
+ },
761
+ ]);
762
+
763
+ const findings = await evaluateRule(rule, bom);
764
+ assert.ok(findings.length > 0, "Should detect yanked Cargo crate");
765
+ assert.strictEqual(findings[0].severity, "high");
766
+ });
767
+
768
+ it("should detect native Cargo build surface in formulation (INT-011)", async () => {
769
+ const rules = await loadRules(RULES_DIR);
770
+ const rule = rules.find((r) => r.id === "INT-011");
771
+ assert.ok(rule, "INT-011 rule should exist");
772
+
773
+ const bom = makeBom(
774
+ [],
775
+ [],
776
+ [
777
+ {
778
+ type: "application",
779
+ name: "cargo-demo",
780
+ version: "config",
781
+ "bom-ref": "urn:cdxgen:formulation:cargo:test",
782
+ properties: [
783
+ { name: "SrcFile", value: "/tmp/Cargo.toml" },
784
+ { name: "cdx:rust:buildTool", value: "cargo" },
785
+ { name: "cdx:cargo:hasNativeBuild", value: "true" },
786
+ { name: "cdx:cargo:buildScript", value: "/tmp/build.rs" },
787
+ ],
788
+ },
789
+ ],
790
+ );
791
+
792
+ const findings = await evaluateRule(rule, bom);
793
+ assert.ok(findings.length > 0, "Should detect native Cargo build surface");
794
+ assert.strictEqual(findings[0].severity, "medium");
795
+ });
796
+
797
+ it("should detect mutable Cargo toolchain setup for native builds (INT-012)", async () => {
798
+ const rules = await loadRules(RULES_DIR);
799
+ const rule = rules.find((r) => r.id === "INT-012");
800
+ assert.ok(rule, "INT-012 rule should exist");
801
+
802
+ const bom = makeBom(
803
+ [
804
+ {
805
+ type: "application",
806
+ name: "rust-toolchain",
807
+ version: "stable",
808
+ purl: "pkg:github/dtolnay/rust-toolchain@stable",
809
+ "bom-ref": "pkg:github/dtolnay/rust-toolchain@stable",
810
+ properties: [
811
+ { name: "cdx:github:action:ecosystem", value: "cargo" },
812
+ { name: "cdx:github:action:role", value: "toolchain" },
813
+ {
814
+ name: "cdx:github:action:versionPinningType",
815
+ value: "tag",
816
+ },
817
+ {
818
+ name: "cdx:github:action:uses",
819
+ value: "dtolnay/rust-toolchain@stable",
820
+ },
821
+ ],
822
+ },
823
+ ],
824
+ [],
825
+ [
826
+ {
827
+ type: "application",
828
+ name: "cargo-demo",
829
+ version: "config",
830
+ "bom-ref": "urn:cdxgen:formulation:cargo:int012",
831
+ properties: [
832
+ { name: "SrcFile", value: "/tmp/Cargo.toml" },
833
+ { name: "cdx:rust:buildTool", value: "cargo" },
834
+ { name: "cdx:cargo:hasNativeBuild", value: "true" },
835
+ { name: "cdx:cargo:buildScript", value: "/tmp/build.rs" },
836
+ ],
837
+ },
838
+ ],
839
+ );
840
+
841
+ const findings = await evaluateRule(rule, bom);
842
+ assert.ok(
843
+ findings.length > 0,
844
+ "Should detect mutable Cargo toolchain setup for native builds",
845
+ );
846
+ assert.strictEqual(findings[0].severity, "medium");
847
+ });
848
+
849
+ it("should detect Cargo build workflow steps against native build surfaces (INT-013)", async () => {
850
+ const rules = await loadRules(RULES_DIR);
851
+ const rule = rules.find((r) => r.id === "INT-013");
852
+ assert.ok(rule, "INT-013 rule should exist");
853
+
854
+ const bom = makeBom(
855
+ [
856
+ {
857
+ type: "application",
858
+ name: "cargo build",
859
+ "bom-ref": "urn:cdxgen:workflow:cargo-build",
860
+ properties: [
861
+ { name: "cdx:github:step:type", value: "run" },
862
+ { name: "cdx:github:step:usesCargo", value: "true" },
863
+ {
864
+ name: "cdx:github:step:cargoSubcommands",
865
+ value: "build,test",
866
+ },
867
+ {
868
+ name: "cdx:github:step:command",
869
+ value: "cargo build --workspace && cargo test --workspace",
870
+ },
871
+ ],
872
+ },
873
+ ],
874
+ [],
875
+ [
876
+ {
877
+ type: "application",
878
+ name: "cargo-demo",
879
+ version: "config",
880
+ "bom-ref": "urn:cdxgen:formulation:cargo:int013",
881
+ properties: [
882
+ { name: "SrcFile", value: "/tmp/Cargo.toml" },
883
+ { name: "cdx:rust:buildTool", value: "cargo" },
884
+ { name: "cdx:cargo:hasNativeBuild", value: "true" },
885
+ {
886
+ name: "cdx:cargo:buildScriptCapabilities",
887
+ value: "process-execution, network-access",
888
+ },
889
+ {
890
+ name: "cdx:cargo:nativeBuildIndicators",
891
+ value: "bindgen, openssl-sys",
892
+ },
893
+ ],
894
+ },
895
+ ],
896
+ );
897
+
898
+ const findings = await evaluateRule(rule, bom);
899
+ assert.ok(
900
+ findings.length > 0,
901
+ "Should detect Cargo build workflow steps against native build surfaces",
902
+ );
903
+ assert.strictEqual(findings[0].severity, "medium");
904
+ });
905
+
274
906
  it("should detect broad host access extensions (CHE-001)", async () => {
275
907
  const rules = await loadRules(RULES_DIR);
276
908
  const rule = rules.find((r) => r.id === "CHE-001");
@@ -1720,6 +2352,62 @@ describe("auditBom", () => {
1720
2352
  }
1721
2353
  });
1722
2354
 
2355
+ it("expands the ai-inventory category alias", async () => {
2356
+ const bom = makeBom(
2357
+ [],
2358
+ [],
2359
+ [
2360
+ {
2361
+ type: "application",
2362
+ name: "agent-guide",
2363
+ version: "latest",
2364
+ "bom-ref": "file:/repo/AGENTS.md",
2365
+ properties: [
2366
+ { name: "SrcFile", value: "/repo/AGENTS.md" },
2367
+ { name: "cdx:agent:inventorySource", value: "agent-file" },
2368
+ { name: "cdx:file:kind", value: "agent-instructions" },
2369
+ {
2370
+ name: "cdx:agent:hasNonOfficialMcpReference",
2371
+ value: "true",
2372
+ },
2373
+ { name: "cdx:agent:mcpPackageRefs", value: "@acme/mcp-server" },
2374
+ ],
2375
+ },
2376
+ ],
2377
+ [
2378
+ {
2379
+ "bom-ref": "urn:service:mcp:demo:1",
2380
+ group: "mcp",
2381
+ name: "demo-server",
2382
+ authenticated: false,
2383
+ endpoints: ["https://mcp.example.com/mcp"],
2384
+ properties: [
2385
+ { name: "cdx:mcp:transport", value: "streamable-http" },
2386
+ { name: "cdx:mcp:serviceType", value: "inferred-endpoint" },
2387
+ { name: "cdx:mcp:inventorySource", value: "agent-file" },
2388
+ { name: "cdx:mcp:toolCount", value: "1" },
2389
+ { name: "cdx:mcp:officialSdk", value: "false" },
2390
+ ],
2391
+ },
2392
+ ],
2393
+ );
2394
+
2395
+ const findings = await auditBom(bom, {
2396
+ bomAuditCategories: "ai-inventory",
2397
+ });
2398
+ assert.ok(findings.some((finding) => finding.category === "ai-agent"));
2399
+ assert.ok(findings.some((finding) => finding.category === "mcp-server"));
2400
+ });
2401
+
2402
+ it("rejects unknown audit categories with valid choices", async () => {
2403
+ await assert.rejects(
2404
+ auditBom(makeBom([]), {
2405
+ bomAuditCategories: "unknown-category",
2406
+ }),
2407
+ /Unknown BOM audit category: unknown-category\. Valid categories: .*ai-inventory \(alias for ai-agent,mcp-server\).*ci-permission.*mcp-server/,
2408
+ );
2409
+ });
2410
+
1723
2411
  it("should filter by minimum severity", async () => {
1724
2412
  const bom = makeBom([
1725
2413
  makeComponent("actions/setup-node", "v3", [
@@ -1951,6 +2639,10 @@ describe("formatAnnotations", () => {
1951
2639
  mitigation: "Pin to SHA",
1952
2640
  attackTactics: ["TA0001", "TA0004"],
1953
2641
  attackTechniques: ["T1195.001"],
2642
+ standards: {
2643
+ "owasp-ai-top-10": ["LLM07: Insecure Plugin Design"],
2644
+ "nist-ai-rmf": ["Manage"],
2645
+ },
1954
2646
  },
1955
2647
  ];
1956
2648
  const annotations = formatAnnotations(findings, bom);
@@ -1961,6 +2653,7 @@ describe("formatAnnotations", () => {
1961
2653
  assert.match(annotations[0].text, /\| Property \| Value \|/);
1962
2654
  assert.match(annotations[0].text, /cdx:audit:attack:tactics/);
1963
2655
  assert.match(annotations[0].text, /cdx:audit:attack:techniques/);
2656
+ assert.match(annotations[0].text, /cdx:audit:standards:owasp-ai-top-10/);
1964
2657
  assert.ok(
1965
2658
  annotations[0].annotator.component,
1966
2659
  "Annotation should have annotator component",