@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
@@ -47,6 +47,10 @@ import { diagnosticStream } from "../core/logger.js";
47
47
  import { dirNameStr, isMac, isWin } from "../core/paths.js";
48
48
  import { isProjectToolingAllowed } from "../core/projectTooling.js";
49
49
  import { brotliDecoderProperties, readBrotliDecoder } from "./brotliDecoder.js";
50
+ import {
51
+ createHostDotnetRuntimeComponent,
52
+ parseDotnetListRuntimes,
53
+ } from "./dotnetRuntime.js";
50
54
  import { isValidSdkmanVersion } from "./jvmToolEnv.js";
51
55
  import { createNodeRuntimeComponent } from "./nodeBuildInfo.js";
52
56
  import {
@@ -179,7 +183,8 @@ export function getOriginUrl(dir) {
179
183
  * @param {string} configKey Git config key
180
184
  * @param {string} dir repo directory
181
185
  *
182
- * @returns Output from git config or undefined
186
+ * @returns {string|undefined} The branch name, or undefined when git fails,
187
+ * for example when the directory is not a git working tree or has no commit
183
188
  */
184
189
  export function getBranch(_configKey, dir) {
185
190
  return execGitCommand(dir, ["rev-parse", "--abbrev-ref", "HEAD"]);
@@ -212,20 +217,20 @@ export function gitTreeHashes(dir) {
212
217
  }
213
218
 
214
219
  /**
215
- * Retrieves the files list from git
216
- * @param {string} dir repo directory
220
+ * Retrieves the files git tracks under a directory at HEAD. Only the
221
+ * directory's own subtree is listed, with names relative to it, so a scan of
222
+ * one directory of a larger repository does not record the rest of that
223
+ * repository.
217
224
  *
218
- * @returns Output from git config or undefined
225
+ * @param {string} dir directory inside a git working tree
226
+ *
227
+ * @returns {Object[]} files with their blob hash, name, OmniBOR id and SWHID;
228
+ * empty when git fails, for example when the directory is not a git working
229
+ * tree or has no commit
219
230
  */
220
231
  export function listFiles(dir) {
221
232
  const filesList = [];
222
- const output = execGitCommand(dir, [
223
- "ls-tree",
224
- "-l",
225
- "-r",
226
- "--full-tree",
227
- "HEAD",
228
- ]);
233
+ const output = execGitCommand(dir, ["ls-tree", "-l", "-r", "HEAD"]);
229
234
  if (output) {
230
235
  output.split("\n").forEach((l) => {
231
236
  l = l.replaceAll("\r", "");
@@ -253,7 +258,10 @@ export function listFiles(dir) {
253
258
  * @param {string} dir Repo directory
254
259
  * @param {Array} args arguments to git command
255
260
  *
256
- * @returns Output from the git command
261
+ * @returns {string|undefined} git's standard output, or undefined when git
262
+ * could not run or exited with a non-zero status. Text git writes to standard
263
+ * error, including warnings of a successful command, is never part of the
264
+ * result.
257
265
  */
258
266
  export function execGitCommand(dir, args) {
259
267
  // Prepend hardening `-c` overrides before the subcommand and run git with a
@@ -270,6 +278,8 @@ export function execGitCommand(dir, args) {
270
278
  {
271
279
  env: hardenedGitEnv(),
272
280
  shell: false,
281
+ requireSuccess: true,
282
+ includeStderr: false,
273
283
  },
274
284
  );
275
285
  }
@@ -407,6 +417,22 @@ function recordToolProbeFailure(tool, ecosystem, command, detail) {
407
417
  }
408
418
  }
409
419
 
420
+ /**
421
+ * Collapse command output to one line. The `\r` of a Windows line ending goes
422
+ * with the break it belongs to, and the empty lines of trailing output are
423
+ * dropped.
424
+ *
425
+ * @param {string} output command output
426
+ * @param {string} separator what to join the lines with
427
+ * @returns {string} the non-empty lines joined into one line
428
+ */
429
+ function collapseOutputLines(output, separator) {
430
+ return `${output || ""}`
431
+ .split(/\r?\n/)
432
+ .filter((line) => line !== "")
433
+ .join(separator);
434
+ }
435
+
410
436
  /**
411
437
  * Collect Java version and installed modules
412
438
  *
@@ -414,9 +440,9 @@ function recordToolProbeFailure(tool, ecosystem, command, detail) {
414
440
  * @returns Object containing the java details
415
441
  */
416
442
  export function collectJavaInfo(dir) {
417
- const versionDesc = getCommandOutput(getJavaCommand(), dir, ["--version"]);
443
+ const versionDesc = probeCommandOutput(getJavaCommand(), dir, ["--version"]);
418
444
  const moduleDesc =
419
- getCommandOutput(getJavaCommand(), dir, ["--list-modules"]) || "";
445
+ probeCommandOutput(getJavaCommand(), dir, ["--list-modules"]) || "";
420
446
  if (versionDesc) {
421
447
  const info = {
422
448
  type: "platform",
@@ -426,7 +452,7 @@ export function collectJavaInfo(dir) {
426
452
  properties: [
427
453
  {
428
454
  name: "java:modules",
429
- value: moduleDesc.replaceAll("\n", ", "),
455
+ value: collapseOutputLines(moduleDesc, ", "),
430
456
  },
431
457
  ],
432
458
  };
@@ -440,20 +466,41 @@ export function collectJavaInfo(dir) {
440
466
  /**
441
467
  * Collect dotnet version
442
468
  *
469
+ * `version` is the version `dotnet --version` prints, which is the SDK's. .NET
470
+ * advisories name runtime versions, so the component itself carries no purl
471
+ * and no CPE. Each shared framework `dotnet --list-runtimes` reports is a
472
+ * nested component built by createHostDotnetRuntimeComponent, without a
473
+ * bom-ref like the other toolchains.
474
+ *
443
475
  * @param {string} dir Working directory
444
476
  * @returns Object containing dotnet details
445
477
  */
446
478
  export function collectDotnetInfo(dir) {
447
- const versionDesc = getCommandOutput(DOTNET_CMD, dir, ["--version"]);
479
+ const versionDesc = probeCommandOutput(DOTNET_CMD, dir, ["--version"]);
448
480
  const moduleDesc =
449
- getCommandOutput(DOTNET_CMD, dir, ["--list-runtimes"]) || "";
481
+ probeCommandOutput(DOTNET_CMD, dir, ["--list-runtimes"]) || "";
450
482
  if (versionDesc) {
451
483
  const info = {
452
484
  type: "platform",
453
485
  name: "dotnet",
454
486
  version: versionDesc.trim(),
455
- description: moduleDesc.replaceAll("\n", "\\n"),
487
+ description: collapseOutputLines(moduleDesc, "\\n"),
456
488
  };
489
+ const components = [];
490
+ const seen = new Set();
491
+ for (const entry of parseDotnetListRuntimes(moduleDesc)) {
492
+ const key = `${entry.framework} ${entry.version}`;
493
+ if (seen.has(key)) {
494
+ continue;
495
+ }
496
+ seen.add(key);
497
+ components.push(
498
+ formulationToolchain(createHostDotnetRuntimeComponent(entry)),
499
+ );
500
+ }
501
+ if (components.length) {
502
+ info.components = components;
503
+ }
457
504
  recordToolProbeResolved("dotnet", "csharp", info.version);
458
505
  return info;
459
506
  }
@@ -469,15 +516,15 @@ export function collectDotnetInfo(dir) {
469
516
  */
470
517
  export function collectPythonInfo(dir) {
471
518
  const pythonCmd = getPythonCommand();
472
- const versionDesc = getCommandOutput(pythonCmd, dir, ["-S", "--version"]);
519
+ const versionDesc = probeCommandOutput(pythonCmd, dir, ["-S", "--version"]);
473
520
  const moduleDesc =
474
- getCommandOutput(pythonCmd, dir, ["-I", "-m", "pip", "--version"]) || "";
521
+ probeCommandOutput(pythonCmd, dir, ["-I", "-m", "pip", "--version"]) || "";
475
522
  if (versionDesc) {
476
523
  const info = {
477
524
  type: "platform",
478
525
  name: "python",
479
526
  version: versionDesc.replace("Python ", ""),
480
- description: moduleDesc.replaceAll("\n", "\\n"),
527
+ description: collapseOutputLines(moduleDesc, "\\n"),
481
528
  };
482
529
  recordToolProbeResolved("python", "python", info.version);
483
530
  return info;
@@ -510,7 +557,7 @@ export function collectNodeInfo(dir) {
510
557
  nodeInfo.cpe = runtime.cpe;
511
558
  annotateNodeBrotli(nodeInfo.components);
512
559
  }
513
- const moduleDesc = getCommandOutput(NPM_CMD, dir, ["--version"]);
560
+ const moduleDesc = probeCommandOutput(NPM_CMD, dir, ["--version"]);
514
561
  if (moduleDesc) {
515
562
  nodeInfo.description = `npm: ${moduleDesc}`;
516
563
  recordToolProbeResolved("npm", "npm", moduleDesc);
@@ -557,14 +604,14 @@ function annotateNodeBrotli(components) {
557
604
  * @returns Object containing gcc details
558
605
  */
559
606
  export function collectGccInfo(dir) {
560
- const versionDesc = getCommandOutput(GCC_CMD, dir, ["--version"]);
561
- const moduleDesc = getCommandOutput(GCC_CMD, dir, ["-print-search-dirs"]);
607
+ const versionDesc = probeCommandOutput(GCC_CMD, dir, ["--version"]);
608
+ const moduleDesc = probeCommandOutput(GCC_CMD, dir, ["-print-search-dirs"]);
562
609
  if (versionDesc) {
563
610
  const info = {
564
611
  type: "platform",
565
612
  name: "gcc",
566
613
  version: versionDesc.split("\n")[0],
567
- description: (moduleDesc || "").replaceAll("\n", "\\n"),
614
+ description: collapseOutputLines(moduleDesc, "\\n"),
568
615
  };
569
616
  recordToolProbeResolved("gcc", "c", info.version);
570
617
  return info;
@@ -580,8 +627,8 @@ export function collectGccInfo(dir) {
580
627
  * @returns Object containing rust details
581
628
  */
582
629
  export function collectRustInfo(dir) {
583
- const versionDesc = getCommandOutput(RUSTC_CMD, dir, ["--version"]);
584
- const moduleDesc = getCommandOutput(CARGO_CMD, dir, ["--version"]);
630
+ const versionDesc = probeCommandOutput(RUSTC_CMD, dir, ["--version"]);
631
+ const moduleDesc = probeCommandOutput(CARGO_CMD, dir, ["--version"]);
585
632
  if (versionDesc) {
586
633
  const info = {
587
634
  type: "platform",
@@ -603,7 +650,7 @@ export function collectRustInfo(dir) {
603
650
  * @returns Object containing go details
604
651
  */
605
652
  export function collectGoInfo(dir) {
606
- const versionDesc = getCommandOutput(GO_CMD, dir, ["version"]);
653
+ const versionDesc = probeCommandOutput(GO_CMD, dir, ["version"]);
607
654
  if (versionDesc) {
608
655
  const info = {
609
656
  type: "platform",
@@ -624,7 +671,7 @@ export function collectGoInfo(dir) {
624
671
  * @returns Object containing swift details
625
672
  */
626
673
  export function collectSwiftInfo(dir) {
627
- const versionDesc = getCommandOutput(SWIFT_CMD, dir, ["--version"]);
674
+ const versionDesc = probeCommandOutput(SWIFT_CMD, dir, ["--version"]);
628
675
  if (versionDesc) {
629
676
  const info = {
630
677
  type: "platform",
@@ -710,7 +757,7 @@ function rubyKeepsCpe(dir) {
710
757
  const target = isWin
711
758
  ? ""
712
759
  : firstOutputLine(
713
- getCommandOutput(RUBY_CMD, dir, ["-e", "print RbConfig.ruby"]),
760
+ probeCommandOutput(RUBY_CMD, dir, ["-e", "print RbConfig.ruby"]),
714
761
  );
715
762
  if (!isAbsolute(target)) {
716
763
  return false;
@@ -738,7 +785,7 @@ function rubyKeepsCpe(dir) {
738
785
  * @returns Object containing Ruby details
739
786
  */
740
787
  export function collectRubyInfo(dir) {
741
- const versionDesc = getCommandOutput(RUBY_CMD, dir, ["--version"]);
788
+ const versionDesc = probeCommandOutput(RUBY_CMD, dir, ["--version"]);
742
789
  const banner = firstOutputLine(versionDesc);
743
790
  const parsed = parseRubyDescription(banner);
744
791
  if (parsed) {
@@ -800,7 +847,9 @@ export function isRubyVersionActive(info, version) {
800
847
  * @returns {Object|undefined} the RubyGems component
801
848
  */
802
849
  export function collectRubyGemsInfo(dir) {
803
- const token = firstOutputLine(getCommandOutput(GEM_CMD, dir, ["--version"]));
850
+ const token = firstOutputLine(
851
+ probeCommandOutput(GEM_CMD, dir, ["--version"]),
852
+ );
804
853
  if (/^\d+\.\d+\.\d+$/.test(token)) {
805
854
  recordToolProbeResolved("gem", "ruby", token);
806
855
  return formulationToolchain(
@@ -828,7 +877,7 @@ export function collectRubyGemsInfo(dir) {
828
877
  */
829
878
  export function collectBundlerInfo(dir) {
830
879
  const line = firstOutputLine(
831
- getCommandOutput(BUNDLE_CMD, dir, ["--version"]),
880
+ probeCommandOutput(BUNDLE_CMD, dir, ["--version"]),
832
881
  );
833
882
  const token = line.startsWith("Bundler version ")
834
883
  ? line.slice("Bundler version ".length).trim()
@@ -892,7 +941,7 @@ function formulationToolchain(component) {
892
941
  * @returns Object containing the PHP details
893
942
  */
894
943
  export function collectPhpInfo(dir) {
895
- const output = getCommandOutput(PHP_CMD, dir, ["-n", "-v"]);
944
+ const output = probeCommandOutput(PHP_CMD, dir, ["-n", "-v"]);
896
945
  const banner = firstOutputLine(output);
897
946
  const reported = banner.startsWith("PHP ")
898
947
  ? parsePhpReportedVersion(banner.slice("PHP ".length))
@@ -934,7 +983,7 @@ export function collectPhpInfo(dir) {
934
983
  * @returns Object containing the Composer details
935
984
  */
936
985
  export function collectComposerInfo(dir) {
937
- const output = getCommandOutput(COMPOSER_CMD, dir, [
986
+ const output = probeCommandOutput(COMPOSER_CMD, dir, [
938
987
  "--version",
939
988
  "--no-plugins",
940
989
  "--no-scripts",
@@ -1053,16 +1102,55 @@ export function collectEnvInfo(dir, options = undefined) {
1053
1102
  return infoComponents;
1054
1103
  }
1055
1104
 
1105
+ /**
1106
+ * Longest a toolchain version probe may run. A `--version` call answers in
1107
+ * well under a second; a tool that sets itself up on first use, such as
1108
+ * dotnet or gem in a fresh home directory, or one that waits on a prompt,
1109
+ * must not hold up a scan for the general spawn timeout.
1110
+ */
1111
+ const VERSION_PROBE_TIMEOUT_MS = 60000;
1112
+
1113
+ /**
1114
+ * Run a toolchain version probe with VERSION_PROBE_TIMEOUT_MS as its limit.
1115
+ *
1116
+ * A probe that fails gives no output. Its text is then an error message, not
1117
+ * a version: `dotnet --version` under a global.json that pins an SDK the host
1118
+ * lacks exits with an error and lists every installed SDK, which would
1119
+ * otherwise become the version of the dotnet component in the formulation.
1120
+ *
1121
+ * @param {string} cmd Command to execute
1122
+ * @param {string} dir Working directory
1123
+ * @param {string[]} args Arguments
1124
+ * @returns {string|undefined} Output of the command, or undefined when it
1125
+ * could not run or exited with a non-zero status
1126
+ */
1127
+ const probeCommandOutput = (cmd, dir, args) =>
1128
+ getCommandOutput(cmd, dir, args, {
1129
+ requireSuccess: true,
1130
+ timeout: VERSION_PROBE_TIMEOUT_MS,
1131
+ });
1132
+
1056
1133
  /**
1057
1134
  * Execute any command to retrieve the output
1058
1135
  *
1059
1136
  * @param {*} cmd Command to execute
1060
1137
  * @param {*} dir working directory
1061
1138
  * @param {*} args arguments
1062
- * @param {Object} [spawnOverrides] extra options merged into spawnSync (e.g. env)
1063
- * @returns String output from the command or undefined in case of error
1139
+ * @param {Object} [options] extra options merged into spawnSync (e.g. env),
1140
+ * plus `requireSuccess`, which gives undefined for a command that exits with
1141
+ * a non-zero status, and `includeStderr`, which joins the command's standard
1142
+ * error into the result (the default). With `includeStderr` false only the
1143
+ * standard output is returned, so text a command writes to standard error
1144
+ * cannot reach a parser of its output.
1145
+ * @returns {string|undefined} Output of the command, or undefined when it
1146
+ * could not run or printed nothing
1064
1147
  */
1065
- const getCommandOutput = (cmd, dir, args, spawnOverrides = {}) => {
1148
+ const getCommandOutput = (cmd, dir, args, options = {}) => {
1149
+ const {
1150
+ requireSuccess = false,
1151
+ includeStderr = true,
1152
+ ...spawnOverrides
1153
+ } = options;
1066
1154
  let commandToUse = cmd;
1067
1155
  // If the command includes space, automatically move it to the front of the args.
1068
1156
  if (cmd?.trim().includes(" ")) {
@@ -1091,7 +1179,13 @@ const getCommandOutput = (cmd, dir, args, spawnOverrides = {}) => {
1091
1179
  maxBuffer: MAX_BUFFER * 2,
1092
1180
  ...spawnOverrides,
1093
1181
  });
1182
+ if (requireSuccess && (result.error || result.status !== 0)) {
1183
+ return undefined;
1184
+ }
1094
1185
  const stdout = result.stdout ? result.stdout.toString() : "";
1186
+ if (!includeStderr) {
1187
+ return stdout.trim() || undefined;
1188
+ }
1095
1189
  const stderr = result.stderr ? result.stderr.toString() : "";
1096
1190
  return `${stdout}\n${stderr}`.trim() || undefined;
1097
1191
  };
@@ -23,6 +23,7 @@ import {
23
23
  } from "./dosaiParsers.js";
24
24
  import { createGtfoBinsPropertiesFromRow } from "./gtfobins.js";
25
25
  import { createLolbasProperties } from "./lolbas.js";
26
+ import { occurrenceLocationKey } from "./occurrenceKey.js";
26
27
  import { isOsPackagePurlType } from "./osPackagePurl.js";
27
28
  import {
28
29
  createOsQueryFallbackBomRef,
@@ -370,7 +371,7 @@ export function addEvidenceForDotnet(pkgList, slicesFile, options = {}) {
370
371
  addDosaiSetValue(
371
372
  purlLocationMap,
372
373
  modPurl,
373
- `${adep.Path}#${adep.LineNumber}`,
374
+ occurrenceLocationKey(adep.Path, adep.LineNumber),
374
375
  );
375
376
  }
376
377
  }
@@ -14,6 +14,13 @@ import { githubActionsParser } from "./ciParsers/githubActions.js";
14
14
  import { gitlabCiParser } from "./ciParsers/gitlabCi.js";
15
15
  import { jenkinsParser } from "./ciParsers/jenkins.js";
16
16
  import { trimComponents } from "./depsUtils.js";
17
+ import {
18
+ dotnetProjectFormulationParser,
19
+ formulationSourcePath,
20
+ goModuleFormulationParser,
21
+ makefileFormulationParser,
22
+ pythonRequirementsFormulationParser,
23
+ } from "./ecosystemFormulationParsers.js";
17
24
  import {
18
25
  collectEnvInfo,
19
26
  getBranch,
@@ -49,8 +56,9 @@ function buildReadmeSecurityComponents(discoveryPath, options) {
49
56
  if (!scan.hasHiddenUnicode) {
50
57
  continue;
51
58
  }
59
+ const sourcePath = formulationSourcePath(filePath, discoveryPath);
52
60
  const properties = [
53
- { name: "internal:SrcFile", value: filePath },
61
+ { name: "internal:SrcFile", value: sourcePath },
54
62
  { name: "cdx:file:kind", value: "readme" },
55
63
  { name: "cdx:file:hasHiddenUnicode", value: "true" },
56
64
  {
@@ -73,7 +81,7 @@ function buildReadmeSecurityComponents(discoveryPath, options) {
73
81
  });
74
82
  }
75
83
  components.push({
76
- "bom-ref": `file:${filePath}`,
84
+ "bom-ref": `file:${sourcePath}`,
77
85
  name: basename(filePath),
78
86
  properties,
79
87
  type: "file",
@@ -92,13 +100,22 @@ function buildReadmeSecurityComponents(discoveryPath, options) {
92
100
  * {
93
101
  * id: string, // unique stable identifier
94
102
  * patterns: string[], // non-empty array of glob patterns for file discovery
95
- * parse(files: string[], options: Object): // synchronous function
103
+ * parse(files: string[], options: Object, context: { rootDir: string }):
104
+ * // synchronous function
96
105
  * { workflows?, components?, services?, properties?, dependencies? }
97
106
  * }
98
107
  * ```
108
+ *
109
+ * `context.rootDir` is the scanned project root. A parser that records file
110
+ * paths records them relative to it, so no path of the scanning machine
111
+ * reaches the BOM.
99
112
  */
100
113
  const _parsers = [
101
114
  rustFormulationParser,
115
+ makefileFormulationParser,
116
+ goModuleFormulationParser,
117
+ pythonRequirementsFormulationParser,
118
+ dotnetProjectFormulationParser,
102
119
  githubActionsParser,
103
120
  gitlabCiParser,
104
121
  jenkinsParser,
@@ -189,6 +206,49 @@ const ENV_BLOCKLIST = [
189
206
  "webhook",
190
207
  ];
191
208
 
209
+ /**
210
+ * Remove the parts of every URL in an environment value that can carry
211
+ * credentials: the userinfo before `@`, the query string and the fragment.
212
+ * The value is scanned with indexOf, one URL at a time, so the work stays
213
+ * linear in its length.
214
+ *
215
+ * @param {string} value environment variable value
216
+ * @returns {string} the value with URL credentials, queries and fragments removed
217
+ */
218
+ function stripUrlCredentials(value) {
219
+ const delimiters = " \t\r\n\"'`,;<>|";
220
+ let result = "";
221
+ let cursor = 0;
222
+ let schemeEnd = value.indexOf("://");
223
+ while (schemeEnd !== -1) {
224
+ const start = schemeEnd + 3;
225
+ let end = start;
226
+ while (end < value.length && !delimiters.includes(value[end])) {
227
+ end += 1;
228
+ }
229
+ const url = value.slice(start, end);
230
+ let authorityEnd = url.length;
231
+ for (const marker of ["/", "?", "#"]) {
232
+ const at = url.indexOf(marker);
233
+ if (at !== -1 && at < authorityEnd) {
234
+ authorityEnd = at;
235
+ }
236
+ }
237
+ const authority = url.slice(0, authorityEnd);
238
+ let rest = url.slice(authorityEnd);
239
+ for (const marker of ["?", "#"]) {
240
+ const at = rest.indexOf(marker);
241
+ if (at !== -1) {
242
+ rest = rest.slice(0, at);
243
+ }
244
+ }
245
+ result += `${value.slice(cursor, start)}${authority.slice(authority.lastIndexOf("@") + 1)}${rest}`;
246
+ cursor = end;
247
+ schemeEnd = value.indexOf("://", end);
248
+ }
249
+ return result + value.slice(cursor);
250
+ }
251
+
192
252
  // The project types a scan asked for, as given on the command line.
193
253
  function requestedProjectTypes(options) {
194
254
  return [].concat(options?.projectType || []).flatMap((type) =>
@@ -302,9 +362,16 @@ export function addFormulationSection(filePath, options, context = {}) {
302
362
  // scanning host's tools say nothing about it.
303
363
  // Only an explicit container type counts: hasAnyProjectType treats
304
364
  // `universal` as every type, and a universal source scan runs on the host.
305
- const describesScanningHost = !requestedProjectTypes(options).some((type) =>
365
+ const requestedTypes = requestedProjectTypes(options);
366
+ const describesScanningHost = !requestedTypes.some((type) =>
306
367
  PROJECT_TYPE_ALIASES.oci.includes(type),
307
368
  );
369
+ // An OS inventory (obom) describes the live host. Its path is usually the
370
+ // working directory, such as a home directory or /, so the build-input,
371
+ // CI and README discovery below would walk a tree that is not a project.
372
+ const describesLiveHost =
373
+ requestedTypes.length > 0 &&
374
+ requestedTypes.every((type) => PROJECT_TYPE_ALIASES.os.includes(type));
308
375
  if (!describesScanningHost) {
309
376
  thoughtLog(
310
377
  "Skipping the toolchain and OS crypto library probes for formulation, since they would describe the scanning host and not the image.",
@@ -320,8 +387,15 @@ export function addFormulationSection(filePath, options, context = {}) {
320
387
  components = components.concat(infoComponents);
321
388
  }
322
389
 
323
- // OS crypto libraries (cbom mode)
324
- if (options.includeCrypto && describesScanningHost) {
390
+ // OS crypto libraries (cbom mode). The queries also read the host's
391
+ // certificates, authorized keys and keychain items, so they run only when
392
+ // formulation was requested explicitly, not when CI, a profile, an OS type
393
+ // or the cbom command turned it on.
394
+ if (
395
+ options.includeCrypto &&
396
+ describesScanningHost &&
397
+ !options.formulationAutomatic
398
+ ) {
325
399
  const cryptoLibs = collectOSCryptoLibs(options, context?.executeOsQuery);
326
400
  if (cryptoLibs?.length) {
327
401
  components = components.concat(cryptoLibs);
@@ -335,8 +409,13 @@ export function addFormulationSection(filePath, options, context = {}) {
335
409
  const ciProperties = [];
336
410
 
337
411
  const discoveryPath = projectPath || ".";
412
+ if (describesLiveHost) {
413
+ thoughtLog(
414
+ "Skipping build-input, CI and README discovery for formulation, since an OS inventory describes the host and not a project tree.",
415
+ );
416
+ }
338
417
 
339
- for (const parser of _parsers) {
418
+ for (const parser of describesLiveHost ? [] : _parsers) {
340
419
  const matchedFiles = [];
341
420
  for (const pattern of parser.patterns) {
342
421
  const found = getAllFiles(discoveryPath, pattern, options);
@@ -351,7 +430,9 @@ export function addFormulationSection(filePath, options, context = {}) {
351
430
 
352
431
  let result;
353
432
  try {
354
- result = parser.parse(uniqueMatchedFiles, options);
433
+ result = parser.parse(uniqueMatchedFiles, options, {
434
+ rootDir: discoveryPath,
435
+ });
355
436
  } catch (err) {
356
437
  // A broken parser must not kill SBOM generation
357
438
  console.warn(
@@ -383,10 +464,9 @@ export function addFormulationSection(filePath, options, context = {}) {
383
464
  components = components.concat(ciComponents);
384
465
  }
385
466
 
386
- const readmeSecurityComponents = buildReadmeSecurityComponents(
387
- discoveryPath,
388
- options,
389
- );
467
+ const readmeSecurityComponents = describesLiveHost
468
+ ? []
469
+ : buildReadmeSecurityComponents(discoveryPath, options);
390
470
  if (readmeSecurityComponents.length) {
391
471
  components = components.concat(readmeSecurityComponents);
392
472
  }
@@ -405,7 +485,9 @@ export function addFormulationSection(filePath, options, context = {}) {
405
485
  !ENV_BLOCKLIST.some((b) => value.toLowerCase().includes(b)) &&
406
486
  value.length
407
487
  ) {
408
- environmentVars.push({ name: aevar, value });
488
+ // A registry or mirror URL can carry a password before `@` or a
489
+ // token in its query string, so only the scheme, host and path stay.
490
+ environmentVars.push({ name: aevar, value: stripUrlCredentials(value) });
409
491
  }
410
492
  }
411
493
 
@@ -8,6 +8,9 @@ import {
8
8
  readEnvironmentVariable,
9
9
  recordActivity,
10
10
  } from "../core/activity.js";
11
+ import { thoughtLog } from "../core/logger.js";
12
+ import { isCycloneDx20SpecVersion } from "./bomUtils.js";
13
+ import { readCycloneDx20 } from "./cyclonedx20.js";
11
14
  import { getHbomCommandDiagnosticSummary } from "./hbomAnalysis.js";
12
15
  import { importHbomModule } from "./hbomLoader.js";
13
16
  import { resolveCdxgenPlugins, resolvePluginBinary } from "./plugins.js";
@@ -411,8 +414,9 @@ export function ensureSupportedHbomSpecVersion(specVersion) {
411
414
  if (specVersion === undefined || specVersion === null || specVersion === "") {
412
415
  return;
413
416
  }
414
- if (Number(specVersion) !== 1.7) {
415
- throw new Error("HBOM generation currently supports only CycloneDX 1.7.");
417
+ // cdxgen renders 2.0 itself from the 1.7 collector output.
418
+ if (Number(specVersion) !== 1.7 && Number(specVersion) !== 2) {
419
+ throw new Error("HBOM generation supports only CycloneDX 1.7 and 2.0.");
416
420
  }
417
421
  }
418
422
 
@@ -474,6 +478,10 @@ export function normalizeHbomOptions(options = {}) {
474
478
  includeSensitiveIdentifiers:
475
479
  options.includeSensitiveIdentifiers ?? options.sensitive ?? false,
476
480
  platform: options.platform,
481
+ // Always 1.7: cdxgen's own pipeline (postProcess and its 2.0 renderer)
482
+ // turns the 1.7 collector output into 2.0, the way it renders every
483
+ // other BOM. A cdx-hbom that can emit 2.0 itself is never asked to.
484
+ specVersion: "1.7",
477
485
  timeoutMs:
478
486
  Number.isNaN(timeoutMs) || timeoutMs <= 0 ? undefined : timeoutMs,
479
487
  };
@@ -722,6 +730,38 @@ export function addHbomAnalysisProperties(bomJson) {
722
730
  return bomJson;
723
731
  }
724
732
 
733
+ /**
734
+ * The 1.x-shaped model of a collector result. cdxgen asks the collector for
735
+ * 1.7; a collector that still answers with a CycloneDX 2.0 document (a
736
+ * `specFormat`, or specVersion 2.0) is read with readCycloneDx20, so the
737
+ * 1.x pipeline that follows never mistakes it for a 1.x BOM and loses its
738
+ * inventories.
739
+ *
740
+ * @param {object} collected What collectHardware returned
741
+ * @returns {object} The 1.x-shaped HBOM
742
+ */
743
+ export function toInternalHbom(collected) {
744
+ if (
745
+ !collected ||
746
+ typeof collected !== "object" ||
747
+ !(collected.specFormat || isCycloneDx20SpecVersion(collected.specVersion))
748
+ ) {
749
+ return collected;
750
+ }
751
+ const { internalBom, losses } = readCycloneDx20(collected);
752
+ if (losses.length) {
753
+ thoughtLog(
754
+ `The HBOM collector returned CycloneDX 2.0; reading it into the 1.x model reported ${losses.length} loss(es).`,
755
+ losses,
756
+ );
757
+ }
758
+ // The pipeline renders 2.0 again from the 1.x model, so the model says
759
+ // what the collector was asked for.
760
+ internalBom.specVersion = "1.7";
761
+ delete internalBom.specFormat;
762
+ return internalBom;
763
+ }
764
+
725
765
  /**
726
766
  * Generate an HBOM using the optional cdx-hbom package.
727
767
  *
@@ -750,12 +790,11 @@ export async function createHbomDocument(options = {}) {
750
790
  });
751
791
  return createDryRunHbomDocument(options);
752
792
  }
753
- const bomJson = addHbomAnalysisProperties(
754
- await hbomModule.collectHardware(normalizedOptions),
755
- );
793
+ const collected = await hbomModule.collectHardware(normalizedOptions);
794
+ const bomJson = addHbomAnalysisProperties(toInternalHbom(collected));
756
795
  if (isDryRun) {
757
796
  recordHbomCollectorTrace(
758
- hbomModule.getCollectorTrace?.(bomJson) ?? normalizedOptions.trace,
797
+ hbomModule.getCollectorTrace?.(collected) ?? normalizedOptions.trace,
759
798
  );
760
799
  }
761
800
  return bomJson;