@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
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Hardware components at 2.0 (cdxgen/cdxgen#4755). What each `cdx:hbom:*`
3
+ * property means in the 2.0 draft is owned by @cdxgen/cdx-hbom, whose
4
+ * optional `./cdx20` export provides `toCdx20HardwareComponent(component,
5
+ * {partyRefId, includeSensitiveIdentifiers, identifierPolicy})`. The caller
6
+ * loads it (lib/inventory/hbomLoader.js: hardwareRenderContext) and passes
7
+ * it, with those options, to renderCycloneDx20 in the render context, so the
8
+ * render stays a pure function of its arguments.
9
+ *
10
+ * The renderer converts the component as usual and then takes only these
11
+ * members from the mapper's result, each checked against the pinned schema:
12
+ *
13
+ * - `identifiers`: only groups asserted by the party the renderer passed
14
+ * (the one its own identifiers use, which it declares when referenced).
15
+ * Their identities are appended to that party's group, or added as a new
16
+ * group, so a purl identity is never replaced. Only identities with a
17
+ * scheme from the identityScheme enum (or a custom `{name}`) and a
18
+ * non-empty value are taken.
19
+ * - `classification` (#/$defs/cyclonedx-physical-2.0/$defs/classification):
20
+ * `category`, `subcategory`, and `codes` that are taxonomyCode objects
21
+ * (`{standard, value}`); a bare string code is dropped and counted.
22
+ * - a manufacturer party, when the generic conversion produced none.
23
+ *
24
+ * Every `cdx:hbom:*` property stays: the hbom audit rules read them.
25
+ */
26
+
27
+ import { thoughtLog } from "../core/logger.js";
28
+ import {
29
+ clone,
30
+ countDrop,
31
+ isNonEmptyString,
32
+ isObjectRecord,
33
+ uniqueByValue,
34
+ } from "./cyclonedx20Common.js";
35
+ import { identifierPartyRefId } from "./cyclonedx20RenderEvidence.js";
36
+ import { IDENTITY_SCHEME_ENUM } from "./cyclonedx20Tables.js";
37
+
38
+ const TAXONOMY_CODE_FIELDS = Object.freeze(["description", "url"]);
39
+
40
+ /** True when the 1.x component carries at least one cdx:hbom:* property. */
41
+ export function isHardwareComponent(component) {
42
+ return (
43
+ Array.isArray(component?.properties) ? component.properties : []
44
+ ).some(
45
+ (property) =>
46
+ typeof property?.name === "string" &&
47
+ property.name.startsWith("cdx:hbom:"),
48
+ );
49
+ }
50
+
51
+ function isValidIdentity(identity) {
52
+ if (!isObjectRecord(identity) || !isNonEmptyString(identity.value)) {
53
+ return false;
54
+ }
55
+ const { scheme } = identity;
56
+ return (
57
+ (typeof scheme === "string" && IDENTITY_SCHEME_ENUM.has(scheme)) ||
58
+ (isObjectRecord(scheme) && isNonEmptyString(scheme.name))
59
+ );
60
+ }
61
+
62
+ function appendIdentifiers(converted, groups, partyRefId, stats) {
63
+ for (const group of Array.isArray(groups) ? groups : []) {
64
+ if (!isObjectRecord(group) || group.party !== partyRefId) {
65
+ // Any other party would have no declaration in this document.
66
+ countDrop(stats, "hardware identifier (undeclared party)");
67
+ continue;
68
+ }
69
+ const identities = (Array.isArray(group.identities) ? group.identities : [])
70
+ .filter((identity) => {
71
+ const valid = isValidIdentity(identity);
72
+ if (!valid) {
73
+ countDrop(stats, "hardware identity (not a 2.0 identity)");
74
+ }
75
+ return valid;
76
+ })
77
+ .map((identity) => ({
78
+ scheme: clone(identity.scheme),
79
+ value: identity.value,
80
+ }));
81
+ if (!identities.length) {
82
+ continue;
83
+ }
84
+ converted.identifiers = Array.isArray(converted.identifiers)
85
+ ? converted.identifiers
86
+ : [];
87
+ const existing = converted.identifiers.find(
88
+ (entry) => entry.party === group.party,
89
+ );
90
+ if (existing) {
91
+ existing.identities = uniqueByValue([
92
+ ...existing.identities,
93
+ ...identities,
94
+ ]);
95
+ } else {
96
+ converted.identifiers.push({ party: group.party, identities });
97
+ }
98
+ }
99
+ }
100
+
101
+ function taxonomyCode(code, stats) {
102
+ if (
103
+ !isObjectRecord(code) ||
104
+ !isNonEmptyString(code.standard) ||
105
+ !isNonEmptyString(code.value)
106
+ ) {
107
+ countDrop(stats, "hardware classification code (not a taxonomyCode)");
108
+ return undefined;
109
+ }
110
+ const converted = { standard: code.standard, value: code.value };
111
+ for (const field of TAXONOMY_CODE_FIELDS) {
112
+ if (typeof code[field] === "string") {
113
+ converted[field] = code[field];
114
+ }
115
+ }
116
+ return converted;
117
+ }
118
+
119
+ function setClassification(converted, classification, stats) {
120
+ if (!isObjectRecord(classification) || converted.classification) {
121
+ return;
122
+ }
123
+ const result = {};
124
+ if (isNonEmptyString(classification.category)) {
125
+ result.category = classification.category;
126
+ }
127
+ if (typeof classification.subcategory === "string") {
128
+ result.subcategory = classification.subcategory;
129
+ }
130
+ const codes = uniqueByValue(
131
+ (Array.isArray(classification.codes) ? classification.codes : [])
132
+ .map((code) => taxonomyCode(code, stats))
133
+ .filter(Boolean),
134
+ );
135
+ if (codes.length) {
136
+ result.codes = codes;
137
+ }
138
+ if (Object.keys(result).length) {
139
+ converted.classification = result;
140
+ }
141
+ }
142
+
143
+ function addManufacturerParty(converted, parties) {
144
+ const hasManufacturer = (converted.parties || []).some((party) =>
145
+ (party?.roles || []).some((role) => role?.role === "manufacturer"),
146
+ );
147
+ if (hasManufacturer) {
148
+ return;
149
+ }
150
+ const mapped = (Array.isArray(parties) ? parties : []).find(
151
+ (party) =>
152
+ (party?.roles || []).some((role) => role?.role === "manufacturer") &&
153
+ isNonEmptyString(party?.organization?.name),
154
+ );
155
+ if (mapped) {
156
+ converted.parties = [
157
+ ...(converted.parties || []),
158
+ {
159
+ roles: [{ role: "manufacturer" }],
160
+ organization: { name: mapped.organization.name },
161
+ },
162
+ ];
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Add the hardware mapper's 2.0 members to a converted hardware component.
168
+ * Without a mapper, or for a component without cdx:hbom:* properties, the
169
+ * converted component is returned unchanged. The mapper gets a clone of the
170
+ * 1.x component, the asserting party refId the generic identifiers use (so
171
+ * its identifiers resolve to the party the renderer declares) and the
172
+ * identifier options of the render context (`stats.hardwareOptions`).
173
+ *
174
+ * @param {object} component The 1.x component
175
+ * @param {object} converted Its generic 2.0 conversion, extended in place
176
+ * @param {object} stats Render counters with the optional `hardwareMapper`
177
+ * @returns {object} The converted component
178
+ */
179
+ export function applyHardwareMapping(component, converted, stats) {
180
+ const mapper = stats.hardwareMapper;
181
+ if (typeof mapper !== "function" || !isHardwareComponent(component)) {
182
+ return converted;
183
+ }
184
+ const partyRefId = identifierPartyRefId(stats.claims);
185
+ let mapped;
186
+ try {
187
+ mapped = mapper(clone(component), {
188
+ ...stats.hardwareOptions,
189
+ partyRefId,
190
+ });
191
+ } catch (error) {
192
+ countDrop(stats, "hardware mapping (mapper failed)");
193
+ thoughtLog(`CycloneDX 2.0 hardware mapping failed: ${error?.message}`);
194
+ return converted;
195
+ }
196
+ if (!isObjectRecord(mapped)) {
197
+ return converted;
198
+ }
199
+ appendIdentifiers(converted, mapped.identifiers, partyRefId, stats);
200
+ setClassification(converted, mapped.classification, stats);
201
+ addManufacturerParty(converted, mapped.parties);
202
+ return converted;
203
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Render-side conversion of 1.x patents, patent families and patent
3
+ * assertions (#/$defs/cyclonedx-patent-2.0). The members are the same; the
4
+ * 1.x `bom-ref` becomes `refId`, and organizations and contacts take the
5
+ * 2.0 common shapes.
6
+ */
7
+
8
+ import {
9
+ clone,
10
+ copyDefined,
11
+ countDrop,
12
+ countUnmapped,
13
+ isNonEmptyString,
14
+ isObjectRecord,
15
+ } from "./cyclonedx20Common.js";
16
+ import { toCommonContact, toCommonEntity } from "./cyclonedx20Parties.js";
17
+
18
+ const PATENT_FIELDS = [
19
+ "patentNumber",
20
+ "applicationNumber",
21
+ "jurisdiction",
22
+ "priorityApplication",
23
+ "publicationNumber",
24
+ "abstract",
25
+ "filingDate",
26
+ "grantDate",
27
+ "patentExpirationDate",
28
+ "patentLegalStatus",
29
+ "externalReferences",
30
+ ];
31
+ const PATENT_HANDLED = new Set([...PATENT_FIELDS, "bom-ref", "patentAssignee"]);
32
+
33
+ const PATENT_FAMILY_FIELDS = [
34
+ "familyId",
35
+ "priorityApplication",
36
+ "members",
37
+ "externalReferences",
38
+ ];
39
+ const PATENT_FAMILY_HANDLED = new Set([...PATENT_FAMILY_FIELDS, "bom-ref"]);
40
+
41
+ const ASSERTION_FIELDS = ["assertionType", "patentRefs", "notes"];
42
+ const ASSERTION_HANDLED = new Set([...ASSERTION_FIELDS, "bom-ref", "asserter"]);
43
+
44
+ /** Start a converted object with the 1.x `bom-ref` as its `refId`. */
45
+ function withRefId(source) {
46
+ return isNonEmptyString(source["bom-ref"])
47
+ ? { refId: source["bom-ref"] }
48
+ : {};
49
+ }
50
+
51
+ /**
52
+ * A 1.x organizationalEntity or organizationalContact as its 2.0 common
53
+ * shape, for the members whose schema is a oneOf of the two. Only a contact
54
+ * has `email` or `phone`, only an organization `url`, `address` or
55
+ * `contact`. With neither (a bare name) the value matches both branches of
56
+ * the oneOf and so validates as neither; it is counted under `label` and
57
+ * undefined is returned.
58
+ */
59
+ function toCommonEntityOrContact(value, label, stats) {
60
+ if (!isObjectRecord(value)) {
61
+ countDrop(stats, `${label} (not an object)`);
62
+ return undefined;
63
+ }
64
+ const isContact =
65
+ (value.email !== undefined || value.phone !== undefined) &&
66
+ value.url === undefined &&
67
+ value.address === undefined &&
68
+ value.contact === undefined;
69
+ const converted = isContact
70
+ ? toCommonContact(value, stats)
71
+ : toCommonEntity(value, stats);
72
+ if (
73
+ !converted ||
74
+ Object.keys(converted).every((key) => key === "refId" || key === "name")
75
+ ) {
76
+ countDrop(stats, `${label} (neither an organization nor a contact)`);
77
+ return undefined;
78
+ }
79
+ return converted;
80
+ }
81
+
82
+ /**
83
+ * Convert 1.x definitions.patents: patents and patent families.
84
+ *
85
+ * @param {object[]} patents 1.x patents and patent families
86
+ * @param {object} stats Render counters
87
+ * @returns {object[]|undefined} 2.0 patents
88
+ */
89
+ export function convertPatents(patents, stats) {
90
+ if (!Array.isArray(patents)) {
91
+ if (patents !== undefined) {
92
+ countDrop(stats, "definitions.patents (not a list)");
93
+ }
94
+ return undefined;
95
+ }
96
+ const converted = [];
97
+ for (const patent of patents) {
98
+ if (!isObjectRecord(patent)) {
99
+ countDrop(stats, "definitions.patents (not an object)");
100
+ continue;
101
+ }
102
+ const out = withRefId(patent);
103
+ if (patent.familyId !== undefined) {
104
+ copyDefined(out, patent, PATENT_FAMILY_FIELDS);
105
+ countUnmapped(
106
+ stats,
107
+ "definitions.patents",
108
+ patent,
109
+ PATENT_FAMILY_HANDLED,
110
+ );
111
+ } else {
112
+ copyDefined(out, patent, PATENT_FIELDS);
113
+ if (Array.isArray(patent.patentAssignee)) {
114
+ const assignees = patent.patentAssignee
115
+ .map((assignee) =>
116
+ toCommonEntityOrContact(
117
+ assignee,
118
+ "definitions.patents.patentAssignee",
119
+ stats,
120
+ ),
121
+ )
122
+ .filter(Boolean);
123
+ if (assignees.length) {
124
+ out.patentAssignee = assignees;
125
+ }
126
+ }
127
+ countUnmapped(stats, "definitions.patents", patent, PATENT_HANDLED);
128
+ }
129
+ converted.push(out);
130
+ }
131
+ return converted.length ? converted : undefined;
132
+ }
133
+
134
+ /**
135
+ * Convert 1.x patentAssertions
136
+ * (#/$defs/cyclonedx-patent-2.0/$defs/patentAssertions): the asserter is an
137
+ * organization, a contact or a refLinkType string.
138
+ *
139
+ * @param {object[]} assertions 1.x patent assertions
140
+ * @param {object} stats Render counters
141
+ * @returns {object[]|undefined} 2.0 patent assertions, or undefined when
142
+ * none is left
143
+ */
144
+ export function convertPatentAssertions(assertions, stats) {
145
+ if (!Array.isArray(assertions)) {
146
+ return clone(assertions);
147
+ }
148
+ const converted = [];
149
+ for (const assertion of assertions) {
150
+ if (!isObjectRecord(assertion)) {
151
+ countDrop(stats, "patentAssertions (not an object)");
152
+ continue;
153
+ }
154
+ // `asserter` is required.
155
+ const asserter =
156
+ typeof assertion.asserter === "string"
157
+ ? assertion.asserter
158
+ : toCommonEntityOrContact(
159
+ assertion.asserter,
160
+ "patentAssertions.asserter",
161
+ stats,
162
+ );
163
+ if (!asserter) {
164
+ countDrop(stats, "patentAssertions (no asserter)");
165
+ continue;
166
+ }
167
+ const out = withRefId(assertion);
168
+ copyDefined(out, assertion, ASSERTION_FIELDS);
169
+ out.asserter = asserter;
170
+ countUnmapped(stats, "patentAssertions", assertion, ASSERTION_HANDLED);
171
+ converted.push(out);
172
+ }
173
+ return converted.length ? converted : undefined;
174
+ }
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Reference integrity of a rendered CycloneDX 2.0 document: every refId is
3
+ * unique (the 2.0 refIdType rule "Every `refId` shall be unique within the
4
+ * document", which JSON Schema cannot check), and dependency and
5
+ * vulnerability references resolve to a declared refId or a BOM-Link.
6
+ */
7
+
8
+ import { isDeepStrictEqual } from "node:util";
9
+
10
+ import {
11
+ clone,
12
+ countDrop,
13
+ countRewrite,
14
+ isBomLink,
15
+ isObjectRecord,
16
+ } from "./cyclonedx20Common.js";
17
+ import { COMPONENT_ENTRY_ARRAY_KEYS } from "./cyclonedx20Tables.js";
18
+
19
+ // Returned by the walk for a declaration-only entry that repeats an earlier
20
+ // declaration; the enclosing array leaves it out.
21
+ const REMOVE = Symbol("remove");
22
+
23
+ // Slots outside component and party lists: a duplicate there has no
24
+ // reference form.
25
+ const PLAIN_SLOT = Object.freeze({});
26
+
27
+ /** How many times each refId string is declared under the roots. */
28
+ function countRefIds(roots) {
29
+ const counts = new Map();
30
+ const visit = (node) => {
31
+ if (Array.isArray(node)) {
32
+ node.forEach(visit);
33
+ return;
34
+ }
35
+ if (!isObjectRecord(node)) {
36
+ return;
37
+ }
38
+ if (typeof node.refId === "string") {
39
+ counts.set(node.refId, (counts.get(node.refId) || 0) + 1);
40
+ }
41
+ for (const value of Object.values(node)) {
42
+ visit(value);
43
+ }
44
+ };
45
+ roots.forEach(({ value }) => {
46
+ visit(value);
47
+ });
48
+ return counts;
49
+ }
50
+
51
+ /**
52
+ * True when a reference to `earlier` loses nothing of `later`: every member
53
+ * of `later` except its refId has a deep-equal counterpart in `earlier`.
54
+ */
55
+ function isCoveredBy(later, earlier) {
56
+ if (!isObjectRecord(earlier)) {
57
+ return false;
58
+ }
59
+ for (const [key, value] of Object.entries(later)) {
60
+ if (key !== "refId" && !isDeepStrictEqual(value, earlier[key])) {
61
+ return false;
62
+ }
63
+ }
64
+ return true;
65
+ }
66
+
67
+ /** `${refId}-dup-<n>` for the smallest n not already taken. */
68
+ function uniqueRefId(refId, taken) {
69
+ let index = 1;
70
+ while (taken.has(`${refId}-dup-${index}`)) {
71
+ index++;
72
+ }
73
+ const fresh = `${refId}-dup-${index}`;
74
+ taken.add(fresh);
75
+ return fresh;
76
+ }
77
+
78
+ /**
79
+ * The slot a member value sits in. Component entry lists, the inventory
80
+ * subject and an annotator component accept a componentReference
81
+ * (#/$defs/cyclonedx-component-2.0/$defs/componentEntry), party lists a
82
+ * partyReference (#/$defs/cyclonedx-party-2.0/$defs/partyChoice). The
83
+ * definitions registry lists components and parties but accepts no
84
+ * reference form (#/$defs/cyclonedx-definition-2.0/$defs/definitions).
85
+ */
86
+ function memberSlot(key, value, ownKey, inDefinitions) {
87
+ const isList = Array.isArray(value);
88
+ if (inDefinitions && isList && (key === "components" || key === "parties")) {
89
+ return {
90
+ kind: key === "components" ? "component" : "party",
91
+ declarationOnly: true,
92
+ };
93
+ }
94
+ if (
95
+ (COMPONENT_ENTRY_ARRAY_KEYS.has(key) && isList) ||
96
+ key === "subject" ||
97
+ (key === "component" && ownKey === "annotator")
98
+ ) {
99
+ return { kind: "component", reference: true };
100
+ }
101
+ if (key === "parties" && isList) {
102
+ return { kind: "party", reference: true };
103
+ }
104
+ return PLAIN_SLOT;
105
+ }
106
+
107
+ /**
108
+ * Make every refId unique. The roots are walked in order, so the earliest
109
+ * root keeps a repeated refId; pass the inventories before metadata so the
110
+ * subject of a self-scan keeps its members when it repeats the refId of the
111
+ * tool. A later declaration with an already declared refId becomes:
112
+ *
113
+ * - a componentReference or partyReference `{ref}` when it sits in a slot
114
+ * that accepts one and an earlier declaration of the same kind already
115
+ * holds every member it has (nothing is lost);
116
+ * - nothing, in the definitions registry, under the same condition (counted
117
+ * as already declared), and also when an inventory declares the refId
118
+ * with other members (the members only the registry entry had are
119
+ * counted as dropped);
120
+ * - unchanged, when neither it nor an earlier declaration sits in a
121
+ * component or party slot (counted);
122
+ * - otherwise the same object under a new refId `<refId>-dup-<n>`, so no data
123
+ * is lost and references to the refId keep resolving to the first
124
+ * declaration.
125
+ *
126
+ * @param {Array<{section: string, value: *}>} roots Rendered sections, the
127
+ * definitions root marked with `section: "definitions"`
128
+ * @param {object} stats Render counters
129
+ */
130
+ export function dedupeRefIds(roots, stats) {
131
+ const counts = countRefIds(roots);
132
+ const taken = new Set(counts.keys());
133
+ const seen = new Map();
134
+ const declare = (node, slot, section) => {
135
+ const refId = node.refId;
136
+ const variants = seen.get(refId);
137
+ if (!variants) {
138
+ seen.set(refId, [
139
+ {
140
+ kind: slot.kind,
141
+ refId,
142
+ section,
143
+ snapshot: counts.get(refId) > 1 ? clone(node) : undefined,
144
+ },
145
+ ]);
146
+ return node;
147
+ }
148
+ const match = slot.kind
149
+ ? variants.find(
150
+ (variant) =>
151
+ variant.kind === slot.kind && isCoveredBy(node, variant.snapshot),
152
+ )
153
+ : undefined;
154
+ if (match && slot.declarationOnly) {
155
+ countRewrite(
156
+ stats,
157
+ `definitions ${slot.kind} (already declared in ${match.section === "inventories" ? "inventory" : match.section})`,
158
+ );
159
+ return REMOVE;
160
+ }
161
+ // A formula component the inventory declares too: 2.0 records no formula
162
+ // membership, so a second definition would only repeat the component
163
+ // under another refId. The inventory declaration stays, references keep
164
+ // resolving to it, and the members only this copy had are counted.
165
+ const inventoryVariant =
166
+ slot.declarationOnly &&
167
+ variants.find(
168
+ (variant) =>
169
+ variant.kind === slot.kind && variant.section === "inventories",
170
+ );
171
+ if (inventoryVariant) {
172
+ countDrop(
173
+ stats,
174
+ `definitions ${slot.kind} (already declared in inventory; members it adds dropped)`,
175
+ );
176
+ return REMOVE;
177
+ }
178
+ if (match && slot.reference) {
179
+ countRewrite(stats, "duplicate refId (rendered as a reference)");
180
+ return { ref: match.refId };
181
+ }
182
+ if (!slot.kind && !variants.some((variant) => variant.kind)) {
183
+ // Neither declaration sits where a componentReference or
184
+ // partyReference could name it (a formula and its workflow, say), so
185
+ // no reference can resolve to the wrong one. A document read from 2.0
186
+ // keeps its refIds as written; the repeat is counted.
187
+ countRewrite(stats, "duplicate refId (kept: no reference form)");
188
+ return node;
189
+ }
190
+ node.refId = uniqueRefId(refId, taken);
191
+ countRewrite(stats, "duplicate refId (kept under a new refId)");
192
+ variants.push({
193
+ kind: slot.kind,
194
+ refId: node.refId,
195
+ section,
196
+ snapshot: clone(node),
197
+ });
198
+ return node;
199
+ };
200
+ const visit = (node, slot, ownKey, section, inDefinitions) => {
201
+ if (Array.isArray(node)) {
202
+ let removed = false;
203
+ for (let index = 0; index < node.length; index++) {
204
+ node[index] = visit(node[index], slot, ownKey, section, false);
205
+ removed ||= node[index] === REMOVE;
206
+ }
207
+ if (removed) {
208
+ const kept = node.filter((entry) => entry !== REMOVE);
209
+ node.length = 0;
210
+ node.push(...kept);
211
+ }
212
+ return node;
213
+ }
214
+ if (!isObjectRecord(node)) {
215
+ return node;
216
+ }
217
+ if (typeof node.refId === "string") {
218
+ const declared = declare(node, slot, section);
219
+ if (declared !== node) {
220
+ return declared;
221
+ }
222
+ }
223
+ for (const [key, value] of Object.entries(node)) {
224
+ node[key] = visit(
225
+ value,
226
+ memberSlot(key, value, ownKey, inDefinitions),
227
+ key,
228
+ section,
229
+ false,
230
+ );
231
+ }
232
+ return node;
233
+ };
234
+ for (const { section, value } of roots) {
235
+ visit(value, PLAIN_SLOT, undefined, section, section === "definitions");
236
+ }
237
+ }
238
+
239
+ const resolves = (ref, declared) =>
240
+ typeof ref === "string" && (declared.has(ref) || isBomLink(ref));
241
+
242
+ /**
243
+ * Keep only dependency references that resolve: an entry whose `ref` names
244
+ * nothing declared is dropped, and so is each such `dependsOn` and
245
+ * `provides` member. An emptied `dependsOn` stays, since an empty list says
246
+ * the component has no dependencies.
247
+ *
248
+ * @param {object[]|undefined} dependencies Rendered dependencies
249
+ * @param {Set<string>} declared Every declared refId
250
+ * @param {object} stats Render counters
251
+ * @returns {object[]|undefined} The dependencies left, or undefined
252
+ */
253
+ export function resolveDependencyRefs(dependencies, declared, stats) {
254
+ if (!Array.isArray(dependencies)) {
255
+ return undefined;
256
+ }
257
+ const kept = [];
258
+ for (const dependency of dependencies) {
259
+ if (!resolves(dependency.ref, declared)) {
260
+ countDrop(stats, "dependency (undeclared ref)");
261
+ continue;
262
+ }
263
+ for (const key of ["dependsOn", "provides"]) {
264
+ if (!Array.isArray(dependency[key])) {
265
+ continue;
266
+ }
267
+ const refs = dependency[key].filter((ref) => resolves(ref, declared));
268
+ if (refs.length !== dependency[key].length) {
269
+ countDrop(
270
+ stats,
271
+ `dependency.${key} (undeclared ref)`,
272
+ dependency[key].length - refs.length,
273
+ );
274
+ }
275
+ dependency[key] = refs;
276
+ }
277
+ kept.push(dependency);
278
+ }
279
+ return kept.length ? kept : undefined;
280
+ }
281
+
282
+ /**
283
+ * Drop vulnerability `affects` entries whose `ref` resolves to nothing, and
284
+ * an `affects` list left empty.
285
+ *
286
+ * @param {object[]|undefined} vulnerabilities Rendered vulnerabilities
287
+ * @param {Set<string>} declared Every declared refId
288
+ * @param {object} stats Render counters
289
+ */
290
+ export function resolveAffectsRefs(vulnerabilities, declared, stats) {
291
+ for (const vulnerability of vulnerabilities || []) {
292
+ if (
293
+ !isObjectRecord(vulnerability) ||
294
+ !Array.isArray(vulnerability.affects)
295
+ ) {
296
+ continue;
297
+ }
298
+ const affects = vulnerability.affects.filter((entry) =>
299
+ resolves(entry?.ref, declared),
300
+ );
301
+ if (affects.length !== vulnerability.affects.length) {
302
+ countDrop(
303
+ stats,
304
+ "vulnerability.affects (undeclared ref)",
305
+ vulnerability.affects.length - affects.length,
306
+ );
307
+ }
308
+ if (affects.length) {
309
+ vulnerability.affects = affects;
310
+ } else {
311
+ delete vulnerability.affects;
312
+ }
313
+ }
314
+ }