@cdxgen/cdxgen 13.6.0 → 13.7.0

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 (264) hide show
  1. package/README.md +15 -8
  2. package/bin/audit.js +7 -4
  3. package/bin/cdxgen.js +256 -37
  4. package/bin/convert.js +146 -22
  5. package/bin/evinse.js +25 -3
  6. package/bin/repl.js +47 -3
  7. package/bin/sign.js +211 -34
  8. package/bin/tracebom.js +18 -6
  9. package/bin/validate.js +18 -3
  10. package/bin/verify.js +113 -5
  11. package/data/behavior-taxonomy.schema.json +1491 -0
  12. package/data/cyclonedx-2.0-bundled.schema.json +18606 -5528
  13. package/data/cyclonedx-2.0-cryptography-defs.schema.json +857 -0
  14. package/data/pqc-migration.json +239 -0
  15. package/data/remediations.json +52 -0
  16. package/data/rules/cbom-security.yaml +4 -3
  17. package/data/rules/package-integrity.yaml +28 -0
  18. package/lib/audit/index.js +25 -3
  19. package/lib/cli/bomAssembly.js +80 -1
  20. package/lib/cli/cliOptions.js +121 -17
  21. package/lib/cli/index.js +609 -898
  22. package/lib/cli/jsBom.js +3 -0
  23. package/lib/cli/jvmBom.js +365 -40
  24. package/lib/cli/managedBom.js +19 -3
  25. package/lib/cli/nativeBom.js +10 -0
  26. package/lib/core/env.js +26 -0
  27. package/lib/core/fs.js +126 -3
  28. package/lib/core/globPatterns.js +218 -0
  29. package/lib/core/processTree.js +99 -12
  30. package/lib/core/projectConfig.js +332 -25
  31. package/lib/core/toolRegistry.js +15 -1
  32. package/lib/ecosystems/denoutils.js +2 -2
  33. package/lib/ecosystems/golem.js +3 -8
  34. package/lib/ecosystems/gradleutils.js +1082 -6
  35. package/lib/ecosystems/jsEvidence.js +90 -38
  36. package/lib/ecosystems/kosi.js +3 -5
  37. package/lib/ecosystems/npmVersionResolution.js +376 -0
  38. package/lib/ecosystems/parsers-python.js +89 -28
  39. package/lib/ecosystems/parsers-rust.js +117 -15
  40. package/lib/ecosystems/remote/huggingface.js +22 -0
  41. package/lib/ecosystems/rusi.js +2 -4
  42. package/lib/ecosystems/scalasem.js +2 -1
  43. package/lib/evinser/evinser.js +196 -34
  44. package/lib/evinser/scalasem.js +5 -1
  45. package/lib/helpers/agenticToolutils.js +112 -1
  46. package/lib/helpers/bomSigner.js +43 -9
  47. package/lib/helpers/jssSigner.js +1276 -0
  48. package/lib/inventory/agentBlueprint.js +193 -0
  49. package/lib/inventory/agentToolNames.js +145 -0
  50. package/lib/inventory/aiCollector.js +153 -7
  51. package/lib/inventory/aiInventory.js +44 -8
  52. package/lib/inventory/aiProviders.js +324 -0
  53. package/lib/inventory/bomUtils.js +8 -1
  54. package/lib/inventory/ciParsers/githubActionsLock.js +821 -0
  55. package/lib/inventory/citations.js +31 -2
  56. package/lib/inventory/communityAiConfigParser.js +978 -80
  57. package/lib/inventory/cyclonedx20.js +152 -0
  58. package/lib/inventory/cyclonedx20Common.js +286 -0
  59. package/lib/inventory/cyclonedx20Derived.js +159 -0
  60. package/lib/inventory/cyclonedx20Parties.js +552 -0
  61. package/lib/inventory/cyclonedx20Read.js +768 -0
  62. package/lib/inventory/cyclonedx20ReadCommon.js +374 -0
  63. package/lib/inventory/cyclonedx20ReadComponents.js +692 -0
  64. package/lib/inventory/cyclonedx20ReadContext.js +380 -0
  65. package/lib/inventory/cyclonedx20ReadEvidence.js +466 -0
  66. package/lib/inventory/cyclonedx20ReadSections.js +599 -0
  67. package/lib/inventory/cyclonedx20Render.js +766 -0
  68. package/lib/inventory/cyclonedx20RenderComponents.js +716 -0
  69. package/lib/inventory/cyclonedx20RenderDefinitions.js +405 -0
  70. package/lib/inventory/cyclonedx20RenderEvidence.js +726 -0
  71. package/lib/inventory/cyclonedx20RenderHardware.js +203 -0
  72. package/lib/inventory/cyclonedx20RenderPatents.js +174 -0
  73. package/lib/inventory/cyclonedx20RenderRefIds.js +314 -0
  74. package/lib/inventory/cyclonedx20RenderSections.js +499 -0
  75. package/lib/inventory/cyclonedx20Tables.js +594 -0
  76. package/lib/inventory/deps.js +106 -13
  77. package/lib/inventory/dosai.js +236 -1
  78. package/lib/inventory/dosaiParsers.js +3 -6
  79. package/lib/inventory/dotnetRuntime.js +164 -14
  80. package/lib/inventory/ecosystemFormulationParsers.js +979 -0
  81. package/lib/inventory/envcontext.js +132 -38
  82. package/lib/inventory/evidenceUtils.js +2 -1
  83. package/lib/inventory/formulationParsers.js +95 -13
  84. package/lib/inventory/hbom.js +45 -6
  85. package/lib/inventory/hbomLoader.js +166 -0
  86. package/lib/inventory/mcpConfigParser.js +5 -0
  87. package/lib/inventory/occurrenceKey.js +90 -0
  88. package/lib/inventory/pqcMigration.js +426 -0
  89. package/lib/inventory/rustFormulationParser.js +21 -10
  90. package/lib/inventory/source.js +65 -6
  91. package/lib/parsers/huggingfaceManifest.js +9 -1
  92. package/lib/parsers/huggingfaceUtils.js +18 -0
  93. package/lib/parsers/kiroPermissions.js +166 -0
  94. package/lib/server/server.js +36 -34
  95. package/lib/stages/postgen/postgen.js +41 -1
  96. package/lib/stages/postgen/specVersionCompat.js +76 -0
  97. package/lib/validator/bomValidator.js +8 -1
  98. package/lib/validator/complianceEngine.js +30 -3
  99. package/lib/validator/complianceRules.js +1416 -4
  100. package/lib/validator/generated/validate-2.0.mjs +57554 -13851
  101. package/lib/validator/index.js +48 -7
  102. package/lib/validator/reporters/json.js +6 -0
  103. package/package.json +9 -9
  104. package/types/bin/repl.d.ts.map +1 -1
  105. package/types/lib/audit/index.d.ts +6 -2
  106. package/types/lib/audit/index.d.ts.map +1 -1
  107. package/types/lib/cli/bomAssembly.d.ts +16 -0
  108. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  109. package/types/lib/cli/cliOptions.d.ts +6 -0
  110. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  111. package/types/lib/cli/index.d.ts.map +1 -1
  112. package/types/lib/cli/jsBom.d.ts +2 -1
  113. package/types/lib/cli/jsBom.d.ts.map +1 -1
  114. package/types/lib/cli/jvmBom.d.ts +15 -0
  115. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  116. package/types/lib/cli/managedBom.d.ts.map +1 -1
  117. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  118. package/types/lib/core/boundedFile.d.ts +30 -0
  119. package/types/lib/core/boundedFile.d.ts.map +1 -1
  120. package/types/lib/core/env.d.ts +10 -0
  121. package/types/lib/core/env.d.ts.map +1 -1
  122. package/types/lib/core/fs.d.ts +4 -0
  123. package/types/lib/core/fs.d.ts.map +1 -1
  124. package/types/lib/core/globPatterns.d.ts +70 -0
  125. package/types/lib/core/globPatterns.d.ts.map +1 -0
  126. package/types/lib/core/paths.d.ts +9 -0
  127. package/types/lib/core/paths.d.ts.map +1 -1
  128. package/types/lib/core/processTree.d.ts +16 -5
  129. package/types/lib/core/processTree.d.ts.map +1 -1
  130. package/types/lib/core/projectConfig.d.ts +47 -8
  131. package/types/lib/core/projectConfig.d.ts.map +1 -1
  132. package/types/lib/core/toolRegistry.d.ts.map +1 -1
  133. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  134. package/types/lib/ecosystems/gradleutils.d.ts +248 -8
  135. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  136. package/types/lib/ecosystems/jsEvidence.d.ts +14 -1
  137. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  138. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  139. package/types/lib/ecosystems/npmVersionResolution.d.ts +119 -0
  140. package/types/lib/ecosystems/npmVersionResolution.d.ts.map +1 -0
  141. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  142. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  143. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  144. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  145. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  146. package/types/lib/ecosystems/scalasem.d.ts +58 -16
  147. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  148. package/types/lib/evinser/evinser.d.ts +61 -9
  149. package/types/lib/evinser/evinser.d.ts.map +1 -1
  150. package/types/lib/evinser/scalasem.d.ts.map +1 -1
  151. package/types/lib/helpers/agenticToolutils.d.ts +53 -19
  152. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -1
  153. package/types/lib/helpers/bomSigner.d.ts +20 -3
  154. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  155. package/types/lib/helpers/jssSigner.d.ts +288 -0
  156. package/types/lib/helpers/jssSigner.d.ts.map +1 -0
  157. package/types/lib/helpers/vsixutils.d.ts +303 -19
  158. package/types/lib/helpers/vsixutils.d.ts.map +1 -1
  159. package/types/lib/inventory/agentBlueprint.d.ts +70 -0
  160. package/types/lib/inventory/agentBlueprint.d.ts.map +1 -0
  161. package/types/lib/inventory/agentToolNames.d.ts +51 -0
  162. package/types/lib/inventory/agentToolNames.d.ts.map +1 -0
  163. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  164. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  165. package/types/lib/inventory/aiProviders.d.ts +85 -0
  166. package/types/lib/inventory/aiProviders.d.ts.map +1 -0
  167. package/types/lib/inventory/bomUtils.d.ts.map +1 -1
  168. package/types/lib/inventory/ciParsers/githubActionsLock.d.ts +101 -0
  169. package/types/lib/inventory/ciParsers/githubActionsLock.d.ts.map +1 -0
  170. package/types/lib/inventory/citations.d.ts +5 -0
  171. package/types/lib/inventory/citations.d.ts.map +1 -1
  172. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  173. package/types/lib/inventory/cyclonedx20.d.ts +99 -0
  174. package/types/lib/inventory/cyclonedx20.d.ts.map +1 -0
  175. package/types/lib/inventory/cyclonedx20Common.d.ts +102 -0
  176. package/types/lib/inventory/cyclonedx20Common.d.ts.map +1 -0
  177. package/types/lib/inventory/cyclonedx20Derived.d.ts +74 -0
  178. package/types/lib/inventory/cyclonedx20Derived.d.ts.map +1 -0
  179. package/types/lib/inventory/cyclonedx20Parties.d.ts +91 -0
  180. package/types/lib/inventory/cyclonedx20Parties.d.ts.map +1 -0
  181. package/types/lib/inventory/cyclonedx20Read.d.ts +51 -0
  182. package/types/lib/inventory/cyclonedx20Read.d.ts.map +1 -0
  183. package/types/lib/inventory/cyclonedx20ReadCommon.d.ts +69 -0
  184. package/types/lib/inventory/cyclonedx20ReadCommon.d.ts.map +1 -0
  185. package/types/lib/inventory/cyclonedx20ReadComponents.d.ts +77 -0
  186. package/types/lib/inventory/cyclonedx20ReadComponents.d.ts.map +1 -0
  187. package/types/lib/inventory/cyclonedx20ReadContext.d.ts +118 -0
  188. package/types/lib/inventory/cyclonedx20ReadContext.d.ts.map +1 -0
  189. package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts +40 -0
  190. package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts.map +1 -0
  191. package/types/lib/inventory/cyclonedx20ReadSections.d.ts +45 -0
  192. package/types/lib/inventory/cyclonedx20ReadSections.d.ts.map +1 -0
  193. package/types/lib/inventory/cyclonedx20Render.d.ts +53 -0
  194. package/types/lib/inventory/cyclonedx20Render.d.ts.map +1 -0
  195. package/types/lib/inventory/cyclonedx20RenderComponents.d.ts +73 -0
  196. package/types/lib/inventory/cyclonedx20RenderComponents.d.ts.map +1 -0
  197. package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts +29 -0
  198. package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts.map +1 -0
  199. package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts +110 -0
  200. package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts.map +1 -0
  201. package/types/lib/inventory/cyclonedx20RenderHardware.d.ts +42 -0
  202. package/types/lib/inventory/cyclonedx20RenderHardware.d.ts.map +1 -0
  203. package/types/lib/inventory/cyclonedx20RenderPatents.d.ts +26 -0
  204. package/types/lib/inventory/cyclonedx20RenderPatents.d.ts.map +1 -0
  205. package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts +55 -0
  206. package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts.map +1 -0
  207. package/types/lib/inventory/cyclonedx20RenderSections.d.ts +45 -0
  208. package/types/lib/inventory/cyclonedx20RenderSections.d.ts.map +1 -0
  209. package/types/lib/inventory/cyclonedx20Tables.d.ts +134 -0
  210. package/types/lib/inventory/cyclonedx20Tables.d.ts.map +1 -0
  211. package/types/lib/inventory/deps.d.ts +35 -2
  212. package/types/lib/inventory/deps.d.ts.map +1 -1
  213. package/types/lib/inventory/dosai.d.ts +19 -0
  214. package/types/lib/inventory/dosai.d.ts.map +1 -1
  215. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  216. package/types/lib/inventory/dotnetRuntime.d.ts +51 -4
  217. package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -1
  218. package/types/lib/inventory/ecosystemFormulationParsers.d.ts +73 -0
  219. package/types/lib/inventory/ecosystemFormulationParsers.d.ts.map +1 -0
  220. package/types/lib/inventory/envcontext.d.ts +22 -6
  221. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  222. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  223. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  224. package/types/lib/inventory/hbom.d.ts +11 -0
  225. package/types/lib/inventory/hbom.d.ts.map +1 -1
  226. package/types/lib/inventory/hbomLoader.d.ts +54 -0
  227. package/types/lib/inventory/hbomLoader.d.ts.map +1 -1
  228. package/types/lib/inventory/jvmLocalRepos.d.ts +5 -1
  229. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -1
  230. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  231. package/types/lib/inventory/occurrenceKey.d.ts +52 -0
  232. package/types/lib/inventory/occurrenceKey.d.ts.map +1 -0
  233. package/types/lib/inventory/pqcMigration.d.ts +84 -0
  234. package/types/lib/inventory/pqcMigration.d.ts.map +1 -0
  235. package/types/lib/inventory/rustFormulationParser.d.ts +7 -2
  236. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  237. package/types/lib/inventory/scalaCoords.d.ts.map +1 -1
  238. package/types/lib/inventory/scalaDedupe.d.ts +43 -0
  239. package/types/lib/inventory/scalaDedupe.d.ts.map +1 -0
  240. package/types/lib/inventory/source.d.ts +15 -0
  241. package/types/lib/inventory/source.d.ts.map +1 -1
  242. package/types/lib/parsers/huggingfaceManifest.d.ts +10 -1
  243. package/types/lib/parsers/huggingfaceManifest.d.ts.map +1 -1
  244. package/types/lib/parsers/huggingfaceUtils.d.ts +12 -0
  245. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  246. package/types/lib/parsers/kiroPermissions.d.ts +62 -0
  247. package/types/lib/parsers/kiroPermissions.d.ts.map +1 -0
  248. package/types/lib/parsers/vscodeExtensionId.d.ts +48 -0
  249. package/types/lib/parsers/vscodeExtensionId.d.ts.map +1 -0
  250. package/types/lib/server/server.d.ts.map +1 -1
  251. package/types/lib/stages/postgen/postgen.d.ts +7 -0
  252. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  253. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  254. package/types/lib/stages/postgen/specVersionCompat.d.ts +25 -0
  255. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  256. package/types/lib/validator/bomValidator.d.ts +5 -1
  257. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  258. package/types/lib/validator/complianceEngine.d.ts +4 -4
  259. package/types/lib/validator/complianceEngine.d.ts.map +1 -1
  260. package/types/lib/validator/complianceRules.d.ts +14 -2
  261. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  262. package/types/lib/validator/index.d.ts +10 -0
  263. package/types/lib/validator/index.d.ts.map +1 -1
  264. package/types/lib/validator/reporters/json.d.ts.map +1 -1
