@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
@@ -190,7 +190,7 @@ describe("evaluateRule", () => {
190
190
  );
191
191
  });
192
192
 
193
- it("should detect npm install script from non-registry source (PKG-001)", async () => {
193
+ it("should detect npm install script from direct manifest source (PKG-001)", async () => {
194
194
  const rules = await loadRules(RULES_DIR);
195
195
  const rule = rules.find((r) => r.id === "PKG-001");
196
196
  assert.ok(rule, "PKG-001 rule should exist");
@@ -198,7 +198,11 @@ describe("evaluateRule", () => {
198
198
  const bom = makeBom([
199
199
  makeComponent("sketchy-pkg", "1.0.0", [
200
200
  ["cdx:npm:hasInstallScript", "true"],
201
- ["cdx:npm:isRegistryDependency", "false"],
201
+ ["cdx:npm:manifestSourceType", "git"],
202
+ [
203
+ "cdx:npm:manifestSource",
204
+ "git+https://github.com/acme/sketchy-pkg.git",
205
+ ],
202
206
  ]),
203
207
  ]);
204
208
 
@@ -207,6 +211,122 @@ describe("evaluateRule", () => {
207
211
  assert.strictEqual(findings[0].severity, "high");
208
212
  });
209
213
 
214
+ it("should detect npm install scripts from url and path manifest sources for PKG-001", async () => {
215
+ const rules = await loadRules(RULES_DIR);
216
+ const rule = rules.find((r) => r.id === "PKG-001");
217
+ assert.ok(rule, "PKG-001 rule should exist");
218
+
219
+ for (const manifestSourceType of ["url", "path"]) {
220
+ const bom = makeBom([
221
+ makeComponent(`sketchy-${manifestSourceType}`, "1.0.0", [
222
+ ["cdx:npm:hasInstallScript", "true"],
223
+ ["cdx:npm:manifestSourceType", manifestSourceType],
224
+ ["cdx:npm:manifestSource", `${manifestSourceType}:example`],
225
+ ]),
226
+ ]);
227
+
228
+ const findings = await evaluateRule(rule, bom);
229
+ assert.ok(findings.length > 0);
230
+ }
231
+ });
232
+
233
+ it("should not detect npm install script without manifest source evidence for PKG-001", async () => {
234
+ const rules = await loadRules(RULES_DIR);
235
+ const rule = rules.find((r) => r.id === "PKG-001");
236
+ assert.ok(rule, "PKG-001 rule should exist");
237
+
238
+ const bom = makeBom([
239
+ makeComponent("registry-pkg", "1.0.0", [
240
+ ["cdx:npm:hasInstallScript", "true"],
241
+ ["cdx:npm:isRegistryDependency", "false"],
242
+ ]),
243
+ ]);
244
+
245
+ const findings = await evaluateRule(rule, bom);
246
+ assert.strictEqual(findings.length, 0);
247
+ });
248
+
249
+ it("should detect Collider packages from insecure HTTP origins (PKG-009)", async () => {
250
+ const rules = await loadRules(RULES_DIR);
251
+ const rule = rules.find((r) => r.id === "PKG-009");
252
+ assert.ok(rule, "PKG-009 rule should exist");
253
+
254
+ const bom = makeBom([
255
+ makeComponent("fmt", "11.0.2", [
256
+ ["cdx:collider:dependencyKind", "direct"],
257
+ ["cdx:collider:origin", "http://mirror.example.com/collider/v2/"],
258
+ ["cdx:collider:originScheme", "http"],
259
+ ["cdx:collider:originHost", "mirror.example.com"],
260
+ ]),
261
+ ]);
262
+
263
+ const findings = await evaluateRule(rule, bom);
264
+ assert.ok(findings.length > 0, "Should detect insecure Collider origin");
265
+ assert.strictEqual(findings[0].ruleId, "PKG-009");
266
+ assert.strictEqual(findings[0].severity, "medium");
267
+ });
268
+
269
+ it("should detect Collider origins that required sanitization (PKG-010)", async () => {
270
+ const rules = await loadRules(RULES_DIR);
271
+ const rule = rules.find((r) => r.id === "PKG-010");
272
+ assert.ok(rule, "PKG-010 rule should exist");
273
+
274
+ const bom = makeBom([
275
+ makeComponent("spdlog", "1.15.0", [
276
+ ["cdx:collider:dependencyKind", "direct"],
277
+ ["cdx:collider:origin", "https://example.com/collider/v2/"],
278
+ ["cdx:collider:originScheme", "https"],
279
+ ["cdx:collider:originSanitized", "true"],
280
+ ]),
281
+ ]);
282
+
283
+ const findings = await evaluateRule(rule, bom);
284
+ assert.ok(findings.length > 0, "Should detect sanitized Collider origin");
285
+ assert.strictEqual(findings[0].ruleId, "PKG-010");
286
+ assert.strictEqual(findings[0].severity, "low");
287
+ });
288
+
289
+ it("should detect python dependency from direct manifest source (PKG-011)", async () => {
290
+ const rules = await loadRules(RULES_DIR);
291
+ const rule = rules.find((r) => r.id === "PKG-011");
292
+ assert.ok(rule, "PKG-011 rule should exist");
293
+
294
+ const bom = makeBom([
295
+ makeComponent("suspicious-python-pkg", "1.0.0", [
296
+ ["cdx:pypi:manifestSourceType", "url"],
297
+ [
298
+ "cdx:pypi:manifestSource",
299
+ "https://example.com/suspicious-python-pkg.whl",
300
+ ],
301
+ ]),
302
+ ]);
303
+
304
+ const findings = await evaluateRule(rule, bom);
305
+ assert.ok(findings.length > 0, "Should detect python direct source risk");
306
+ assert.strictEqual(findings[0].ruleId, "PKG-011");
307
+ assert.strictEqual(findings[0].severity, "high");
308
+ });
309
+
310
+ it("should detect Collider packages missing valid wrap hashes (INT-014)", async () => {
311
+ const rules = await loadRules(RULES_DIR);
312
+ const rule = rules.find((r) => r.id === "INT-014");
313
+ assert.ok(rule, "INT-014 rule should exist");
314
+
315
+ const bom = makeBom([
316
+ makeComponent("fast_float", "8.0.2", [
317
+ ["cdx:collider:dependencyKind", "transitive"],
318
+ ["cdx:collider:hasWrapHash", "false"],
319
+ ["cdx:collider:wrapHash", "not-a-sha256"],
320
+ ["cdx:collider:wrapHashInvalid", "true"],
321
+ ]),
322
+ ]);
323
+
324
+ const findings = await evaluateRule(rule, bom);
325
+ assert.ok(findings.length > 0, "Should detect missing Collider wrap hash");
326
+ assert.strictEqual(findings[0].ruleId, "INT-014");
327
+ assert.strictEqual(findings[0].severity, "high");
328
+ });
329
+
210
330
  it("should detect OIDC token issuance to a non-official action (CI-002)", async () => {
211
331
  const rules = await loadRules(RULES_DIR);
212
332
  const rule = rules.find((r) => r.id === "CI-002");
@@ -524,13 +644,14 @@ describe("evaluateRule", () => {
524
644
  { name: "cdx:mcp:inventorySource", value: "config-file" },
525
645
  { name: "cdx:mcp:credentialExposure", value: "true" },
526
646
  {
527
- name: "cdx:mcp:credentialExposureFields",
528
- value: "header:Authorization,env:OPENAI_API_KEY",
647
+ name: "cdx:mcp:credentialExposureFieldCount",
648
+ value: "2",
529
649
  },
530
650
  {
531
- name: "cdx:mcp:credentialRiskIndicators",
532
- value: "bearer-token,generic-secret",
651
+ name: "cdx:mcp:credentialIndicatorCount",
652
+ value: "2",
533
653
  },
654
+ { name: "cdx:mcp:credentialReferenceCount", value: "1" },
534
655
  ],
535
656
  },
536
657
  ],
@@ -602,6 +723,56 @@ describe("evaluateRule", () => {
602
723
  assert.ok(findings.length > 0, "Should detect token passthrough risk");
603
724
  });
604
725
 
726
+ it("should flag shipped AI instruction files in build/post-build BOMs (AGT-007)", async () => {
727
+ const rules = await loadRules(RULES_DIR);
728
+ const rule = rules.find((r) => r.id === "AGT-007");
729
+ assert.ok(rule, "AGT-007 rule should exist");
730
+
731
+ const bom = makeBom([
732
+ {
733
+ "bom-ref": "file:/repo/CLAUDE.md",
734
+ name: "CLAUDE.md",
735
+ type: "file",
736
+ properties: [
737
+ { name: "SrcFile", value: "/repo/CLAUDE.md" },
738
+ { name: "cdx:file:kind", value: "agent-instructions" },
739
+ { name: "cdx:agent:inventorySource", value: "agent-file" },
740
+ ],
741
+ },
742
+ ]);
743
+ bom.metadata.lifecycles = [{ phase: "build" }, { phase: "post-build" }];
744
+
745
+ const findings = await evaluateRule(rule, bom);
746
+ assert.ok(findings.length > 0, "Should detect shipped AI instructions");
747
+ assert.strictEqual(findings[0].severity, "medium");
748
+ });
749
+
750
+ it("should flag shipped MCP config files in build/post-build BOMs (MCP-008)", async () => {
751
+ const rules = await loadRules(RULES_DIR);
752
+ const rule = rules.find((r) => r.id === "MCP-008");
753
+ assert.ok(rule, "MCP-008 rule should exist");
754
+
755
+ const bom = makeBom([
756
+ {
757
+ "bom-ref": "file:/repo/.vscode/mcp.json",
758
+ name: "mcp.json",
759
+ type: "file",
760
+ properties: [
761
+ { name: "SrcFile", value: "/repo/.vscode/mcp.json" },
762
+ { name: "cdx:file:kind", value: "mcp-config" },
763
+ { name: "cdx:mcp:configFormat", value: "vscode" },
764
+ { name: "cdx:mcp:configuredServiceCount", value: "1" },
765
+ { name: "cdx:mcp:configuredServiceNames", value: "releaseDocs" },
766
+ ],
767
+ },
768
+ ]);
769
+ bom.metadata.lifecycles = [{ phase: "build" }];
770
+
771
+ const findings = await evaluateRule(rule, bom);
772
+ assert.ok(findings.length > 0, "Should detect shipped MCP config");
773
+ assert.strictEqual(findings[0].severity, "medium");
774
+ });
775
+
605
776
  it("should detect npm name mismatch (INT-002)", async () => {
606
777
  const rules = await loadRules(RULES_DIR);
607
778
  const rule = rules.find((r) => r.id === "INT-002");
@@ -1383,6 +1554,528 @@ describe("evaluateRule", () => {
1383
1554
  assert.match(findings[0].message, /Heuristic review/);
1384
1555
  });
1385
1556
 
1557
+ it("should detect disabled npm cache when npm distributions are resolved remotely (CI-022)", async () => {
1558
+ const rules = await loadRules(RULES_DIR);
1559
+ const rule = rules.find((r) => r.id === "CI-022");
1560
+ assert.ok(rule, "CI-022 rule should exist");
1561
+
1562
+ const bom = makeBom(
1563
+ [
1564
+ {
1565
+ type: "library",
1566
+ name: "left-pad",
1567
+ version: "1.3.0",
1568
+ purl: "pkg:npm/left-pad@1.3.0",
1569
+ "bom-ref": "pkg:npm/left-pad@1.3.0",
1570
+ externalReferences: [
1571
+ {
1572
+ type: "distribution",
1573
+ url: "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz",
1574
+ },
1575
+ ],
1576
+ properties: [
1577
+ {
1578
+ name: "cdx:npm:manifestSourceType",
1579
+ value: "url",
1580
+ },
1581
+ {
1582
+ name: "cdx:npm:manifestSource",
1583
+ value: "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz",
1584
+ },
1585
+ ],
1586
+ },
1587
+ ],
1588
+ [],
1589
+ [
1590
+ {
1591
+ type: "application",
1592
+ name: "setup-node",
1593
+ version: "v4",
1594
+ purl: "pkg:github/actions/setup-node@v4",
1595
+ "bom-ref": "pkg:github/actions/setup-node@v4",
1596
+ properties: [
1597
+ {
1598
+ name: "cdx:github:action:uses",
1599
+ value: "actions/setup-node@v4",
1600
+ },
1601
+ { name: "cdx:github:action:disablesBuildCache", value: "true" },
1602
+ { name: "cdx:github:action:buildCacheEcosystem", value: "npm" },
1603
+ {
1604
+ name: "cdx:github:action:buildCacheDisableInput",
1605
+ value: "package-manager-cache",
1606
+ },
1607
+ {
1608
+ name: "cdx:github:action:buildCacheDisableValue",
1609
+ value: "false",
1610
+ },
1611
+ {
1612
+ name: "cdx:github:workflow:file",
1613
+ value: ".github/workflows/ci.yml",
1614
+ },
1615
+ ],
1616
+ },
1617
+ ],
1618
+ );
1619
+
1620
+ const findings = await evaluateRule(rule, bom);
1621
+ assert.ok(findings.length > 0, "Should detect disabled npm cache");
1622
+ assert.strictEqual(findings[0].severity, "medium");
1623
+ });
1624
+
1625
+ it("should detect disabled npm cache for git manifest sources (CI-022)", async () => {
1626
+ const rules = await loadRules(RULES_DIR);
1627
+ const rule = rules.find((r) => r.id === "CI-022");
1628
+ assert.ok(rule, "CI-022 rule should exist");
1629
+
1630
+ const bom = makeBom(
1631
+ [
1632
+ {
1633
+ type: "library",
1634
+ name: "git-dep",
1635
+ version: "2.0.0",
1636
+ purl: "pkg:npm/git-dep@2.0.0",
1637
+ "bom-ref": "pkg:npm/git-dep@2.0.0",
1638
+ externalReferences: [
1639
+ {
1640
+ type: "distribution",
1641
+ url: "git+https://github.com/acme/git-dep.git",
1642
+ },
1643
+ ],
1644
+ properties: [
1645
+ {
1646
+ name: "cdx:npm:manifestSourceType",
1647
+ value: "git",
1648
+ },
1649
+ {
1650
+ name: "cdx:npm:manifestSource",
1651
+ value: "git+https://github.com/acme/git-dep.git",
1652
+ },
1653
+ ],
1654
+ },
1655
+ ],
1656
+ [],
1657
+ [
1658
+ {
1659
+ type: "application",
1660
+ name: "setup-node",
1661
+ version: "v4",
1662
+ purl: "pkg:github/actions/setup-node@v4",
1663
+ "bom-ref": "pkg:github/actions/setup-node@v4",
1664
+ properties: [
1665
+ {
1666
+ name: "cdx:github:action:uses",
1667
+ value: "actions/setup-node@v4",
1668
+ },
1669
+ { name: "cdx:github:action:disablesBuildCache", value: "true" },
1670
+ { name: "cdx:github:action:buildCacheEcosystem", value: "npm" },
1671
+ ],
1672
+ },
1673
+ ],
1674
+ );
1675
+
1676
+ const findings = await evaluateRule(rule, bom);
1677
+ assert.ok(findings.length > 0, "Should detect disabled npm cache");
1678
+ });
1679
+
1680
+ it("should not detect disabled npm cache for registry-only npm dependencies (CI-022)", async () => {
1681
+ const rules = await loadRules(RULES_DIR);
1682
+ const rule = rules.find((r) => r.id === "CI-022");
1683
+ assert.ok(rule, "CI-022 rule should exist");
1684
+
1685
+ const bom = makeBom(
1686
+ [
1687
+ {
1688
+ type: "library",
1689
+ name: "left-pad",
1690
+ version: "1.3.0",
1691
+ purl: "pkg:npm/left-pad@1.3.0",
1692
+ "bom-ref": "pkg:npm/left-pad@1.3.0",
1693
+ externalReferences: [
1694
+ {
1695
+ type: "distribution",
1696
+ url: "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz",
1697
+ },
1698
+ ],
1699
+ },
1700
+ ],
1701
+ [],
1702
+ [
1703
+ {
1704
+ type: "application",
1705
+ name: "setup-node",
1706
+ version: "v4",
1707
+ purl: "pkg:github/actions/setup-node@v4",
1708
+ "bom-ref": "pkg:github/actions/setup-node@v4",
1709
+ properties: [
1710
+ {
1711
+ name: "cdx:github:action:uses",
1712
+ value: "actions/setup-node@v4",
1713
+ },
1714
+ { name: "cdx:github:action:disablesBuildCache", value: "true" },
1715
+ { name: "cdx:github:action:buildCacheEcosystem", value: "npm" },
1716
+ ],
1717
+ },
1718
+ ],
1719
+ );
1720
+
1721
+ const findings = await evaluateRule(rule, bom);
1722
+ assert.strictEqual(findings.length, 0);
1723
+ });
1724
+
1725
+ it("should not detect disabled npm cache for local path manifest sources (CI-022)", async () => {
1726
+ const rules = await loadRules(RULES_DIR);
1727
+ const rule = rules.find((r) => r.id === "CI-022");
1728
+ assert.ok(rule, "CI-022 rule should exist");
1729
+
1730
+ const bom = makeBom(
1731
+ [
1732
+ {
1733
+ type: "library",
1734
+ name: "local-dep",
1735
+ version: "1.0.0",
1736
+ purl: "pkg:npm/local-dep@1.0.0",
1737
+ "bom-ref": "pkg:npm/local-dep@1.0.0",
1738
+ externalReferences: [
1739
+ {
1740
+ type: "distribution",
1741
+ url: "file:../libs/local-dep",
1742
+ },
1743
+ ],
1744
+ properties: [
1745
+ {
1746
+ name: "cdx:npm:manifestSourceType",
1747
+ value: "path",
1748
+ },
1749
+ {
1750
+ name: "cdx:npm:manifestSource",
1751
+ value: "file:../libs/local-dep",
1752
+ },
1753
+ ],
1754
+ },
1755
+ ],
1756
+ [],
1757
+ [
1758
+ {
1759
+ type: "application",
1760
+ name: "setup-node",
1761
+ version: "v4",
1762
+ purl: "pkg:github/actions/setup-node@v4",
1763
+ "bom-ref": "pkg:github/actions/setup-node@v4",
1764
+ properties: [
1765
+ {
1766
+ name: "cdx:github:action:uses",
1767
+ value: "actions/setup-node@v4",
1768
+ },
1769
+ { name: "cdx:github:action:disablesBuildCache", value: "true" },
1770
+ { name: "cdx:github:action:buildCacheEcosystem", value: "npm" },
1771
+ ],
1772
+ },
1773
+ ],
1774
+ );
1775
+
1776
+ const findings = await evaluateRule(rule, bom);
1777
+ assert.strictEqual(findings.length, 0);
1778
+ });
1779
+
1780
+ it("should detect disabled Python cache when pylock sources use remote artifacts (CI-023)", async () => {
1781
+ const rules = await loadRules(RULES_DIR);
1782
+ const rule = rules.find((r) => r.id === "CI-023");
1783
+ assert.ok(rule, "CI-023 rule should exist");
1784
+
1785
+ const bom = makeBom(
1786
+ [
1787
+ {
1788
+ type: "library",
1789
+ name: "requests",
1790
+ version: "2.32.0",
1791
+ purl: "pkg:pypi/requests@2.32.0",
1792
+ "bom-ref": "pkg:pypi/requests@2.32.0",
1793
+ properties: [
1794
+ {
1795
+ name: "cdx:pypi:manifestSourceType",
1796
+ value: "url",
1797
+ },
1798
+ {
1799
+ name: "cdx:pypi:manifestSource",
1800
+ value:
1801
+ "https://files.pythonhosted.org/packages/requests-2.32.0.tar.gz",
1802
+ },
1803
+ ],
1804
+ },
1805
+ ],
1806
+ [],
1807
+ [
1808
+ {
1809
+ type: "application",
1810
+ name: "setup-python",
1811
+ version: "v5",
1812
+ purl: "pkg:github/actions/setup-python@v5",
1813
+ "bom-ref": "pkg:github/actions/setup-python@v5",
1814
+ properties: [
1815
+ {
1816
+ name: "cdx:github:action:uses",
1817
+ value: "actions/setup-python@v5",
1818
+ },
1819
+ { name: "cdx:github:action:disablesBuildCache", value: "true" },
1820
+ { name: "cdx:github:action:buildCacheEcosystem", value: "pypi" },
1821
+ {
1822
+ name: "cdx:github:action:buildCacheDisableInput",
1823
+ value: "cache",
1824
+ },
1825
+ {
1826
+ name: "cdx:github:action:buildCacheDisableValue",
1827
+ value: "false",
1828
+ },
1829
+ {
1830
+ name: "cdx:github:workflow:file",
1831
+ value: ".github/workflows/ci.yml",
1832
+ },
1833
+ ],
1834
+ },
1835
+ ],
1836
+ );
1837
+
1838
+ const findings = await evaluateRule(rule, bom);
1839
+ assert.ok(findings.length > 0, "Should detect disabled Python cache");
1840
+ assert.strictEqual(findings[0].severity, "medium");
1841
+ });
1842
+
1843
+ it("should detect disabled Python cache for git manifest sources (CI-023)", async () => {
1844
+ const rules = await loadRules(RULES_DIR);
1845
+ const rule = rules.find((r) => r.id === "CI-023");
1846
+ assert.ok(rule, "CI-023 rule should exist");
1847
+
1848
+ const bom = makeBom(
1849
+ [
1850
+ {
1851
+ type: "library",
1852
+ name: "private-lib",
1853
+ version: "1.0.0",
1854
+ purl: "pkg:pypi/private-lib@1.0.0",
1855
+ "bom-ref": "pkg:pypi/private-lib@1.0.0",
1856
+ properties: [
1857
+ {
1858
+ name: "cdx:pypi:manifestSourceType",
1859
+ value: "git",
1860
+ },
1861
+ {
1862
+ name: "cdx:pypi:manifestSource",
1863
+ value: "git+https://github.com/acme/private-lib.git",
1864
+ },
1865
+ ],
1866
+ },
1867
+ ],
1868
+ [],
1869
+ [
1870
+ {
1871
+ type: "application",
1872
+ name: "setup-python",
1873
+ version: "v5",
1874
+ purl: "pkg:github/actions/setup-python@v5",
1875
+ "bom-ref": "pkg:github/actions/setup-python@v5",
1876
+ properties: [
1877
+ {
1878
+ name: "cdx:github:action:uses",
1879
+ value: "actions/setup-python@v5",
1880
+ },
1881
+ { name: "cdx:github:action:disablesBuildCache", value: "true" },
1882
+ { name: "cdx:github:action:buildCacheEcosystem", value: "pypi" },
1883
+ ],
1884
+ },
1885
+ ],
1886
+ );
1887
+
1888
+ const findings = await evaluateRule(rule, bom);
1889
+ assert.ok(findings.length > 0, "Should detect disabled Python cache");
1890
+ });
1891
+
1892
+ it("should not detect disabled Python cache for registry-only lockfile sources (CI-023)", async () => {
1893
+ const rules = await loadRules(RULES_DIR);
1894
+ const rule = rules.find((r) => r.id === "CI-023");
1895
+ assert.ok(rule, "CI-023 rule should exist");
1896
+
1897
+ const bom = makeBom(
1898
+ [
1899
+ {
1900
+ type: "library",
1901
+ name: "requests",
1902
+ version: "2.32.0",
1903
+ purl: "pkg:pypi/requests@2.32.0",
1904
+ "bom-ref": "pkg:pypi/requests@2.32.0",
1905
+ properties: [
1906
+ {
1907
+ name: "cdx:pylock:archive",
1908
+ value:
1909
+ '{"url":"https://files.pythonhosted.org/packages/requests-2.32.0.tar.gz"}',
1910
+ },
1911
+ ],
1912
+ },
1913
+ ],
1914
+ [],
1915
+ [
1916
+ {
1917
+ type: "application",
1918
+ name: "setup-python",
1919
+ version: "v5",
1920
+ purl: "pkg:github/actions/setup-python@v5",
1921
+ "bom-ref": "pkg:github/actions/setup-python@v5",
1922
+ properties: [
1923
+ {
1924
+ name: "cdx:github:action:uses",
1925
+ value: "actions/setup-python@v5",
1926
+ },
1927
+ { name: "cdx:github:action:disablesBuildCache", value: "true" },
1928
+ { name: "cdx:github:action:buildCacheEcosystem", value: "pypi" },
1929
+ ],
1930
+ },
1931
+ ],
1932
+ );
1933
+
1934
+ const findings = await evaluateRule(rule, bom);
1935
+ assert.strictEqual(findings.length, 0);
1936
+ });
1937
+
1938
+ it("should not detect disabled Python cache for local path manifest sources (CI-023)", async () => {
1939
+ const rules = await loadRules(RULES_DIR);
1940
+ const rule = rules.find((r) => r.id === "CI-023");
1941
+ assert.ok(rule, "CI-023 rule should exist");
1942
+
1943
+ const bom = makeBom(
1944
+ [
1945
+ {
1946
+ type: "library",
1947
+ name: "local-lib",
1948
+ version: "1.0.0",
1949
+ purl: "pkg:pypi/local-lib@1.0.0",
1950
+ "bom-ref": "pkg:pypi/local-lib@1.0.0",
1951
+ properties: [
1952
+ {
1953
+ name: "cdx:pypi:manifestSourceType",
1954
+ value: "path",
1955
+ },
1956
+ {
1957
+ name: "cdx:pypi:manifestSource",
1958
+ value: "../libs/local-lib",
1959
+ },
1960
+ ],
1961
+ },
1962
+ ],
1963
+ [],
1964
+ [
1965
+ {
1966
+ type: "application",
1967
+ name: "setup-python",
1968
+ version: "v5",
1969
+ purl: "pkg:github/actions/setup-python@v5",
1970
+ "bom-ref": "pkg:github/actions/setup-python@v5",
1971
+ properties: [
1972
+ {
1973
+ name: "cdx:github:action:uses",
1974
+ value: "actions/setup-python@v5",
1975
+ },
1976
+ { name: "cdx:github:action:disablesBuildCache", value: "true" },
1977
+ { name: "cdx:github:action:buildCacheEcosystem", value: "pypi" },
1978
+ ],
1979
+ },
1980
+ ],
1981
+ );
1982
+
1983
+ const findings = await evaluateRule(rule, bom);
1984
+ assert.strictEqual(findings.length, 0);
1985
+ });
1986
+
1987
+ it("should detect disabled Cargo cache for git manifest sources (CI-024)", async () => {
1988
+ const rules = await loadRules(RULES_DIR);
1989
+ const rule = rules.find((r) => r.id === "CI-024");
1990
+ assert.ok(rule, "CI-024 rule should exist");
1991
+
1992
+ const bom = makeBom(
1993
+ [
1994
+ {
1995
+ type: "library",
1996
+ name: "git-crate",
1997
+ version: "git+https://github.com/acme/git-crate.git",
1998
+ purl: "pkg:cargo/git-crate@git+https://github.com/acme/git-crate.git",
1999
+ "bom-ref":
2000
+ "pkg:cargo/git-crate@git+https://github.com/acme/git-crate.git",
2001
+ properties: [
2002
+ {
2003
+ name: "cdx:cargo:git",
2004
+ value: "https://github.com/acme/git-crate.git",
2005
+ },
2006
+ ],
2007
+ },
2008
+ ],
2009
+ [],
2010
+ [
2011
+ {
2012
+ type: "application",
2013
+ name: "setup-rust",
2014
+ version: "v1",
2015
+ purl: "pkg:github/moonrepo/setup-rust@v1",
2016
+ "bom-ref": "pkg:github/moonrepo/setup-rust@v1",
2017
+ properties: [
2018
+ {
2019
+ name: "cdx:github:action:uses",
2020
+ value: "moonrepo/setup-rust@v1",
2021
+ },
2022
+ { name: "cdx:github:action:disablesBuildCache", value: "true" },
2023
+ { name: "cdx:github:action:buildCacheEcosystem", value: "cargo" },
2024
+ ],
2025
+ },
2026
+ ],
2027
+ );
2028
+
2029
+ const findings = await evaluateRule(rule, bom);
2030
+ assert.ok(findings.length > 0, "Should detect disabled Cargo cache");
2031
+ assert.strictEqual(findings[0].severity, "medium");
2032
+ });
2033
+
2034
+ it("should not detect disabled Cargo cache for local path manifest sources (CI-024)", async () => {
2035
+ const rules = await loadRules(RULES_DIR);
2036
+ const rule = rules.find((r) => r.id === "CI-024");
2037
+ assert.ok(rule, "CI-024 rule should exist");
2038
+
2039
+ const bom = makeBom(
2040
+ [
2041
+ {
2042
+ type: "library",
2043
+ name: "path-crate",
2044
+ version: "path+../path-crate",
2045
+ purl: "pkg:cargo/path-crate@path+../path-crate",
2046
+ "bom-ref": "pkg:cargo/path-crate@path+../path-crate",
2047
+ properties: [
2048
+ {
2049
+ name: "cdx:cargo:path",
2050
+ value: "../path-crate",
2051
+ },
2052
+ ],
2053
+ },
2054
+ ],
2055
+ [],
2056
+ [
2057
+ {
2058
+ type: "application",
2059
+ name: "setup-rust",
2060
+ version: "v1",
2061
+ purl: "pkg:github/moonrepo/setup-rust@v1",
2062
+ "bom-ref": "pkg:github/moonrepo/setup-rust@v1",
2063
+ properties: [
2064
+ {
2065
+ name: "cdx:github:action:uses",
2066
+ value: "moonrepo/setup-rust@v1",
2067
+ },
2068
+ { name: "cdx:github:action:disablesBuildCache", value: "true" },
2069
+ { name: "cdx:github:action:buildCacheEcosystem", value: "cargo" },
2070
+ ],
2071
+ },
2072
+ ],
2073
+ );
2074
+
2075
+ const findings = await evaluateRule(rule, bom);
2076
+ assert.strictEqual(findings.length, 0);
2077
+ });
2078
+
1386
2079
  it("should detect root authorized_keys without restrictions (OBOM-LNX-003)", async () => {
1387
2080
  const rules = await loadRules(RULES_DIR);
1388
2081
  const rule = rules.find((r) => r.id === "OBOM-LNX-003");
@@ -2301,6 +2994,62 @@ describe("auditBom", () => {
2301
2994
  }
2302
2995
  });
2303
2996
 
2997
+ it("expands the ai-inventory category alias", async () => {
2998
+ const bom = makeBom(
2999
+ [],
3000
+ [],
3001
+ [
3002
+ {
3003
+ type: "application",
3004
+ name: "agent-guide",
3005
+ version: "latest",
3006
+ "bom-ref": "file:/repo/AGENTS.md",
3007
+ properties: [
3008
+ { name: "SrcFile", value: "/repo/AGENTS.md" },
3009
+ { name: "cdx:agent:inventorySource", value: "agent-file" },
3010
+ { name: "cdx:file:kind", value: "agent-instructions" },
3011
+ {
3012
+ name: "cdx:agent:hasNonOfficialMcpReference",
3013
+ value: "true",
3014
+ },
3015
+ { name: "cdx:agent:mcpPackageRefs", value: "@acme/mcp-server" },
3016
+ ],
3017
+ },
3018
+ ],
3019
+ [
3020
+ {
3021
+ "bom-ref": "urn:service:mcp:demo:1",
3022
+ group: "mcp",
3023
+ name: "demo-server",
3024
+ authenticated: false,
3025
+ endpoints: ["https://mcp.example.com/mcp"],
3026
+ properties: [
3027
+ { name: "cdx:mcp:transport", value: "streamable-http" },
3028
+ { name: "cdx:mcp:serviceType", value: "inferred-endpoint" },
3029
+ { name: "cdx:mcp:inventorySource", value: "agent-file" },
3030
+ { name: "cdx:mcp:toolCount", value: "1" },
3031
+ { name: "cdx:mcp:officialSdk", value: "false" },
3032
+ ],
3033
+ },
3034
+ ],
3035
+ );
3036
+
3037
+ const findings = await auditBom(bom, {
3038
+ bomAuditCategories: "ai-inventory",
3039
+ });
3040
+ assert.ok(findings.some((finding) => finding.category === "ai-agent"));
3041
+ assert.ok(findings.some((finding) => finding.category === "mcp-server"));
3042
+ });
3043
+
3044
+ it("rejects unknown audit categories with valid choices", async () => {
3045
+ await assert.rejects(
3046
+ auditBom(makeBom([]), {
3047
+ bomAuditCategories: "unknown-category",
3048
+ }),
3049
+ /Unknown BOM audit category: unknown-category\. Valid categories: .*ai-inventory \(alias for ai-agent,mcp-server\).*ci-permission.*mcp-server/,
3050
+ );
3051
+ });
3052
+
2304
3053
  it("should filter by minimum severity", async () => {
2305
3054
  const bom = makeBom([
2306
3055
  makeComponent("actions/setup-node", "v3", [