@@ -12,7 +12,7 @@
12
12
  * id - Stable short identifier (e.g. "SCVS-1.1").
13
13
  * name - Human readable short name.
14
14
  * description - Long description (wording taken from the source standard).
15
- * standard - Source standard key: "SCVS" or "CRA".
15
+ * standard - Source standard key: "SCVS", "CRA" or "CISA".
16
16
  * standardRefs - Array of canonical control identifiers.
17
17
  * category - Grouping used by --categories.
18
18
  * severity - Severity emitted for a failing automatable rule.
@@ -36,7 +36,7 @@
36
36
 
37
37
  import { Purl } from "@cdxgen/cdx-purl";
38
38
 
39
- import { isCycloneDxBom } from "../inventory/bomUtils.js";
39
+ import { getCycloneDxFormat, isCycloneDxBom } from "../inventory/bomUtils.js";
40
40
 
41
41
  /**
42
42
  * Extract the first SPDX-ish license id from a CycloneDX component's licenses
@@ -1634,17 +1634,1411 @@ const CRA_RULES = [
1634
1634
  },
1635
1635
  ];
1636
1636
 
1637
+ // ---------------------------------------------------------------------------
1638
+ // CISA 2026 Minimum Elements for a Software Bill of Materials (SBOM)
1639
+ // Joint guidance of CISA, NSA, FBI and international partners, version 2.1
1640
+ // published on 29 July 2026. It replaces the NTIA 2021 minimum elements with
1641
+ // seventeen data fields (Appendix A, Table 1) and six practices and processes.
1642
+ // Every rule cites the element it implements through the standardRefs array
1643
+ // so users can judge each mapping against the source.
1644
+ // ---------------------------------------------------------------------------
1645
+
1646
+ /**
1647
+ * RFC 9557 (IXDTF) date-time shape. RFC 3339 timestamps such as
1648
+ * "2026-01-02T03:04:05Z" are valid RFC 9557 as well; the pattern also accepts
1649
+ * the bracketed suffix annotations and numeric-offset forms IXDTF adds.
1650
+ */
1651
+ const RFC9557_TIMESTAMP_RE =
1652
+ /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})(\[[^\]]+\])*$/;
1653
+
1654
+ /**
1655
+ * Check the field ranges of an RFC 3339 date-time whose shape
1656
+ * RFC9557_TIMESTAMP_RE already accepted: a real calendar day (leap years
1657
+ * included), hour 0-23, minute 0-59, second 0-60 (RFC 3339 allows a leap
1658
+ * second) and an offset of at most 23:59. Date.parse is not used because it
1659
+ * rolls 2026-02-30 over to 2 March and rejects a leap second.
1660
+ *
1661
+ * @param {string} ts timestamp matching RFC9557_TIMESTAMP_RE
1662
+ * @returns {boolean}
1663
+ */
1664
+ function isValidRfc3339DateTime(ts) {
1665
+ const num = (start, end) => Number.parseInt(ts.slice(start, end), 10);
1666
+ const year = num(0, 4);
1667
+ const month = num(5, 7);
1668
+ const day = num(8, 10);
1669
+ if (month < 1 || month > 12 || day < 1) {
1670
+ return false;
1671
+ }
1672
+ const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
1673
+ const daysInMonth = [
1674
+ 31,
1675
+ leap ? 29 : 28,
1676
+ 31,
1677
+ 30,
1678
+ 31,
1679
+ 30,
1680
+ 31,
1681
+ 31,
1682
+ 30,
1683
+ 31,
1684
+ 30,
1685
+ 31,
1686
+ ];
1687
+ if (day > daysInMonth[month - 1]) {
1688
+ return false;
1689
+ }
1690
+ if (num(11, 13) > 23 || num(14, 16) > 59 || num(17, 19) > 60) {
1691
+ return false;
1692
+ }
1693
+ const bracket = ts.indexOf("[");
1694
+ const base = bracket === -1 ? ts : ts.slice(0, bracket);
1695
+ const last = base[base.length - 1];
1696
+ if (last === "Z" || last === "z") {
1697
+ return true;
1698
+ }
1699
+ const offset = base.slice(-5);
1700
+ return (
1701
+ Number.parseInt(offset.slice(0, 2), 10) <= 23 &&
1702
+ Number.parseInt(offset.slice(3, 5), 10) <= 59
1703
+ );
1704
+ }
1705
+
1706
+ /**
1707
+ * CycloneDX specification versions the maintainers have published. None of
1708
+ * them is declared deprecated, so any of them satisfies the data format
1709
+ * version element; a missing or unpublished version does not.
1710
+ */
1711
+ const PUBLISHED_SPEC_VERSIONS = new Set([
1712
+ "1.0",
1713
+ "1.1",
1714
+ "1.2",
1715
+ "1.3",
1716
+ "1.4",
1717
+ "1.5",
1718
+ "1.6",
1719
+ "1.7",
1720
+ "2.0",
1721
+ ]);
1722
+
1723
+ /** Lifecycle phases CycloneDX defines for metadata.lifecycles. */
1724
+ const LIFECYCLE_PHASES = new Set([
1725
+ "design",
1726
+ "pre-build",
1727
+ "build",
1728
+ "post-build",
1729
+ "operations",
1730
+ "discovery",
1731
+ "decommission",
1732
+ ]);
1733
+
1734
+ /**
1735
+ * Lifecycle phases at which the target component exists as an executable
1736
+ * artifact. Before and during the build there is no artifact of the target
1737
+ * to hash yet.
1738
+ */
1739
+ const ARTIFACT_PHASES = new Set([
1740
+ "post-build",
1741
+ "operations",
1742
+ "discovery",
1743
+ "decommission",
1744
+ ]);
1745
+
1746
+ /** Hash algorithms CISA would not accept as the sole hash (not NIST approved). */
1747
+ const WEAK_HASH_ALGS = new Set(["MD5", "SHA-1"]);
1748
+
1749
+ /**
1750
+ * JSF algorithms whose signatures are attributable to a single key holder:
1751
+ * RSA, RSA-PSS, ECDSA and EdDSA, all part of the NIST Digital Signature
1752
+ * Standard (FIPS 186-5). HS256, HS384 and HS512 are HMACs: anyone holding the
1753
+ * shared secret computes the same value, so they do not identify the author.
1754
+ */
1755
+ const ATTRIBUTABLE_JSF_ALGORITHMS = new Set([
1756
+ "RS256",
1757
+ "RS384",
1758
+ "RS512",
1759
+ "PS256",
1760
+ "PS384",
1761
+ "PS512",
1762
+ "ES256",
1763
+ "ES384",
1764
+ "ES512",
1765
+ "Ed25519",
1766
+ "Ed448",
1767
+ ]);
1768
+
1769
+ /**
1770
+ * Component types the CISA component elements apply to: software of every
1771
+ * kind, firmware and AI models included. Data, files, devices, platforms and
1772
+ * cryptographic assets are left to the rule sets that model them.
1773
+ */
1774
+ const CISA_COMPONENT_TYPES = new Set([
1775
+ "application",
1776
+ "framework",
1777
+ "library",
1778
+ "container",
1779
+ "operating-system",
1780
+ "firmware",
1781
+ "device-driver",
1782
+ "machine-learning-model",
1783
+ ]);
1784
+
1785
+ /**
1786
+ * Collect the tool entries of metadata.tools across both shapes CycloneDX
1787
+ * allows: the 1.4 array and the 1.5+ object with components and services.
1788
+ *
1789
+ * @param {object} bomJson
1790
+ * @returns {Array<object>}
1791
+ */
1792
+ function toolEntries(bomJson) {
1793
+ const tools = bomJson?.metadata?.tools;
1794
+ if (Array.isArray(tools)) {
1795
+ return tools;
1796
+ }
1797
+ return [...(tools?.components || []), ...(tools?.services || [])];
1798
+ }
1799
+
1800
+ /**
1801
+ * The entries of a JSF signature: the signers of a multi-signature, the
1802
+ * links of a signature chain, or the single signature itself.
1803
+ *
1804
+ * @param {object} signature the BOM's top-level signature
1805
+ * @returns {Array<object>}
1806
+ */
1807
+ function jsfSignatureEntries(signature) {
1808
+ if (!signature || typeof signature !== "object") {
1809
+ return [];
1810
+ }
1811
+ if (Array.isArray(signature.signers)) {
1812
+ return signature.signers;
1813
+ }
1814
+ if (Array.isArray(signature.chain)) {
1815
+ return signature.chain;
1816
+ }
1817
+ return [signature];
1818
+ }
1819
+
1820
+ /**
1821
+ * The producer a component names. CycloneDX manufacturer is the organisation
1822
+ * that created the component and authors are the people who did, which is
1823
+ * what the element asks for. supplier, the field the NTIA 2021 Supplier Name
1824
+ * element mapped to, may name a distributor, but it is what most ecosystems
1825
+ * record and it carries the explicit "unknown" provenance marking.
1826
+ *
1827
+ * @param {object} comp
1828
+ * @returns {string | undefined}
1829
+ */
1830
+ function componentProducerName(comp) {
1831
+ if (comp?.manufacturer?.name) {
1832
+ return comp.manufacturer.name;
1833
+ }
1834
+ if (comp?.supplier?.name) {
1835
+ return comp.supplier.name;
1836
+ }
1837
+ const author = Array.isArray(comp?.authors)
1838
+ ? comp.authors.find((a) => a?.name)
1839
+ : undefined;
1840
+ if (author) {
1841
+ return author.name;
1842
+ }
1843
+ // The free-text author that CycloneDX 1.6 deprecated in favour of authors.
1844
+ if (typeof comp?.author === "string" && comp.author.trim()) {
1845
+ return comp.author;
1846
+ }
1847
+ return undefined;
1848
+ }
1849
+
1850
+ /**
1851
+ * Whether a component carries a software identifier the element names: a
1852
+ * purl, a CPE, a SWID tag, an OmniBOR artifact id or a SWHID.
1853
+ *
1854
+ * @param {object} comp
1855
+ * @returns {boolean}
1856
+ */
1857
+ function hasSoftwareIdentifier(comp) {
1858
+ return Boolean(
1859
+ comp?.purl ||
1860
+ comp?.cpe ||
1861
+ comp?.swid?.tagId ||
1862
+ (Array.isArray(comp?.omniborId) && comp.omniborId.length) ||
1863
+ (Array.isArray(comp?.swhid) && comp.swhid.length),
1864
+ );
1865
+ }
1866
+
1867
+ /**
1868
+ * Whether a component carries a hexadecimal hash from an algorithm other
1869
+ * than MD5 or SHA-1.
1870
+ *
1871
+ * @param {object} comp
1872
+ * @returns {boolean}
1873
+ */
1874
+ function hasApprovedHash(comp) {
1875
+ return (
1876
+ Array.isArray(comp?.hashes) &&
1877
+ comp.hashes.some(
1878
+ (h) =>
1879
+ h?.alg &&
1880
+ !WEAK_HASH_ALGS.has(h.alg) &&
1881
+ typeof h.content === "string" &&
1882
+ /^[0-9a-fA-F]+$/.test(h.content),
1883
+ )
1884
+ );
1885
+ }
1886
+
1887
+ /**
1888
+ * The lifecycle phases and custom lifecycle names in metadata.lifecycles.
1889
+ *
1890
+ * @param {object} bomJson
1891
+ * @returns {{ phases: string[], names: string[] }}
1892
+ */
1893
+ function lifecycleEntries(bomJson) {
1894
+ const lifecycles = Array.isArray(bomJson?.metadata?.lifecycles)
1895
+ ? bomJson.metadata.lifecycles
1896
+ : [];
1897
+ return {
1898
+ phases: lifecycles
1899
+ .map((l) => l?.phase)
1900
+ .filter((p) => LIFECYCLE_PHASES.has(p)),
1901
+ names: lifecycles
1902
+ .map((l) => (typeof l?.name === "string" ? l.name.trim() : ""))
1903
+ .filter(Boolean),
1904
+ };
1905
+ }
1906
+
1907
+ /**
1908
+ * The component set the CISA 2026 component rules judge. The component
1909
+ * elements cover "the target component (the component for which the SBOM is
1910
+ * generated) and all enumerated subcomponents", so metadata.component comes
1911
+ * first, followed by the software components. AI models are included because
1912
+ * the AI SBOM guidance applies the same minimum elements to them; datasets
1913
+ * are checked by the AI rule set instead.
1914
+ *
1915
+ * @param {object} bomJson
1916
+ * @returns {Array<object>}
1917
+ */
1918
+ function cisaComponents(bomJson) {
1919
+ const comps = getAllComponents(bomJson).filter((c) =>
1920
+ CISA_COMPONENT_TYPES.has(c?.type),
1921
+ );
1922
+ const target = bomJson?.metadata?.component;
1923
+ if (
1924
+ target &&
1925
+ !comps.some((c) => target["bom-ref"] && c["bom-ref"] === target["bom-ref"])
1926
+ ) {
1927
+ comps.unshift(target);
1928
+ }
1929
+ return comps;
1930
+ }
1931
+
1932
+ /**
1933
+ * The subcomponents the CISA 2026 rules judge: cisaComponents without the
1934
+ * target component.
1935
+ *
1936
+ * @param {object} bomJson
1937
+ * @returns {Array<object>}
1938
+ */
1939
+ function cisaSubcomponents(bomJson) {
1940
+ const target = bomJson?.metadata?.component;
1941
+ return cisaComponents(bomJson).filter((c) => c !== target);
1942
+ }
1943
+
1944
+ /**
1945
+ * Map components to the location entries rule results carry.
1946
+ *
1947
+ * @param {Array<object>} comps
1948
+ * @returns {Array<object>}
1949
+ */
1950
+ function cisaLocations(comps) {
1951
+ return comps.slice(0, 25).map((c) => ({
1952
+ bomRef: c["bom-ref"],
1953
+ name: c.name,
1954
+ purl: c.purl,
1955
+ }));
1956
+ }
1957
+
1958
+ /** The --author default of bin/cdxgen.js: the tool's maintainer. */
1959
+ const CDXGEN_DEFAULT_AUTHOR = "OWASP Foundation";
1960
+
1961
+ const AUTHOR_MITIGATION =
1962
+ "cdxgen records the SBOM author only from the --author option; pass the person or organisation generating the SBOM (repeat --author for several).";
1963
+
1964
+ /** @type {Array<object>} */
1965
+ const CISA_RULES = [
1966
+ {
1967
+ id: "CISA-MIN-001",
1968
+ name: "SBOM author identified",
1969
+ description:
1970
+ "CISA 2026 SBOM Author: the name of the entity that creates the SBOM data for the target component. The element captures the entity operating the tool, not the tool itself.",
1971
+ standard: "CISA",
1972
+ standardRefs: ["CISA-2026-SBOM-AUTHOR"],
1973
+ category: "compliance-cisa",
1974
+ severity: "high",
1975
+ scvsLevels: [],
1976
+ automatable: true,
1977
+ evaluate(bomJson) {
1978
+ const names = [];
1979
+ if (Array.isArray(bomJson?.metadata?.authors)) {
1980
+ for (const a of bomJson.metadata.authors) {
1981
+ if (a?.name) {
1982
+ names.push(a.name);
1983
+ }
1984
+ }
1985
+ }
1986
+ // From CycloneDX 1.6, metadata.manufacturer is the organisation that
1987
+ // created the BOM, the usual record for automated generation.
1988
+ if (bomJson?.metadata?.manufacturer?.name) {
1989
+ names.push(bomJson.metadata.manufacturer.name);
1990
+ }
1991
+ if (!names.length) {
1992
+ return fail(
1993
+ "Neither metadata.authors nor metadata.manufacturer names the SBOM author.",
1994
+ { mitigation: AUTHOR_MITIGATION },
1995
+ );
1996
+ }
1997
+ // cdxgen writes its maintainer as the author when --author is not
1998
+ // given. That names the tool, which the element excludes, so a list
1999
+ // holding only the default does not satisfy it.
2000
+ if (names.every((n) => n.trim() === CDXGEN_DEFAULT_AUTHOR)) {
2001
+ return fail(
2002
+ `The BOM names only "${CDXGEN_DEFAULT_AUTHOR}" as its author, the default cdxgen writes when --author is not given; it identifies the tool's maintainer, not the entity that created the SBOM.`,
2003
+ { mitigation: AUTHOR_MITIGATION },
2004
+ );
2005
+ }
2006
+ return pass(`SBOM author(s) recorded: ${names.join(", ")}.`);
2007
+ },
2008
+ },
2009
+ {
2010
+ id: "CISA-MIN-002",
2011
+ name: "SBOM author signature present",
2012
+ description:
2013
+ "CISA 2026 SBOM Author Signature: a digital signature attributable to the SBOM author, giving assurance the claimed signatory signed the data and it was not modified afterwards, using a signature algorithm approved by an authority such as the NIST Digital Signature Standard.",
2014
+ standard: "CISA",
2015
+ standardRefs: ["CISA-2026-SBOM-AUTHOR-SIGNATURE"],
2016
+ category: "compliance-cisa",
2017
+ severity: "medium",
2018
+ scvsLevels: [],
2019
+ automatable: true,
2020
+ evaluate(bomJson) {
2021
+ const entries = jsfSignatureEntries(bomJson?.signature).filter(
2022
+ (e) => typeof e?.value === "string" && e.value.length > 0,
2023
+ );
2024
+ if (!entries.length) {
2025
+ return fail("The BOM carries no JSF signature value.", {
2026
+ mitigation:
2027
+ "Sign the generated BOM with cdx-sign (or another JSON Signature Format signer) and an RSA, ECDSA or EdDSA key before distributing it.",
2028
+ });
2029
+ }
2030
+ const algorithms = [...new Set(entries.map((e) => e.algorithm))];
2031
+ if (!entries.some((e) => ATTRIBUTABLE_JSF_ALGORITHMS.has(e.algorithm))) {
2032
+ return fail(
2033
+ `The BOM signature uses ${algorithms.join(", ")}, which does not attribute it to one signer; an HMAC can be computed by anyone holding the shared secret.`,
2034
+ {
2035
+ mitigation:
2036
+ "Sign with an RSA, RSA-PSS, ECDSA or EdDSA key, for example cdx-sign with an RS256, ES256 or Ed25519 key.",
2037
+ },
2038
+ );
2039
+ }
2040
+ return pass(
2041
+ `A JSF signature (${algorithms.join(", ")}) is present on the BOM; cdx-verify checks it against the signer's public key.`,
2042
+ );
2043
+ },
2044
+ },
2045
+ {
2046
+ id: "CISA-MIN-003",
2047
+ name: "Data format named with a current version",
2048
+ description:
2049
+ "CISA 2026 SBOM Data Format Name and SBOM Data Format Version: the machine-processable format holding the SBOM and the version of that format, with the Machine-Processable Data practice naming SPDX and CycloneDX. Versions declared deprecated by the format maintainers should not be used.",
2050
+ standard: "CISA",
2051
+ standardRefs: [
2052
+ "CISA-2026-SBOM-DATA-FORMAT-NAME",
2053
+ "CISA-2026-SBOM-DATA-FORMAT-VERSION",
2054
+ "CISA-2026-PRACTICE-MACHINE-PROCESSABLE-DATA",
2055
+ ],
2056
+ category: "compliance-cisa",
2057
+ severity: "high",
2058
+ scvsLevels: [],
2059
+ automatable: true,
2060
+ evaluate(bomJson) {
2061
+ // CycloneDX 1.x names the format in bomFormat and 2.0 in specFormat.
2062
+ const format = getCycloneDxFormat(bomJson);
2063
+ if (format !== "CycloneDX") {
2064
+ return fail(
2065
+ `bomFormat or specFormat is ${JSON.stringify(format)}, expected "CycloneDX".`,
2066
+ );
2067
+ }
2068
+ const spec = bomJson?.specVersion;
2069
+ if (!spec) {
2070
+ return fail("specVersion is missing.", {
2071
+ mitigation:
2072
+ "Regenerate with a specVersion; cdxgen writes 1.7 by default and --spec-version overrides it.",
2073
+ });
2074
+ }
2075
+ if (!PUBLISHED_SPEC_VERSIONS.has(String(spec))) {
2076
+ return fail(
2077
+ `specVersion ${spec} is not a published CycloneDX version.`,
2078
+ {
2079
+ mitigation:
2080
+ "Regenerate with a published version such as --spec-version 1.7.",
2081
+ },
2082
+ );
2083
+ }
2084
+ return pass(`CycloneDX at specVersion ${spec}.`);
2085
+ },
2086
+ },
2087
+ {
2088
+ id: "CISA-MIN-004",
2089
+ name: "Generation context recorded",
2090
+ description:
2091
+ 'CISA 2026 SBOM Generation Context: the relative software lifecycle phase and data available when the SBOM author generated the SBOM. General references such as "before build", "build" and "after build", or more specific identifiers, satisfy the element.',
2092
+ standard: "CISA",
2093
+ standardRefs: ["CISA-2026-SBOM-GENERATION-CONTEXT"],
2094
+ category: "compliance-cisa",
2095
+ severity: "medium",
2096
+ scvsLevels: [],
2097
+ automatable: true,
2098
+ evaluate(bomJson) {
2099
+ const { phases, names } = lifecycleEntries(bomJson);
2100
+ if (!phases.length && !names.length) {
2101
+ return fail(
2102
+ "metadata.lifecycles records neither a lifecycle phase nor a named lifecycle.",
2103
+ {
2104
+ mitigation:
2105
+ 'cdxgen writes the phase itself ("pre-build", "build" or "post-build"); --lifecycle overrides it.',
2106
+ },
2107
+ );
2108
+ }
2109
+ return pass(`Generation context: ${[...phases, ...names].join(", ")}.`);
2110
+ },
2111
+ },
2112
+ {
2113
+ id: "CISA-MIN-005",
2114
+ name: "Tool name and version recorded",
2115
+ description:
2116
+ "CISA 2026 SBOM Tool Name and SBOM Tool Version: the software tool the SBOM author used to generate or amend the SBOM, and its version. When no version is available the author should indicate the information is unknown.",
2117
+ standard: "CISA",
2118
+ standardRefs: ["CISA-2026-SBOM-TOOL-NAME", "CISA-2026-SBOM-TOOL-VERSION"],
2119
+ category: "compliance-cisa",
2120
+ severity: "medium",
2121
+ scvsLevels: [],
2122
+ automatable: true,
2123
+ evaluate(bomJson) {
2124
+ const entries = toolEntries(bomJson);
2125
+ if (!entries.length) {
2126
+ return fail("metadata.tools records no tool.", {
2127
+ mitigation:
2128
+ "cdxgen records itself in metadata.tools.components on every BOM it generates.",
2129
+ });
2130
+ }
2131
+ const unnamed = entries.filter((t) => !t?.name);
2132
+ // A version of "unknown" is the explicit statement the element asks
2133
+ // for, so only an absent version fails.
2134
+ const unversioned = entries.filter((t) => !t?.version);
2135
+ if (unnamed.length || unversioned.length) {
2136
+ const details = [];
2137
+ if (unnamed.length) {
2138
+ details.push(`${unnamed.length} tool entry/entries without a name`);
2139
+ }
2140
+ if (unversioned.length) {
2141
+ details.push(
2142
+ `${unversioned.length} tool entry/entries without a version or an explicit "unknown"`,
2143
+ );
2144
+ }
2145
+ return fail(`metadata.tools is incomplete: ${details.join("; ")}.`, {
2146
+ locations: [...new Set([...unnamed, ...unversioned])]
2147
+ .slice(0, 25)
2148
+ .map((t) => ({ name: t?.name })),
2149
+ });
2150
+ }
2151
+ return pass(
2152
+ `${entries.length} tool(s) recorded with name and version: ${entries.map((t) => `${t.name}@${t.version}`).join(", ")}.`,
2153
+ );
2154
+ },
2155
+ },
2156
+ {
2157
+ id: "CISA-MIN-006",
2158
+ name: "SBOM version recorded",
2159
+ description:
2160
+ "CISA 2026 SBOM Version: an identifier the SBOM author uses to specify a change from a previously identified version or to mark the first version. A serial number used to tell SBOM versions apart should conform to standards such as RFC 9562.",
2161
+ standard: "CISA",
2162
+ standardRefs: ["CISA-2026-SBOM-VERSION"],
2163
+ category: "compliance-cisa",
2164
+ severity: "medium",
2165
+ scvsLevels: [],
2166
+ automatable: true,
2167
+ evaluate(bomJson) {
2168
+ const v = bomJson?.version;
2169
+ if (!Number.isInteger(v) || v < 1) {
2170
+ return fail(
2171
+ `version is ${JSON.stringify(v)}, expected an integer of 1 or more.`,
2172
+ {
2173
+ mitigation:
2174
+ "cdxgen writes version 1 for a first BOM; tools that amend the document should increment it.",
2175
+ },
2176
+ );
2177
+ }
2178
+ const serial = bomJson?.serialNumber;
2179
+ if (
2180
+ serial !== undefined &&
2181
+ !/^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
2182
+ serial,
2183
+ )
2184
+ ) {
2185
+ return fail(
2186
+ `serialNumber ${JSON.stringify(serial)} is not an RFC 9562 UUID URN.`,
2187
+ {
2188
+ mitigation:
2189
+ 'Write the serial number as "urn:uuid:" followed by a UUID; cdxgen generates one for every BOM.',
2190
+ },
2191
+ );
2192
+ }
2193
+ return pass(
2194
+ serial
2195
+ ? `SBOM version ${v} of ${serial}.`
2196
+ : `SBOM version ${v} (no serial number).`,
2197
+ );
2198
+ },
2199
+ },
2200
+ {
2201
+ id: "CISA-MIN-007",
2202
+ name: "Timestamp in RFC 9557 form",
2203
+ description:
2204
+ "CISA 2026 SBOM Timestamp: the date and time of the most recent update to the SBOM data. The field content should adhere to RFC 9557.",
2205
+ standard: "CISA",
2206
+ standardRefs: ["CISA-2026-SBOM-TIMESTAMP"],
2207
+ category: "compliance-cisa",
2208
+ severity: "high",
2209
+ scvsLevels: [],
2210
+ automatable: true,
2211
+ evaluate(bomJson) {
2212
+ const ts = bomJson?.metadata?.timestamp;
2213
+ if (!ts) {
2214
+ return fail("metadata.timestamp is missing.");
2215
+ }
2216
+ // The pattern checks the RFC 9557 shape, IXDTF suffix annotations
2217
+ // included; the field ranges are then checked on the RFC 3339 part.
2218
+ if (
2219
+ typeof ts !== "string" ||
2220
+ !RFC9557_TIMESTAMP_RE.test(ts) ||
2221
+ !isValidRfc3339DateTime(ts)
2222
+ ) {
2223
+ return fail(
2224
+ `metadata.timestamp is not RFC 9557 (IXDTF) shaped: ${ts}`,
2225
+ {
2226
+ mitigation:
2227
+ 'Write an RFC 3339/9557 date-time with its offset, for example "2026-07-29T12:00:00Z". A date-only value does not satisfy the element.',
2228
+ },
2229
+ );
2230
+ }
2231
+ return pass(`metadata.timestamp present (${ts}).`);
2232
+ },
2233
+ },
2234
+ {
2235
+ id: "CISA-MIN-008",
2236
+ name: "Component names recorded",
2237
+ description:
2238
+ "CISA 2026 Component Name: the name assigned by the component producer to a software component, in human-readable form, for the target component and every subcomponent.",
2239
+ standard: "CISA",
2240
+ standardRefs: ["CISA-2026-COMPONENT-NAME"],
2241
+ category: "compliance-cisa",
2242
+ severity: "high",
2243
+ scvsLevels: [],
2244
+ automatable: true,
2245
+ evaluate(bomJson) {
2246
+ if (!bomJson?.metadata?.component) {
2247
+ return fail(
2248
+ "metadata.component is missing, so the BOM does not identify its target component.",
2249
+ {
2250
+ mitigation:
2251
+ "Regenerate from the project root so cdxgen records the project as metadata.component; --project-name and --project-version set its name and version.",
2252
+ },
2253
+ );
2254
+ }
2255
+ const comps = cisaComponents(bomJson);
2256
+ const missing = comps.filter((c) => !c.name);
2257
+ if (missing.length) {
2258
+ return fail(
2259
+ `${missing.length}/${comps.length} component(s) have no name.`,
2260
+ { locations: cisaLocations(missing) },
2261
+ );
2262
+ }
2263
+ return pass(`All ${comps.length} component(s) carry a name.`);
2264
+ },
2265
+ },
2266
+ {
2267
+ id: "CISA-MIN-009",
2268
+ name: "Component versions recorded",
2269
+ description:
2270
+ "CISA 2026 Component Version: the identifier the component producer uses to specify a change in a component, for the target component and every subcomponent. When the producer supplies no version the SBOM author should mark the information unknown.",
2271
+ standard: "CISA",
2272
+ standardRefs: ["CISA-2026-COMPONENT-VERSION"],
2273
+ category: "compliance-cisa",
2274
+ severity: "high",
2275
+ scvsLevels: [],
2276
+ automatable: true,
2277
+ evaluate(bomJson) {
2278
+ const comps = cisaComponents(bomJson);
2279
+ if (!comps.length) {
2280
+ return fail("BOM has no target component and no inventory components.");
2281
+ }
2282
+ // "unknown" is a non-empty version, so the explicit marker passes.
2283
+ const missing = comps.filter((c) => !c.version);
2284
+ if (missing.length) {
2285
+ return fail(
2286
+ `${missing.length}/${comps.length} component(s) have neither a version nor an explicit "unknown".`,
2287
+ {
2288
+ mitigation:
2289
+ 'Regenerate so every component resolves a version (--project-version sets the target\'s), or set version to the string "unknown" where the producer publishes none.',
2290
+ locations: cisaLocations(missing),
2291
+ },
2292
+ );
2293
+ }
2294
+ return pass(`All ${comps.length} component(s) carry a version.`);
2295
+ },
2296
+ },
2297
+ {
2298
+ id: "CISA-MIN-010",
2299
+ name: "Component producer identified",
2300
+ description:
2301
+ "CISA 2026 Component Producer: the entity that creates, defines and identifies a component, replacing the NTIA 2021 Supplier Name. Where there is no clear producer, the SBOM author should explicitly indicate the component is of unknown provenance.",
2302
+ standard: "CISA",
2303
+ standardRefs: ["CISA-2026-COMPONENT-PRODUCER"],
2304
+ category: "compliance-cisa",
2305
+ severity: "medium",
2306
+ scvsLevels: [],
2307
+ automatable: true,
2308
+ evaluate(bomJson) {
2309
+ const comps = cisaComponents(bomJson);
2310
+ if (!comps.length) {
2311
+ return fail("BOM has no target component and no inventory components.");
2312
+ }
2313
+ // A producer name of "unknown" counts: it is the explicit
2314
+ // unknown-provenance marking the element asks for.
2315
+ const missing = comps.filter((c) => !componentProducerName(c));
2316
+ if (missing.length) {
2317
+ return fail(
2318
+ `${missing.length}/${comps.length} component(s) declare no producer (manufacturer, supplier or authors).`,
2319
+ {
2320
+ mitigation:
2321
+ 'Record the creating organisation in manufacturer.name or supplier.name, or set supplier.name to "unknown" to mark the component of unknown provenance.',
2322
+ locations: cisaLocations(missing),
2323
+ },
2324
+ );
2325
+ }
2326
+ return pass(
2327
+ `All ${comps.length} component(s) name a producer or mark it unknown.`,
2328
+ );
2329
+ },
2330
+ },
2331
+ {
2332
+ id: "CISA-MIN-011",
2333
+ name: "Machine-processable component identifiers",
2334
+ description:
2335
+ "CISA 2026 Component Identifiers: at least one software identifier per component, using common identifiers such as CPE and Package-URL, or intrinsic identifiers such as OmniBOR and SWHID, so tools can correlate the component with vulnerability and license databases.",
2336
+ standard: "CISA",
2337
+ standardRefs: ["CISA-2026-COMPONENT-IDENTIFIERS"],
2338
+ category: "compliance-cisa",
2339
+ severity: "high",
2340
+ scvsLevels: [],
2341
+ automatable: true,
2342
+ evaluate(bomJson) {
2343
+ const comps = cisaComponents(bomJson);
2344
+ if (!comps.length) {
2345
+ return fail("BOM has no target component and no inventory components.");
2346
+ }
2347
+ const missing = comps.filter((c) => !hasSoftwareIdentifier(c));
2348
+ if (missing.length) {
2349
+ return fail(
2350
+ `${missing.length}/${comps.length} component(s) have no purl, cpe, swid, omniborId or swhid identifier.`,
2351
+ {
2352
+ mitigation: "Regenerate with cdxgen so purls are emitted.",
2353
+ locations: cisaLocations(missing),
2354
+ },
2355
+ );
2356
+ }
2357
+ return pass(
2358
+ `All ${comps.length} component(s) carry a machine-processable identifier.`,
2359
+ );
2360
+ },
2361
+ },
2362
+ {
2363
+ id: "CISA-MIN-012",
2364
+ name: "Component hashes with a named algorithm",
2365
+ description:
2366
+ "CISA 2026 Component Hash Value and Component Hash Algorithm: a hexadecimal hash of the executable component artifact and the cryptographic algorithm that produced it, approved by an authority such as NIST. The target component is judged once the generation context is after build, when its executable artifact exists.",
2367
+ standard: "CISA",
2368
+ standardRefs: [
2369
+ "CISA-2026-COMPONENT-HASH-VALUE",
2370
+ "CISA-2026-COMPONENT-HASH-ALGORITHM",
2371
+ ],
2372
+ category: "compliance-cisa",
2373
+ severity: "medium",
2374
+ scvsLevels: [],
2375
+ automatable: true,
2376
+ evaluate(bomJson) {
2377
+ const { phases } = lifecycleEntries(bomJson);
2378
+ const comps = phases.some((p) => ARTIFACT_PHASES.has(p))
2379
+ ? cisaComponents(bomJson)
2380
+ : cisaSubcomponents(bomJson);
2381
+ if (!comps.length) {
2382
+ return fail("BOM has no inventory components.");
2383
+ }
2384
+ const missing = comps.filter((c) => !hasApprovedHash(c));
2385
+ if (missing.length) {
2386
+ return fail(
2387
+ `${missing.length}/${comps.length} component(s) lack a hexadecimal hash from an approved algorithm (MD5 and SHA-1 do not satisfy the element alone).`,
2388
+ {
2389
+ mitigation:
2390
+ "Regenerate where the package artifacts or a SHA-256-or-stronger integrity value is reachable; some ecosystems publish only SHA-1, which leaves the element partially satisfied.",
2391
+ locations: cisaLocations(missing),
2392
+ },
2393
+ );
2394
+ }
2395
+ return pass(
2396
+ `All ${comps.length} component(s) carry a hash with a named algorithm.`,
2397
+ );
2398
+ },
2399
+ },
2400
+ {
2401
+ id: "CISA-MIN-013",
2402
+ name: "Component licenses identified",
2403
+ description:
2404
+ "CISA 2026 Component License: the identifiers for the licenses under which the target component and every subcomponent are available, machine-processable where possible such as SPDX identifiers. When the license is not known the SBOM author should say so.",
2405
+ standard: "CISA",
2406
+ standardRefs: ["CISA-2026-COMPONENT-LICENSE"],
2407
+ category: "compliance-cisa",
2408
+ severity: "medium",
2409
+ scvsLevels: [],
2410
+ automatable: true,
2411
+ evaluate(bomJson) {
2412
+ const comps = cisaComponents(bomJson);
2413
+ if (!comps.length) {
2414
+ return fail("BOM has no target component and no inventory components.");
2415
+ }
2416
+ const missing = comps.filter((c) => !componentLicenseId(c));
2417
+ if (missing.length) {
2418
+ return fail(
2419
+ `${missing.length}/${comps.length} component(s) declare no license identifier or name.`,
2420
+ {
2421
+ mitigation:
2422
+ 'Declare the project license in its manifest, run cdxgen with FETCH_LICENSE=true so dependency licenses are looked up, or record license.name "unknown" where the license is genuinely not known.',
2423
+ locations: cisaLocations(missing),
2424
+ },
2425
+ );
2426
+ }
2427
+ return pass(
2428
+ `All ${comps.length} component(s) declare license information.`,
2429
+ );
2430
+ },
2431
+ },
2432
+ {
2433
+ id: "CISA-MIN-014",
2434
+ name: "Dependency relationships cover the inventory",
2435
+ description:
2436
+ "CISA 2026 Component Dependency Relationship and the Coverage practice: the relationship between the target component and its subcomponents, so a dependency graph can be built, covering every component that makes up the target software, transitive dependencies included, with no minimum depth.",
2437
+ standard: "CISA",
2438
+ standardRefs: [
2439
+ "CISA-2026-COMPONENT-DEPENDENCY-RELATIONSHIP",
2440
+ "CISA-2026-PRACTICE-COVERAGE",
2441
+ ],
2442
+ category: "compliance-cisa",
2443
+ severity: "high",
2444
+ scvsLevels: [],
2445
+ automatable: true,
2446
+ evaluate(bomJson) {
2447
+ const comps = cisaSubcomponents(bomJson);
2448
+ if (!comps.length) {
2449
+ return fail("BOM has no inventory components.");
2450
+ }
2451
+ const deps = Array.isArray(bomJson?.dependencies)
2452
+ ? bomJson.dependencies
2453
+ : [];
2454
+ if (!deps.length) {
2455
+ return fail("BOM has components but no dependency relationships.", {
2456
+ mitigation:
2457
+ "Run cdxgen with access to the lockfiles so the dependency graph is captured.",
2458
+ });
2459
+ }
2460
+ // A component is in the graph when another node depends on or
2461
+ // provides it. Its own node alone, with no edge leading to it, does
2462
+ // not relate it to the target.
2463
+ const related = new Set();
2464
+ for (const dep of deps) {
2465
+ for (const child of dep?.dependsOn || []) {
2466
+ related.add(child);
2467
+ }
2468
+ for (const prov of dep?.provides || []) {
2469
+ related.add(prov);
2470
+ }
2471
+ }
2472
+ const uncovered = comps.filter(
2473
+ (c) => !c["bom-ref"] || !related.has(c["bom-ref"]),
2474
+ );
2475
+ if (uncovered.length) {
2476
+ return fail(
2477
+ `${uncovered.length}/${comps.length} component(s) are not the target of any dependency relationship, so their place in the graph is unknown.`,
2478
+ {
2479
+ mitigation:
2480
+ "Regenerate with the lockfiles or build tool available so every component is linked into the dependency graph.",
2481
+ locations: cisaLocations(uncovered),
2482
+ },
2483
+ );
2484
+ }
2485
+ return pass(
2486
+ `${deps.length} dependency node(s) relate all ${comps.length} component(s).`,
2487
+ );
2488
+ },
2489
+ },
2490
+ {
2491
+ id: "CISA-MIN-015",
2492
+ name: "Unknown versus withheld information",
2493
+ description:
2494
+ "CISA 2026 practice Explicitly Identifying Unknown Information: when data required for an element is not provided, the SBOM author should state whether the information is unknown or withheld, and keep a process for recipients to ask about redacted data.",
2495
+ standard: "CISA",
2496
+ standardRefs: ["CISA-2026-PRACTICE-UNKNOWN-INFORMATION"],
2497
+ category: "compliance-cisa",
2498
+ severity: "info",
2499
+ scvsLevels: [],
2500
+ automatable: false,
2501
+ evaluate: () =>
2502
+ manual(
2503
+ "Whether omitted values are unknown or withheld, and the process for recipients to ask about redacted data, cannot be judged from the BOM alone.",
2504
+ {
2505
+ mitigation:
2506
+ 'Keep a published process for recipients to ask about withheld data, and mark genuinely absent values with "unknown" as the other CISA rules check.',
2507
+ },
2508
+ ),
2509
+ },
2510
+ {
2511
+ id: "CISA-MIN-016",
2512
+ name: "Updates to SBOM data accommodated",
2513
+ description:
2514
+ "CISA 2026 practice Accommodation of Updates to SBOM Data: organisations should accommodate updates to SBOM data, including corrections, and SBOM authors should correct errors promptly.",
2515
+ standard: "CISA",
2516
+ standardRefs: ["CISA-2026-PRACTICE-ACCOMMODATION-OF-UPDATES"],
2517
+ category: "compliance-cisa",
2518
+ severity: "info",
2519
+ scvsLevels: [],
2520
+ automatable: false,
2521
+ evaluate: () =>
2522
+ manual(
2523
+ "Whether corrections reach recipients promptly is a process question a single BOM cannot answer.",
2524
+ {
2525
+ mitigation:
2526
+ "Publish a corrected BOM with the same serialNumber and an incremented version, and sign it again with cdx-sign.",
2527
+ },
2528
+ ),
2529
+ },
2530
+ {
2531
+ id: "CISA-MIN-017",
2532
+ name: "SBOM distribution and delivery",
2533
+ description:
2534
+ "CISA 2026 practice Distribution and Delivery: SBOMs should be available promptly to those who need them, for example alongside the installation, at a version-specific URL, through an API or in a public repository. Access controls may limit sharing with unauthorised parties but should not prevent sharing between authorised ones.",
2535
+ standard: "CISA",
2536
+ standardRefs: ["CISA-2026-PRACTICE-DISTRIBUTION-AND-DELIVERY"],
2537
+ category: "compliance-cisa",
2538
+ severity: "info",
2539
+ scvsLevels: [],
2540
+ automatable: false,
2541
+ evaluate: () =>
2542
+ manual(
2543
+ "How and to whom the SBOM is delivered cannot be judged from the BOM alone.",
2544
+ {
2545
+ mitigation:
2546
+ "Ship the BOM with the release artifacts, or publish it with --server-url to Dependency-Track or a TEA server.",
2547
+ },
2548
+ ),
2549
+ },
2550
+ {
2551
+ id: "CISA-MIN-018",
2552
+ name: "SBOM generated for every release",
2553
+ description:
2554
+ "CISA 2026 practice Frequency: each software version or update should have an associated SBOM, and a revised SBOM should be issued when new details about the components are discovered or an error is corrected.",
2555
+ standard: "CISA",
2556
+ standardRefs: ["CISA-2026-PRACTICE-FREQUENCY"],
2557
+ category: "compliance-cisa",
2558
+ severity: "info",
2559
+ scvsLevels: [],
2560
+ automatable: false,
2561
+ evaluate: () =>
2562
+ manual(
2563
+ "Whether every build and release gets its own SBOM cannot be judged from a single BOM.",
2564
+ {
2565
+ mitigation:
2566
+ "Run cdxgen in the release pipeline so each build produces its BOM.",
2567
+ },
2568
+ ),
2569
+ },
2570
+ ];
2571
+
2572
+ // ---------------------------------------------------------------------------
2573
+ // CISA and G7 minimum elements for an SBOM for AI
2574
+ // Joint guidance "Software Bill of Materials for AI - Minimum Elements" of the
2575
+ // G7 Cybersecurity Working Group (BSI, ACN, ANSSI, CSE, CISA, NCSC and NCO
2576
+ // with the European Commission), published on 12 May 2026. Its elements are
2577
+ // in addition to the general SBOM minimum elements, so the CISA_RULES above
2578
+ // cover the Metadata cluster and the component elements, and this catalog
2579
+ // adds the System Level Properties, Models and Datasets clusters. The
2580
+ // Infrastructure, Security Properties and KPI clusters need producer
2581
+ // documentation and are reported for manual review. Rules pass with a note
2582
+ // when the BOM holds no AI components, because the elements apply to AI
2583
+ // systems alone. docs/AI_BOM.md maps every element, including those cdxgen
2584
+ // does not emit.
2585
+ // ---------------------------------------------------------------------------
2586
+
2587
+ /**
2588
+ * Collect the machine-learning-model components of the BOM.
2589
+ *
2590
+ * @param {object} bomJson
2591
+ * @returns {Array<object>}
2592
+ */
2593
+ function aiModelComponents(bomJson) {
2594
+ return getAllComponents(bomJson).filter(
2595
+ (c) => c?.type === "machine-learning-model",
2596
+ );
2597
+ }
2598
+
2599
+ /**
2600
+ * Collect the dataset components: type "data" carrying a dataset entry or
2601
+ * dataset tag, as the Hugging Face parsers emit them.
2602
+ *
2603
+ * @param {object} bomJson
2604
+ * @returns {Array<object>}
2605
+ */
2606
+ function aiDatasetComponents(bomJson) {
2607
+ return getAllComponents(bomJson).filter(
2608
+ (c) =>
2609
+ c?.type === "data" &&
2610
+ ((Array.isArray(c.data) && c.data.some((d) => d?.type === "dataset")) ||
2611
+ (Array.isArray(c.tags) && c.tags.includes("dataset"))),
2612
+ );
2613
+ }
2614
+
2615
+ /**
2616
+ * A rule body for AI elements that only apply when the BOM describes AI
2617
+ * components; a BOM without them passes with a note instead of failing.
2618
+ *
2619
+ * @param {Array<object>} comps the AI components the element applies to
2620
+ * @param {string} kind human word such as "model" or "dataset"
2621
+ * @param {(comps: Array<object>) => object} body the real evaluation
2622
+ * @returns {object}
2623
+ */
2624
+ function aiElement(comps, kind, body) {
2625
+ if (!comps.length) {
2626
+ return pass(
2627
+ `The BOM holds no ${kind} components; the AI ${kind} elements do not apply.`,
2628
+ );
2629
+ }
2630
+ return body(comps);
2631
+ }
2632
+
2633
+ /**
2634
+ * A rule body for the System Level Properties elements. They describe the AI
2635
+ * system, so they apply only when the BOM holds model or dataset components;
2636
+ * any other BOM passes with a note.
2637
+ *
2638
+ * @param {object} bomJson
2639
+ * @param {() => object} body the real evaluation
2640
+ * @returns {object}
2641
+ */
2642
+ function aiSystemElement(bomJson, body) {
2643
+ if (
2644
+ !aiModelComponents(bomJson).length &&
2645
+ !aiDatasetComponents(bomJson).length
2646
+ ) {
2647
+ return pass(
2648
+ "The BOM holds no model or dataset components; the AI system elements do not apply.",
2649
+ );
2650
+ }
2651
+ return body();
2652
+ }
2653
+
2654
+ /**
2655
+ * The producer of the AI system the BOM describes. CycloneDX defines
2656
+ * metadata.supplier and the deprecated metadata.manufacture as the
2657
+ * organisation that supplied or manufactured the described component, and
2658
+ * the root component's own producer fields hold the same facts, so each of
2659
+ * them names the system producer. metadata.manufacturer is left out: from 1.6
2660
+ * it names the organisation that created the BOM, which is the SBOM author,
2661
+ * not the system producer.
2662
+ *
2663
+ * @param {object} bomJson
2664
+ * @returns {string | undefined}
2665
+ */
2666
+ function systemProducerName(bomJson) {
2667
+ const metadata = bomJson?.metadata;
2668
+ return (
2669
+ metadata?.supplier?.name ||
2670
+ metadata?.manufacture?.name ||
2671
+ componentProducerName(metadata?.component)
2672
+ );
2673
+ }
2674
+
2675
+ /**
2676
+ * Build a rule that fails for the AI components lacking an element.
2677
+ *
2678
+ * @param {object} spec rule fields plus `components`, `kind`, `has`,
2679
+ * `missingMessage`, `passMessage` and `mitigation`
2680
+ * @returns {object}
2681
+ */
2682
+ function aiComponentRule(spec) {
2683
+ const {
2684
+ components,
2685
+ kind,
2686
+ has,
2687
+ missingMessage,
2688
+ passMessage,
2689
+ mitigation,
2690
+ ...rule
2691
+ } = spec;
2692
+ return {
2693
+ standard: "CISA",
2694
+ category: "compliance-cisa-ai",
2695
+ scvsLevels: [],
2696
+ automatable: true,
2697
+ ...rule,
2698
+ evaluate(bomJson) {
2699
+ return aiElement(components(bomJson), kind, (comps) => {
2700
+ const missing = comps.filter((c) => !has(c));
2701
+ if (missing.length) {
2702
+ return fail(
2703
+ `${missing.length}/${comps.length} ${kind} component(s) ${missingMessage}.`,
2704
+ { mitigation, locations: cisaLocations(missing) },
2705
+ );
2706
+ }
2707
+ return pass(`All ${comps.length} ${kind}(s) ${passMessage}.`);
2708
+ });
2709
+ },
2710
+ };
2711
+ }
2712
+
2713
+ /** @type {Array<object>} */
2714
+ const CISA_AI_RULES = [
2715
+ {
2716
+ id: "CISA-AI-001",
2717
+ name: "System name and version",
2718
+ description:
2719
+ "CISA/G7 System Level Properties: the name the system producer assigns to the AI system, and the version that identifies a change from a previously identified version, or an explicit unknown.",
2720
+ standard: "CISA",
2721
+ standardRefs: ["CISA-2026-AI-SYSTEM-NAME", "CISA-2026-AI-SYSTEM-VERSION"],
2722
+ category: "compliance-cisa-ai",
2723
+ severity: "high",
2724
+ scvsLevels: [],
2725
+ automatable: true,
2726
+ evaluate(bomJson) {
2727
+ return aiSystemElement(bomJson, () => {
2728
+ const system = bomJson?.metadata?.component;
2729
+ if (!system?.name) {
2730
+ return fail("metadata.component is missing the AI system name.", {
2731
+ mitigation:
2732
+ "cdxgen records the project as metadata.component; --project-name and --project-version override it.",
2733
+ });
2734
+ }
2735
+ if (!system.version) {
2736
+ return fail(
2737
+ `metadata.component (${system.name}) carries no version or "unknown" marker.`,
2738
+ {
2739
+ mitigation:
2740
+ 'Pass --project-version, or set version to "unknown" when the system has none.',
2741
+ },
2742
+ );
2743
+ }
2744
+ return pass(`System ${system.name}@${system.version} recorded.`);
2745
+ });
2746
+ },
2747
+ },
2748
+ {
2749
+ id: "CISA-AI-002",
2750
+ name: "System producer",
2751
+ description:
2752
+ "CISA/G7 System Level Properties: the entity that creates, defines and identifies the AI system.",
2753
+ standard: "CISA",
2754
+ standardRefs: ["CISA-2026-AI-SYSTEM-PRODUCER"],
2755
+ category: "compliance-cisa-ai",
2756
+ severity: "medium",
2757
+ scvsLevels: [],
2758
+ automatable: true,
2759
+ evaluate(bomJson) {
2760
+ return aiSystemElement(bomJson, () => {
2761
+ const producer = systemProducerName(bomJson);
2762
+ if (!producer) {
2763
+ return fail("No system producer is declared for the AI system.", {
2764
+ mitigation:
2765
+ "cdxgen does not populate the system producer. Add the producing organisation to metadata.supplier.name (or metadata.component.manufacturer.name) after generation, for example by post-processing the BOM before signing it.",
2766
+ });
2767
+ }
2768
+ return pass(`System producer declared: ${producer}.`);
2769
+ });
2770
+ },
2771
+ },
2772
+ aiComponentRule({
2773
+ id: "CISA-AI-003",
2774
+ name: "Model name and version",
2775
+ description:
2776
+ "CISA/G7 Models cluster: the name the model producer assigns to the model, and the version identifying a change from a previously identified version, or an explicit unknown.",
2777
+ standardRefs: ["CISA-2026-AI-MODEL-NAME", "CISA-2026-AI-MODEL-VERSION"],
2778
+ severity: "high",
2779
+ components: aiModelComponents,
2780
+ kind: "model",
2781
+ has: (c) => c.name && c.version,
2782
+ missingMessage: 'lack a name or a version (or an "unknown" marker)',
2783
+ passMessage: "carry a name and version",
2784
+ mitigation:
2785
+ "Hugging Face revisions fill the version from the repository sha; local artifacts need the version recorded next to them.",
2786
+ }),
2787
+ aiComponentRule({
2788
+ id: "CISA-AI-004",
2789
+ name: "Model producer",
2790
+ description:
2791
+ "CISA/G7 Models cluster: the entity that creates, defines and identifies the AI model, the originator or manufacturer, such as the organisations behind its pre-training, post-training or fine-tuning.",
2792
+ standardRefs: ["CISA-2026-AI-MODEL-PRODUCER"],
2793
+ severity: "medium",
2794
+ components: aiModelComponents,
2795
+ kind: "model",
2796
+ has: (c) => componentProducerName(c),
2797
+ missingMessage: "declare no producer",
2798
+ passMessage: "name a producer or mark it unknown",
2799
+ mitigation:
2800
+ 'cdxgen records the creating organisation in manufacturer.name when a GGUF header, a creator\'s own Hugging Face organisation, a model name prefix or a creator-only API names it, and the publishing Hub namespace in supplier.name; models served by local runtimes name neither. Record the producer manually, or "unknown" where it is not known.',
2801
+ }),
2802
+ aiComponentRule({
2803
+ id: "CISA-AI-005",
2804
+ name: "Model identifiers",
2805
+ description:
2806
+ "CISA/G7 Models cluster: at least one machine-processable identifier per model, such as a purl, CPE, OmniBOR id or SWHID, so tools can correlate the model with databases.",
2807
+ standardRefs: ["CISA-2026-AI-MODEL-IDENTIFIER"],
2808
+ severity: "high",
2809
+ components: aiModelComponents,
2810
+ kind: "model",
2811
+ has: hasSoftwareIdentifier,
2812
+ missingMessage: "carry no purl, cpe, swid, omniborId or swhid identifier",
2813
+ passMessage: "carry a machine-processable identifier",
2814
+ mitigation:
2815
+ "cdxgen emits pkg:huggingface purls for Hub repositories and keeps dosai's purls; models seen only in source need one recorded.",
2816
+ }),
2817
+ aiComponentRule({
2818
+ id: "CISA-AI-006",
2819
+ name: "Model hashes with a named algorithm",
2820
+ description:
2821
+ "CISA/G7 Models cluster: a cryptographic hash of the weights, model file or other model artifacts, with the algorithm that produced it, so the artifact can be verified.",
2822
+ standardRefs: [
2823
+ "CISA-2026-AI-MODEL-HASH-VALUE",
2824
+ "CISA-2026-AI-MODEL-HASH-ALGORITHM",
2825
+ ],
2826
+ severity: "medium",
2827
+ components: aiModelComponents,
2828
+ kind: "model",
2829
+ has: hasApprovedHash,
2830
+ missingMessage: "lack a hexadecimal hash from an approved algorithm",
2831
+ passMessage: "carry a hash with a named algorithm",
2832
+ mitigation:
2833
+ "Local artifacts carry the SHA-256 dosai computes; remote repository references hold the revision instead, and a downloaded artifact needs its hash recorded.",
2834
+ }),
2835
+ aiComponentRule({
2836
+ id: "CISA-AI-007",
2837
+ name: "Model licenses",
2838
+ description:
2839
+ "CISA/G7 Models cluster: the license type of the AI model, machine-processable where possible, or a link to the licensing document.",
2840
+ standardRefs: ["CISA-2026-AI-MODEL-LICENSE"],
2841
+ severity: "medium",
2842
+ components: aiModelComponents,
2843
+ kind: "model",
2844
+ has: (c) => componentLicenseId(c),
2845
+ missingMessage: "declare no license",
2846
+ passMessage: "declare a license",
2847
+ mitigation:
2848
+ "Hugging Face model cards fill the license from the card tags; models without a card need the license or its URL recorded.",
2849
+ }),
2850
+ aiComponentRule({
2851
+ id: "CISA-AI-008",
2852
+ name: "Model description or model card",
2853
+ description:
2854
+ "CISA/G7 Models cluster: the model's capabilities, known limitations and lineage, which cdxgen records as a description, a CycloneDX modelCard and pedigree ancestors.",
2855
+ standardRefs: ["CISA-2026-AI-MODEL-DESCRIPTION"],
2856
+ severity: "low",
2857
+ components: aiModelComponents,
2858
+ kind: "model",
2859
+ has: (c) => c.description || c.modelCard,
2860
+ missingMessage: "carry neither a description nor a modelCard",
2861
+ passMessage: "carry a description or model card",
2862
+ mitigation:
2863
+ "Model cards from Hugging Face and GGUF metadata populate modelCard automatically; models seen only in source need a description.",
2864
+ }),
2865
+ aiComponentRule({
2866
+ id: "CISA-AI-009",
2867
+ name: "Dataset name and identifier",
2868
+ description:
2869
+ "CISA/G7 Datasets cluster: the dataset's name and the information that identifies it univocally, such as a URL, URI or repository identifier.",
2870
+ standardRefs: [
2871
+ "CISA-2026-AI-DATASET-NAME",
2872
+ "CISA-2026-AI-DATASET-IDENTIFIER",
2873
+ ],
2874
+ severity: "medium",
2875
+ components: aiDatasetComponents,
2876
+ kind: "dataset",
2877
+ has: (c) =>
2878
+ c.name &&
2879
+ (hasSoftwareIdentifier(c) ||
2880
+ (Array.isArray(c.data) && c.data.some((d) => d?.contents?.url)) ||
2881
+ (Array.isArray(c.externalReferences) &&
2882
+ c.externalReferences.some((ref) => ref?.url))),
2883
+ missingMessage: "lack a name or an identifying URL or purl",
2884
+ passMessage: "carry a name and identifier",
2885
+ mitigation:
2886
+ "Hugging Face datasets carry pkg:huggingface purls with the datasets repository URL; private datasets need their URL recorded.",
2887
+ }),
2888
+ aiComponentRule({
2889
+ id: "CISA-AI-010",
2890
+ name: "Dataset licenses",
2891
+ description:
2892
+ "CISA/G7 Datasets cluster: the license type of the dataset, or a link to the licensing document.",
2893
+ standardRefs: ["CISA-2026-AI-DATASET-LICENSE"],
2894
+ severity: "medium",
2895
+ components: aiDatasetComponents,
2896
+ kind: "dataset",
2897
+ has: (c) => componentLicenseId(c),
2898
+ missingMessage: "declare no license",
2899
+ passMessage: "declare a license",
2900
+ mitigation:
2901
+ "Hugging Face dataset cards fill the license from the card tags; datasets without a card need the license or its URL recorded.",
2902
+ }),
2903
+ {
2904
+ id: "CISA-AI-011",
2905
+ name: "Descriptive AI elements",
2906
+ description:
2907
+ "CISA/G7 elements that need the producer's documentation: system timestamp, data flow, data usage, input/output properties and intended application area; model timestamp and training properties; dataset content, provenance, statistical properties, sensitivity and dependency relationship; infrastructure hardware; the Security Properties cluster; and the KPI cluster.",
2908
+ standard: "CISA",
2909
+ standardRefs: [
2910
+ "CISA-2026-AI-SYSTEM-TIMESTAMP",
2911
+ "CISA-2026-AI-SYSTEM-DATA-FLOW",
2912
+ "CISA-2026-AI-SYSTEM-DATA-USAGE",
2913
+ "CISA-2026-AI-SYSTEM-INPUT-OUTPUT-PROPERTIES",
2914
+ "CISA-2026-AI-SYSTEM-INTENDED-APPLICATION-AREA",
2915
+ "CISA-2026-AI-MODEL-TIMESTAMP",
2916
+ "CISA-2026-AI-MODEL-TRAINING-PROPERTIES",
2917
+ "CISA-2026-AI-DATASET-CONTENT",
2918
+ "CISA-2026-AI-DATASET-PROVENANCE",
2919
+ "CISA-2026-AI-DATASET-STATISTICAL-PROPERTIES",
2920
+ "CISA-2026-AI-DATASET-SENSITIVITY",
2921
+ "CISA-2026-AI-DATASET-DEPENDENCY-RELATIONSHIP",
2922
+ "CISA-2026-AI-INFRASTRUCTURE-HARDWARE",
2923
+ "CISA-2026-AI-SECURITY-CONTROLS",
2924
+ "CISA-2026-AI-SECURITY-COMPLIANCE",
2925
+ "CISA-2026-AI-CYBERSECURITY-POLICY-INFORMATION",
2926
+ "CISA-2026-AI-VULNERABILITY-REFERENCING",
2927
+ "CISA-2026-AI-SECURITY-METRICS",
2928
+ "CISA-2026-AI-OPERATIONAL-PERFORMANCE-KPIS",
2929
+ ],
2930
+ category: "compliance-cisa-ai",
2931
+ severity: "info",
2932
+ scvsLevels: [],
2933
+ automatable: false,
2934
+ evaluate: (bomJson) =>
2935
+ aiSystemElement(bomJson, () =>
2936
+ manual(
2937
+ "These elements need producer documentation; a BOM can carry parts of them (model card use cases and considerations, service endpoints, data classification, pedigree) but completeness cannot be judged from the BOM alone.",
2938
+ {
2939
+ mitigation:
2940
+ "Attach the producer's system, model and dataset cards, link the HBOM for the hardware, and keep the security controls, compliance, security.txt, vulnerability and metrics documentation with the release.",
2941
+ },
2942
+ ),
2943
+ ),
2944
+ },
2945
+ aiComponentRule({
2946
+ id: "CISA-AI-012",
2947
+ name: "Model properties",
2948
+ description:
2949
+ "CISA/G7 Models cluster: the characteristics of the model, such as its architecture family and network architecture.",
2950
+ standardRefs: ["CISA-2026-AI-MODEL-PROPERTIES"],
2951
+ severity: "low",
2952
+ components: aiModelComponents,
2953
+ kind: "model",
2954
+ has: (c) =>
2955
+ c.modelCard?.modelParameters?.architectureFamily ||
2956
+ c.modelCard?.modelParameters?.modelArchitecture,
2957
+ missingMessage:
2958
+ "record no modelCard.modelParameters.architectureFamily or modelArchitecture",
2959
+ passMessage: "record their architecture",
2960
+ mitigation:
2961
+ "cdxgen reads the architecture from a Hugging Face config.json or the GGUF header; record it in modelCard.modelParameters for other models.",
2962
+ }),
2963
+ aiComponentRule({
2964
+ id: "CISA-AI-013",
2965
+ name: "Model input-output properties",
2966
+ description:
2967
+ "CISA/G7 Models cluster: the type and modality of the data the model takes in and produces.",
2968
+ standardRefs: ["CISA-2026-AI-MODEL-INPUT-OUTPUT-PROPERTIES"],
2969
+ severity: "low",
2970
+ components: aiModelComponents,
2971
+ kind: "model",
2972
+ has: (c) =>
2973
+ c.modelCard?.modelParameters?.inputs?.length &&
2974
+ c.modelCard?.modelParameters?.outputs?.length,
2975
+ missingMessage: "lack modelCard.modelParameters.inputs or outputs",
2976
+ passMessage: "record their inputs and outputs",
2977
+ mitigation:
2978
+ "cdxgen derives the inputs and outputs from the Hugging Face pipeline tag or the GGUF tokenizer; record them in modelCard.modelParameters for other models.",
2979
+ }),
2980
+ aiComponentRule({
2981
+ id: "CISA-AI-014",
2982
+ name: "Model external references",
2983
+ description:
2984
+ "CISA/G7 Models cluster: links to external references related to the model, such as the model or system card, public documentation or a research paper.",
2985
+ standardRefs: ["CISA-2026-AI-MODEL-EXTERNAL-REFERENCES"],
2986
+ severity: "low",
2987
+ components: aiModelComponents,
2988
+ kind: "model",
2989
+ has: (c) =>
2990
+ Array.isArray(c.externalReferences) &&
2991
+ c.externalReferences.some((ref) => ref?.url),
2992
+ missingMessage: "carry no externalReferences",
2993
+ passMessage: "carry external references",
2994
+ mitigation:
2995
+ "Hugging Face models carry their repository and paper links; record the model card or documentation URL for other models.",
2996
+ }),
2997
+ aiComponentRule({
2998
+ id: "CISA-AI-015",
2999
+ name: "Dataset description",
3000
+ description:
3001
+ "CISA/G7 Datasets cluster: a description of the dataset, such as its intended use in pre-training, fine-tuning or evaluation, and whether it is private or public.",
3002
+ standardRefs: ["CISA-2026-AI-DATASET-DESCRIPTION"],
3003
+ severity: "low",
3004
+ components: aiDatasetComponents,
3005
+ kind: "dataset",
3006
+ has: (c) =>
3007
+ c.description ||
3008
+ (Array.isArray(c.data) && c.data.some((d) => d?.description)),
3009
+ missingMessage: "carry no description",
3010
+ passMessage: "carry a description",
3011
+ mitigation:
3012
+ "Hugging Face dataset cards fill the description; record one for private datasets.",
3013
+ }),
3014
+ aiComponentRule({
3015
+ id: "CISA-AI-016",
3016
+ name: "Dataset hash",
3017
+ description:
3018
+ "CISA/G7 Datasets cluster: the cryptographic hash of the dataset file.",
3019
+ standardRefs: ["CISA-2026-AI-DATASET-HASH"],
3020
+ severity: "low",
3021
+ components: aiDatasetComponents,
3022
+ kind: "dataset",
3023
+ has: hasApprovedHash,
3024
+ missingMessage: "lack a hexadecimal hash from an approved algorithm",
3025
+ passMessage: "carry a hash with a named algorithm",
3026
+ mitigation:
3027
+ "cdxgen references Hub datasets by repository and does not hash their files; record the SHA-256 of the dataset files that were used.",
3028
+ }),
3029
+ ];
3030
+
1637
3031
  // ---------------------------------------------------------------------------
1638
3032
  // Public API
1639
3033
  // ---------------------------------------------------------------------------
1640
3034
 
1641
3035
  /**
1642
- * Returns the full catalog of compliance rules (SCVS + CRA).
3036
+ * Returns the full catalog of compliance rules (SCVS + CRA + CISA).
1643
3037
  *
1644
3038
  * @returns {Array<object>}
1645
3039
  */
1646
3040
  export function getAllComplianceRules() {
1647
- return [...SCVS_RULES, ...CRA_RULES];
3041
+ return [...SCVS_RULES, ...CRA_RULES, ...CISA_RULES, ...CISA_AI_RULES];
1648
3042
  }
1649
3043
 
1650
3044
  /**
@@ -1665,6 +3059,24 @@ export function getCraRules() {
1665
3059
  return [...CRA_RULES];
1666
3060
  }
1667
3061
 
3062
+ /**
3063
+ * Returns only CISA 2026 minimum elements rules.
3064
+ *
3065
+ * @returns {Array<object>}
3066
+ */
3067
+ export function getCisaRules() {
3068
+ return [...CISA_RULES];
3069
+ }
3070
+
3071
+ /**
3072
+ * Returns only the CISA and G7 AI SBOM element rules.
3073
+ *
3074
+ * @returns {Array<object>}
3075
+ */
3076
+ export function getCisaAiRules() {
3077
+ return [...CISA_AI_RULES];
3078
+ }
3079
+
1668
3080
  /**
1669
3081
  * Test-only export exposing internal helper functions for unit tests.
1670
3082
  * Double-underscore prefixed to discourage external use.