@cdxgen/cdxgen 13.0.1 → 13.2.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 (286) hide show
  1. package/README.md +30 -16
  2. package/bin/audit.js +6 -0
  3. package/bin/cdxgen.js +141 -7
  4. package/bin/evinse.js +3 -1
  5. package/data/component-tags.json +8 -1
  6. package/data/crypto-oid.json +88 -12
  7. package/data/frameworks-list.json +86 -1
  8. package/data/remediations.json +1167 -0
  9. package/data/rules/ai-bom.yaml +78 -0
  10. package/data/rules/asar-archives.yaml +1 -1
  11. package/data/rules/build-fidelity.yaml +782 -0
  12. package/data/rules/cargo-rust.yaml +132 -0
  13. package/data/rules/mcp-servers.yaml +122 -0
  14. package/data/rules/package-integrity.yaml +13 -13
  15. package/lib/audit/index.js +48 -4
  16. package/lib/audit/scoring.js +2 -8
  17. package/lib/audit/targets.js +6 -1
  18. package/lib/cli/bomAssembly.js +32 -1
  19. package/lib/cli/cliOptions.js +94 -7
  20. package/lib/cli/index.js +268 -49
  21. package/lib/cli/jsBom.js +91 -5
  22. package/lib/cli/jvmBom.js +1515 -1314
  23. package/lib/cli/managedBom.js +249 -28
  24. package/lib/cli/nativeBom.js +986 -10
  25. package/lib/core/activity.js +120 -2
  26. package/lib/core/buildLedger.js +799 -0
  27. package/lib/core/deferredExit.js +141 -0
  28. package/lib/core/env.js +108 -2
  29. package/lib/core/fs.js +60 -0
  30. package/lib/core/logger.js +12 -99
  31. package/lib/core/severity.js +16 -0
  32. package/lib/core/syncFileWriter.js +106 -0
  33. package/lib/core/userAgent.js +151 -0
  34. package/lib/ecosystems/cargoCache.js +364 -0
  35. package/lib/ecosystems/caxa.js +13 -5
  36. package/lib/ecosystems/cppEvidence.js +40 -2
  37. package/lib/ecosystems/ecosystems.js +250 -91
  38. package/lib/ecosystems/golem.js +11 -1
  39. package/lib/ecosystems/gradleutils.js +498 -17
  40. package/lib/ecosystems/kosi.js +1256 -0
  41. package/lib/ecosystems/parsers-crystal.js +179 -0
  42. package/lib/ecosystems/parsers-dotnet.js +69 -39
  43. package/lib/ecosystems/parsers-elm.js +816 -0
  44. package/lib/ecosystems/parsers-gleam.js +1 -1
  45. package/lib/ecosystems/parsers-js.js +26 -0
  46. package/lib/ecosystems/parsers-julia.js +268 -0
  47. package/lib/ecosystems/parsers-jvm.js +9 -0
  48. package/lib/ecosystems/parsers-lua.js +218 -0
  49. package/lib/ecosystems/parsers-misc.js +188 -2
  50. package/lib/ecosystems/parsers-nim.js +221 -0
  51. package/lib/ecosystems/parsers-opam.js +180 -0
  52. package/lib/ecosystems/parsers-perl.js +277 -0
  53. package/lib/ecosystems/parsers-python.js +172 -36
  54. package/lib/ecosystems/parsers-r.js +148 -0
  55. package/lib/ecosystems/parsers-rebar.js +411 -0
  56. package/lib/ecosystems/parsers-rust.js +944 -19
  57. package/lib/ecosystems/parsers-spack.js +204 -0
  58. package/lib/ecosystems/parsers-stack.js +264 -0
  59. package/lib/ecosystems/parsers-terraform.js +194 -0
  60. package/lib/ecosystems/pypiClassifiers.js +326 -0
  61. package/lib/ecosystems/pythonutils.js +49 -1
  62. package/lib/ecosystems/rubyutils.js +13 -0
  63. package/lib/ecosystems/rusi.js +126 -6
  64. package/lib/evinser/evinser.js +141 -13
  65. package/lib/helpers/agenticToolutils.js +925 -0
  66. package/lib/helpers/remote/dependency-track.js +81 -14
  67. package/lib/inventory/agentFormulationParser.js +57 -4
  68. package/lib/inventory/aiCollector.js +504 -10
  69. package/lib/inventory/aiInventory.js +12 -0
  70. package/lib/inventory/analyzer.js +488 -30
  71. package/lib/inventory/astWorker.js +6 -0
  72. package/lib/inventory/auditCategories.js +17 -0
  73. package/lib/inventory/cbomutils.js +30 -0
  74. package/lib/inventory/cdxrs.js +7 -2
  75. package/lib/inventory/communityAiConfigParser.js +215 -2
  76. package/lib/inventory/depsUtils.js +72 -0
  77. package/lib/inventory/dosai.js +508 -22
  78. package/lib/inventory/envcontext.js +215 -24
  79. package/lib/inventory/evidenceUtils.js +13 -0
  80. package/lib/inventory/fetchBatch.js +131 -2
  81. package/lib/inventory/fetchRate.js +74 -11
  82. package/lib/inventory/forgeEnricher.js +3 -2
  83. package/lib/inventory/jvmToolEnv.js +594 -0
  84. package/lib/inventory/mcpConfigParser.js +166 -2
  85. package/lib/inventory/mcpDiscovery.js +11 -0
  86. package/lib/inventory/mcpServerManifest.js +389 -0
  87. package/lib/inventory/mcpbBundle.js +419 -0
  88. package/lib/inventory/osinfo.js +1 -0
  89. package/lib/inventory/plugins.js +3 -0
  90. package/lib/inventory/protobom.js +54 -88
  91. package/lib/inventory/protobomLoader.js +4 -0
  92. package/lib/inventory/rustFormulationParser.js +1 -1
  93. package/lib/inventory/source.js +10 -4
  94. package/lib/inventory/stylePackageRefs.js +41 -0
  95. package/lib/inventory/svelteUtils.js +524 -0
  96. package/lib/inventory/toolRequirements.js +903 -0
  97. package/lib/managers/binary.js +24 -0
  98. package/lib/managers/docker.js +26 -5
  99. package/lib/parsers/huggingfaceUtils.js +15 -7
  100. package/lib/parsers/onnx.js +294 -0
  101. package/lib/parsers/safetensors.js +114 -0
  102. package/lib/server/server.js +1 -1
  103. package/lib/stages/postgen/annotationBuilder.js +97 -0
  104. package/lib/stages/postgen/auditBom.js +76 -79
  105. package/lib/stages/postgen/introspection/annotate.js +320 -0
  106. package/lib/stages/postgen/introspection/commandFacts.js +293 -0
  107. package/lib/stages/postgen/introspection/emit.js +192 -0
  108. package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
  109. package/lib/stages/postgen/introspection/markers.js +250 -0
  110. package/lib/stages/postgen/introspection/reflect.js +1214 -0
  111. package/lib/stages/postgen/introspection/report.js +1208 -0
  112. package/lib/stages/postgen/introspection/score.js +1067 -0
  113. package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
  114. package/lib/stages/postgen/parentEdge.js +156 -0
  115. package/lib/stages/postgen/postgen.js +284 -3
  116. package/lib/stages/postgen/ruleEngine.js +17 -0
  117. package/lib/stages/pregen/envAudit.js +42 -2
  118. package/lib/stages/pregen/pregen.js +1171 -6
  119. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  120. package/lib/validator/bomValidator.js +11 -2
  121. package/package.json +26 -26
  122. package/types/lib/audit/index.d.ts.map +1 -1
  123. package/types/lib/audit/scoring.d.ts +2 -8
  124. package/types/lib/audit/scoring.d.ts.map +1 -1
  125. package/types/lib/audit/targets.d.ts.map +1 -1
  126. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  127. package/types/lib/cli/cliOptions.d.ts +2 -1
  128. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  129. package/types/lib/cli/index.d.ts +10 -2
  130. package/types/lib/cli/index.d.ts.map +1 -1
  131. package/types/lib/cli/jsBom.d.ts +23 -0
  132. package/types/lib/cli/jsBom.d.ts.map +1 -1
  133. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  134. package/types/lib/cli/managedBom.d.ts +33 -0
  135. package/types/lib/cli/managedBom.d.ts.map +1 -1
  136. package/types/lib/cli/nativeBom.d.ts +88 -8
  137. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  138. package/types/lib/core/activity.d.ts.map +1 -1
  139. package/types/lib/core/buildLedger.d.ts +272 -0
  140. package/types/lib/core/buildLedger.d.ts.map +1 -0
  141. package/types/lib/core/deferredExit.d.ts +84 -0
  142. package/types/lib/core/deferredExit.d.ts.map +1 -0
  143. package/types/lib/core/env.d.ts +43 -1
  144. package/types/lib/core/env.d.ts.map +1 -1
  145. package/types/lib/core/fs.d.ts +21 -0
  146. package/types/lib/core/fs.d.ts.map +1 -1
  147. package/types/lib/core/logger.d.ts +2 -11
  148. package/types/lib/core/logger.d.ts.map +1 -1
  149. package/types/lib/core/severity.d.ts +11 -0
  150. package/types/lib/core/severity.d.ts.map +1 -0
  151. package/types/lib/core/syncFileWriter.d.ts +33 -0
  152. package/types/lib/core/syncFileWriter.d.ts.map +1 -0
  153. package/types/lib/core/userAgent.d.ts +56 -0
  154. package/types/lib/core/userAgent.d.ts.map +1 -0
  155. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  156. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  157. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  158. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  159. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  160. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  161. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  162. package/types/lib/ecosystems/gradleutils.d.ts +53 -0
  163. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  164. package/types/lib/ecosystems/kosi.d.ts +73 -0
  165. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  166. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  167. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  168. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  169. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  170. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  171. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  172. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  173. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  174. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  175. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  176. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  177. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  178. package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
  179. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  180. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  181. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  182. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  183. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  184. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  185. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  186. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  187. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  188. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  189. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  190. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  191. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  192. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  193. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  194. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  195. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  196. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  197. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  198. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  199. package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
  200. package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
  201. package/types/lib/ecosystems/pythonutils.d.ts +21 -1
  202. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  203. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  204. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  205. package/types/lib/evinser/evinser.d.ts +34 -1
  206. package/types/lib/evinser/evinser.d.ts.map +1 -1
  207. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  208. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  209. package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
  210. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  211. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  212. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  213. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  214. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  215. package/types/lib/inventory/auditCategories.d.ts +12 -0
  216. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  217. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  218. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  219. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  220. package/types/lib/inventory/dosai.d.ts +43 -0
  221. package/types/lib/inventory/dosai.d.ts.map +1 -1
  222. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  223. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  224. package/types/lib/inventory/fetchBatch.d.ts +43 -0
  225. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  226. package/types/lib/inventory/fetchRate.d.ts +9 -6
  227. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  228. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  229. package/types/lib/inventory/jvmToolEnv.d.ts +190 -0
  230. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
  231. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  232. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  233. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  234. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  235. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  236. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  237. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  238. package/types/lib/inventory/plugins.d.ts.map +1 -1
  239. package/types/lib/inventory/protobom.d.ts +18 -13
  240. package/types/lib/inventory/protobom.d.ts.map +1 -1
  241. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  242. package/types/lib/inventory/source.d.ts.map +1 -1
  243. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  244. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  245. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  246. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  247. package/types/lib/inventory/toolRequirements.d.ts +216 -0
  248. package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
  249. package/types/lib/managers/binary.d.ts.map +1 -1
  250. package/types/lib/managers/docker.d.ts.map +1 -1
  251. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  252. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  253. package/types/lib/parsers/onnx.d.ts +28 -0
  254. package/types/lib/parsers/onnx.d.ts.map +1 -0
  255. package/types/lib/parsers/safetensors.d.ts +22 -0
  256. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  257. package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
  258. package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
  259. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  260. package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
  261. package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
  262. package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
  263. package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
  264. package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
  265. package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
  266. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
  267. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
  268. package/types/lib/stages/postgen/introspection/markers.d.ts +58 -0
  269. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
  270. package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
  271. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
  272. package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
  273. package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
  274. package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
  275. package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
  276. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
  277. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
  278. package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
  279. package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
  280. package/types/lib/stages/postgen/postgen.d.ts +35 -2
  281. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  282. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  283. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  284. package/types/lib/stages/pregen/pregen.d.ts +131 -0
  285. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  286. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -0,0 +1,419 @@
1
+ import { closeSync, openSync, readSync, statSync } from "node:fs";
2
+ import { basename } from "node:path";
3
+ import { inflateRawSync } from "node:zlib";
4
+
5
+ import { DEBUG_MODE } from "../core/activity.js";
6
+ import { sanitizeBomPropertyValue } from "../core/propertySanitizer.js";
7
+ import { sanitizeMcpRefToken } from "./mcpDiscovery.js";
8
+ import { scanTextForHiddenUnicode } from "./unicodeScan.js";
9
+
10
+ const MCPB_BUNDLE_PATTERNS = ["**/*.mcpb", "**/*.dxt"];
11
+
12
+ // Bound only what is decompressed. The archive itself is no longer fully
13
+ // buffered: the End-Of-Central-Directory record, the central directory, and a
14
+ // single local entry are read positionally, so a large vendored bundle is
15
+ // inspected rather than silently skipped.
16
+ const MAX_MANIFEST_BYTES = 4 * 1024 * 1024;
17
+ // Cap the central directory read so a hostile archive cannot force a huge
18
+ // allocation. 16 MiB comfortably covers tens of thousands of entries.
19
+ const MAX_CENTRAL_DIR_BYTES = 16 * 1024 * 1024;
20
+ // The EOCD record is 22 bytes plus an optional comment of up to 65535 bytes.
21
+ const MAX_EOCD_SCAN_BYTES = 22 + 65535;
22
+ const MAX_DESCRIPTION_BYTES = 1000;
23
+ const EOCD_SIGNATURE = 0x06054b50;
24
+ const CENTRAL_SIGNATURE = 0x02014b50;
25
+ const LOCAL_SIGNATURE = 0x04034b50;
26
+
27
+ const SECRET_FIELD_NAME_PATTERN =
28
+ /(token|secret|password|api[_-]?key|client[_-]?secret|authorization|credential)/iu;
29
+
30
+ /**
31
+ * Read a byte range from an open file descriptor into a fresh buffer.
32
+ *
33
+ * @param {number} fd Open file descriptor
34
+ * @param {number} position Absolute byte offset
35
+ * @param {number} length Number of bytes to read
36
+ * @returns {Buffer|undefined} Filled buffer, or undefined on short read
37
+ */
38
+ function readRange(fd, position, length) {
39
+ if (length <= 0) {
40
+ return Buffer.alloc(0);
41
+ }
42
+ const buffer = Buffer.alloc(length);
43
+ const bytesRead = readSync(fd, buffer, 0, length, position);
44
+ if (bytesRead !== length) {
45
+ return undefined;
46
+ }
47
+ return buffer;
48
+ }
49
+
50
+ /**
51
+ * Read and decompress the `manifest.json` entry from a zip archive using
52
+ * positional reads, without any third-party dependency and without buffering
53
+ * the whole archive. Only the End-Of-Central-Directory record, the central
54
+ * directory, and the single manifest entry are read, so an untrusted bundle is
55
+ * never fully expanded.
56
+ *
57
+ * Zip64 archives (more than 65535 entries, or offsets beyond 4 GiB) are not
58
+ * decoded; such a bundle is skipped with a debug log rather than mis-read.
59
+ *
60
+ * @param {string} filePath Path to the .mcpb/.dxt bundle
61
+ * @returns {string|undefined} Decoded manifest.json text, or undefined
62
+ */
63
+ function readManifestFromBundle(filePath) {
64
+ let fd;
65
+ try {
66
+ const size = statSync(filePath).size;
67
+ if (size < 22) {
68
+ return undefined;
69
+ }
70
+ fd = openSync(filePath, "r");
71
+ const eocd = findEndOfCentralDirectory(fd, size);
72
+ if (!eocd) {
73
+ debugSkip(filePath, "no end-of-central-directory record");
74
+ return undefined;
75
+ }
76
+ if (eocd.entryCount === 0xffff || eocd.centralDirOffset === 0xffffffff) {
77
+ debugSkip(filePath, "zip64 archive is not supported");
78
+ return undefined;
79
+ }
80
+ if (eocd.centralDirSize > MAX_CENTRAL_DIR_BYTES) {
81
+ debugSkip(filePath, "central directory exceeds the read cap");
82
+ return undefined;
83
+ }
84
+ const central = readRange(fd, eocd.centralDirOffset, eocd.centralDirSize);
85
+ if (!central) {
86
+ debugSkip(filePath, "could not read the central directory");
87
+ return undefined;
88
+ }
89
+ const entry = findManifestEntry(central, eocd.entryCount);
90
+ if (!entry) {
91
+ return undefined;
92
+ }
93
+ const data = readLocalEntry(fd, size, entry);
94
+ if (data === undefined) {
95
+ debugSkip(filePath, "could not inflate the manifest entry");
96
+ }
97
+ return data;
98
+ } catch (err) {
99
+ debugSkip(filePath, err?.message || "read error");
100
+ return undefined;
101
+ } finally {
102
+ if (fd !== undefined) {
103
+ try {
104
+ closeSync(fd);
105
+ } catch {
106
+ // ignore close failures
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ function debugSkip(filePath, reason) {
113
+ if (DEBUG_MODE) {
114
+ console.log(`Skipping MCPB bundle ${filePath}: ${reason}`);
115
+ }
116
+ }
117
+
118
+ function findEndOfCentralDirectory(fd, size) {
119
+ const scanLength = Math.min(size, MAX_EOCD_SCAN_BYTES);
120
+ const start = size - scanLength;
121
+ const tail = readRange(fd, start, scanLength);
122
+ if (!tail) {
123
+ return undefined;
124
+ }
125
+ for (let offset = tail.length - 22; offset >= 0; offset--) {
126
+ if (tail.readUInt32LE(offset) === EOCD_SIGNATURE) {
127
+ return {
128
+ entryCount: tail.readUInt16LE(offset + 10),
129
+ centralDirSize: tail.readUInt32LE(offset + 12),
130
+ centralDirOffset: tail.readUInt32LE(offset + 16),
131
+ };
132
+ }
133
+ }
134
+ return undefined;
135
+ }
136
+
137
+ function findManifestEntry(central, entryCount) {
138
+ let pointer = 0;
139
+ for (let index = 0; index < entryCount; index++) {
140
+ if (pointer + 46 > central.length) {
141
+ return undefined;
142
+ }
143
+ if (central.readUInt32LE(pointer) !== CENTRAL_SIGNATURE) {
144
+ return undefined;
145
+ }
146
+ const compressionMethod = central.readUInt16LE(pointer + 10);
147
+ const compressedSize = central.readUInt32LE(pointer + 20);
148
+ const uncompressedSize = central.readUInt32LE(pointer + 24);
149
+ const fileNameLength = central.readUInt16LE(pointer + 28);
150
+ const extraLength = central.readUInt16LE(pointer + 30);
151
+ const commentLength = central.readUInt16LE(pointer + 32);
152
+ const localHeaderOffset = central.readUInt32LE(pointer + 42);
153
+ const fileName = central
154
+ .subarray(pointer + 46, pointer + 46 + fileNameLength)
155
+ .toString("utf-8");
156
+ const normalizedName = fileName.replaceAll("\\", "/");
157
+ const isManifest =
158
+ normalizedName === "manifest.json" ||
159
+ normalizedName.endsWith("/manifest.json");
160
+ if (isManifest && uncompressedSize <= MAX_MANIFEST_BYTES) {
161
+ return { compressionMethod, compressedSize, localHeaderOffset };
162
+ }
163
+ pointer += 46 + fileNameLength + extraLength + commentLength;
164
+ }
165
+ return undefined;
166
+ }
167
+
168
+ function readLocalEntry(fd, size, entry) {
169
+ const { localHeaderOffset, compressionMethod, compressedSize } = entry;
170
+ // Bound the compressed read for every method, not just stored entries. The
171
+ // size comes from the archive's own central directory, so without this a
172
+ // hostile bundle could declare a multi-gigabyte entry and force that much to
173
+ // be allocated before `maxOutputLength` ever gets a chance to apply. A
174
+ // deflate stream that inflates to at most MAX_MANIFEST_BYTES cannot
175
+ // meaningfully exceed that compressed.
176
+ if (compressedSize > MAX_MANIFEST_BYTES) {
177
+ return undefined;
178
+ }
179
+ const header = readRange(fd, localHeaderOffset, 30);
180
+ if (!header || header.readUInt32LE(0) !== LOCAL_SIGNATURE) {
181
+ return undefined;
182
+ }
183
+ const fileNameLength = header.readUInt16LE(26);
184
+ const extraLength = header.readUInt16LE(28);
185
+ const dataStart = localHeaderOffset + 30 + fileNameLength + extraLength;
186
+ if (dataStart + compressedSize > size) {
187
+ return undefined;
188
+ }
189
+ const compressed = readRange(fd, dataStart, compressedSize);
190
+ if (!compressed) {
191
+ return undefined;
192
+ }
193
+ try {
194
+ if (compressionMethod === 0) {
195
+ return compressed.toString("utf-8");
196
+ }
197
+ if (compressionMethod === 8) {
198
+ // Bound the inflated output: the central-directory size field is
199
+ // attacker-controlled, so the real guard is the output length limit.
200
+ return inflateRawSync(compressed, {
201
+ maxOutputLength: MAX_MANIFEST_BYTES,
202
+ }).toString("utf-8");
203
+ }
204
+ } catch {
205
+ return undefined;
206
+ }
207
+ return undefined;
208
+ }
209
+
210
+ function addUniqueProperty(properties, name, value) {
211
+ const sanitizedValue = sanitizeBomPropertyValue(name, value);
212
+ if (
213
+ sanitizedValue === undefined ||
214
+ sanitizedValue === null ||
215
+ sanitizedValue === ""
216
+ ) {
217
+ return;
218
+ }
219
+ if (
220
+ properties.some(
221
+ (property) =>
222
+ property.name === name && property.value === String(sanitizedValue),
223
+ )
224
+ ) {
225
+ return;
226
+ }
227
+ properties.push({ name, value: String(sanitizedValue) });
228
+ }
229
+
230
+ function countCollection(value) {
231
+ if (Array.isArray(value)) {
232
+ return value.length;
233
+ }
234
+ if (value && typeof value === "object") {
235
+ return Object.keys(value).length;
236
+ }
237
+ return 0;
238
+ }
239
+
240
+ /**
241
+ * Count how many declared user-configuration fields look secret-bearing.
242
+ *
243
+ * The MCPB client injects `user_config` values as environment variables when it
244
+ * launches the bundled server, so secret-typed fields are a credential surface
245
+ * worth surfacing. Only field names and types are inspected; no user-provided
246
+ * values are read or emitted.
247
+ *
248
+ * @param {Object|Array} userConfig Declared user_config block
249
+ * @returns {{ fieldCount: number, secretFieldCount: number }} field counts
250
+ */
251
+ function summarizeUserConfig(userConfig) {
252
+ const entries = Array.isArray(userConfig)
253
+ ? userConfig.map((entry) => [entry?.key || entry?.name || "", entry])
254
+ : Object.entries(userConfig || {});
255
+ let secretFieldCount = 0;
256
+ for (const [key, definition] of entries) {
257
+ const type = String(definition?.type || "").toLowerCase();
258
+ const sensitive =
259
+ definition?.sensitive === true ||
260
+ type === "secret" ||
261
+ type === "password" ||
262
+ SECRET_FIELD_NAME_PATTERN.test(String(key));
263
+ if (sensitive) {
264
+ secretFieldCount++;
265
+ }
266
+ }
267
+ return { fieldCount: entries.length, secretFieldCount };
268
+ }
269
+
270
+ /**
271
+ * Count how many precompiled platform binaries a bundle declares across the
272
+ * `server.mcp_config.platform_overrides` / `compatibility.platforms` shapes
273
+ * used by MCPB manifests.
274
+ *
275
+ * @param {Object} manifest Parsed manifest.json
276
+ * @returns {number} platform binary count
277
+ */
278
+ function platformBinaryCount(manifest) {
279
+ const overrides =
280
+ manifest?.server?.mcp_config?.platform_overrides ||
281
+ manifest?.server?.platform_overrides;
282
+ if (overrides) {
283
+ return countCollection(overrides);
284
+ }
285
+ const platforms = manifest?.compatibility?.platforms || manifest?.platforms;
286
+ return countCollection(platforms);
287
+ }
288
+
289
+ /**
290
+ * Build a component from a parsed MCPB manifest.
291
+ *
292
+ * @param {string} filePath Bundle path
293
+ * @param {Object} manifest Parsed manifest.json
294
+ * @returns {Object} CycloneDX component
295
+ */
296
+ function componentFromManifest(filePath, manifest) {
297
+ const name =
298
+ (typeof manifest?.name === "string" && manifest.name) || basename(filePath);
299
+ const serverType = String(manifest?.server?.type || "").toLowerCase();
300
+ const userConfig = summarizeUserConfig(manifest?.user_config);
301
+ const properties = [
302
+ { name: "internal:SrcFile", value: filePath },
303
+ { name: "cdx:file:kind", value: "mcp-bundle" },
304
+ { name: "cdx:mcp:inventorySource", value: "mcp-bundle" },
305
+ // A bundle ships a whole local server plus its dependencies, so treat it as
306
+ // an install-time supply-chain surface that warrants review.
307
+ { name: "cdx:mcp:reviewNeeded", value: "true" },
308
+ ];
309
+ addUniqueProperty(
310
+ properties,
311
+ "cdx:mcpb:manifestVersion",
312
+ manifest?.manifest_version ? String(manifest.manifest_version) : undefined,
313
+ );
314
+ addUniqueProperty(properties, "cdx:mcpb:serverType", serverType || undefined);
315
+ addUniqueProperty(
316
+ properties,
317
+ "cdx:mcpb:toolCount",
318
+ String(countCollection(manifest?.tools)),
319
+ );
320
+ addUniqueProperty(
321
+ properties,
322
+ "cdx:mcpb:promptCount",
323
+ String(countCollection(manifest?.prompts)),
324
+ );
325
+ addUniqueProperty(
326
+ properties,
327
+ "cdx:mcpb:userConfigFieldCount",
328
+ String(userConfig.fieldCount),
329
+ );
330
+ if (userConfig.secretFieldCount) {
331
+ addUniqueProperty(
332
+ properties,
333
+ "cdx:mcpb:secretFieldCount",
334
+ String(userConfig.secretFieldCount),
335
+ );
336
+ addUniqueProperty(properties, "cdx:mcpb:injectsSecretEnv", "true");
337
+ }
338
+ const binaryCount = platformBinaryCount(manifest);
339
+ if (binaryCount) {
340
+ addUniqueProperty(
341
+ properties,
342
+ "cdx:mcpb:platformBinaryCount",
343
+ String(binaryCount),
344
+ );
345
+ }
346
+ const component = {
347
+ "bom-ref": `urn:mcp:bundle:${sanitizeMcpRefToken(name)}:${sanitizeMcpRefToken(basename(filePath))}`,
348
+ type: "application",
349
+ name,
350
+ properties,
351
+ };
352
+ if (manifest?.version) {
353
+ component.version = String(manifest.version);
354
+ }
355
+ // The description is attacker-controlled free text from an untrusted zip.
356
+ // Sanitize, truncate, and scan it for hidden Unicode like the sibling
357
+ // manifest/agent parsers do, rather than copying it verbatim.
358
+ if (typeof manifest?.description === "string") {
359
+ const sanitizedDescription = sanitizeBomPropertyValue(
360
+ "description",
361
+ manifest.description.slice(0, MAX_DESCRIPTION_BYTES),
362
+ );
363
+ if (sanitizedDescription) {
364
+ component.description = String(sanitizedDescription);
365
+ }
366
+ const hiddenUnicodeScan = scanTextForHiddenUnicode(manifest.description, {
367
+ syntax: "text",
368
+ });
369
+ if (hiddenUnicodeScan.hasHiddenUnicode) {
370
+ addUniqueProperty(
371
+ component.properties,
372
+ "cdx:file:hasHiddenUnicode",
373
+ "true",
374
+ );
375
+ addUniqueProperty(
376
+ component.properties,
377
+ "cdx:file:hiddenUnicodeCodePoints",
378
+ hiddenUnicodeScan.codePoints.join(","),
379
+ );
380
+ }
381
+ }
382
+ return component;
383
+ }
384
+
385
+ /**
386
+ * Parser for MCPB / DXT desktop-extension bundles.
387
+ *
388
+ * A bundle is a zip archive carrying a local MCP server, its dependencies, and
389
+ * a `manifest.json`. This parser reads only the manifest to describe the
390
+ * bundled server, the tools/prompts it exposes, the number of precompiled
391
+ * platform binaries, and whether it injects secret-typed configuration values
392
+ * as environment variables.
393
+ *
394
+ * @type {{id: string, patterns: string[], parse(files: string[], options?: Object): {components: Object[]}}}
395
+ */
396
+ export const mcpbBundleParser = {
397
+ id: "mcp-bundle",
398
+ patterns: MCPB_BUNDLE_PATTERNS,
399
+ parse(files, _options = {}) {
400
+ const components = [];
401
+ for (const filePath of [...new Set(files || [])]) {
402
+ const manifestText = readManifestFromBundle(filePath);
403
+ if (!manifestText) {
404
+ continue;
405
+ }
406
+ let manifest;
407
+ try {
408
+ manifest = JSON.parse(manifestText);
409
+ } catch {
410
+ continue;
411
+ }
412
+ if (!manifest || typeof manifest !== "object") {
413
+ continue;
414
+ }
415
+ components.push(componentFromManifest(filePath, manifest));
416
+ }
417
+ return { components };
418
+ },
419
+ };
@@ -288,6 +288,7 @@ export function getDistroInfo(root = "/") {
288
288
  purlType = "deb";
289
289
  break;
290
290
  case "alpine":
291
+ case "alpaquita":
291
292
  case "openwrt":
292
293
  purlType = "apk";
293
294
  break;
@@ -19,6 +19,7 @@ const PLUGIN_ENV_COMMAND_NAMES = {
19
19
  trivy: "TRIVY_CMD",
20
20
  trustinspector: "TRUSTINSPECTOR_CMD",
21
21
  cdxui: "CDXUI_CMD",
22
+ kosi: "KOSI_CMD",
22
23
  };
23
24
 
24
25
  function isMusl() {
@@ -41,6 +42,7 @@ function hasUsablePluginsDir(pluginsDir) {
41
42
  "trivy",
42
43
  "trustinspector",
43
44
  "cdxui",
45
+ "kosi",
44
46
  ].some((pluginName) => safeExistsSync(join(pluginsDir, pluginName))))
45
47
  );
46
48
  }
@@ -431,6 +433,7 @@ function resolveBundledPluginBinary(toolName, pluginRuntime) {
431
433
  case "rusi":
432
434
  case "cdxrs":
433
435
  case "cdxui":
436
+ case "kosi":
434
437
  return join(
435
438
  pluginRuntime.pluginsDir,
436
439
  toolName,
@@ -3,18 +3,19 @@ import { readFileSync } from "node:fs";
3
3
  import {
4
4
  bomStats,
5
5
  convertBom,
6
- createBom,
7
6
  decodeBomBinary,
8
- decodeBomJson,
9
7
  encodeBomBinary,
10
8
  encodeBomJson,
9
+ isSupportedSpecVersion,
11
10
  parseBomBinary,
12
- parseBomJson,
13
11
  supportedSpecVersions,
12
+ toBomMessage,
14
13
  } from "@cdxgen/cdx-proto";
14
+ import { isProtoBomFile } from "@cdxgen/cdx-proto/node";
15
15
 
16
16
  import { safeExistsSync, safeWriteSync } from "../core/fs.js";
17
- import { toCycloneDxSpecVersionString } from "./bomUtils.js";
17
+
18
+ export { isProtoBomFile };
18
19
 
19
20
  const JSON_READ_OPTIONS = {
20
21
  ignoreUnknownFields: true,
@@ -28,35 +29,8 @@ const BINARY_WRITE_OPTIONS = {
28
29
  writeUnknownFields: true,
29
30
  };
30
31
 
31
- const PROTO_BOM_FILE_EXTENSIONS = [".cdx", ".cdx.bin", ".proto"];
32
-
33
32
  const DEFAULT_SPEC_VERSION =
34
33
  supportedSpecVersions[supportedSpecVersions.length - 1];
35
- const PROTO_SUPPORTED_SPEC_VERSIONS = new Set(
36
- supportedSpecVersions.map((specVersion) =>
37
- toCycloneDxSpecVersionString(specVersion),
38
- ),
39
- );
40
-
41
- const isProtoMessageBom = (bom) =>
42
- Boolean(
43
- bom &&
44
- typeof bom === "object" &&
45
- !Array.isArray(bom) &&
46
- typeof bom.$typeName === "string" &&
47
- bom.specVersion,
48
- );
49
-
50
- const hasExplicitSpecVersion = (bomJson) =>
51
- Boolean(
52
- bomJson &&
53
- typeof bomJson === "object" &&
54
- !Array.isArray(bomJson) &&
55
- (bomJson.specVersion !== undefined || bomJson.spec_version !== undefined),
56
- );
57
-
58
- const resolveExplicitSpecVersion = (bomJson) =>
59
- bomJson?.specVersion ?? bomJson?.spec_version;
60
34
 
61
35
  const hasProvidedSpecVersion = (specVersion) =>
62
36
  specVersion !== undefined &&
@@ -66,19 +40,15 @@ const hasProvidedSpecVersion = (specVersion) =>
66
40
  /**
67
41
  * Determine whether a spec version is supported for protobuf serialization.
68
42
  *
43
+ * Delegates to cdx-proto's `isSupportedSpecVersion`, which accepts the
44
+ * spellings seen in the wild (`v1.6`, `1.6`, `1.6.0`) in addition to the
45
+ * canonical strings.
46
+ *
69
47
  * @param {string|undefined} specVersion CycloneDX spec version string
70
48
  * @returns {boolean} `true` when supported by `@cdxgen/cdx-proto`, or when no spec version is provided
71
49
  */
72
- export const isProtoSupportedSpecVersion = (specVersion) => {
73
- if (!hasProvidedSpecVersion(specVersion)) {
74
- return true;
75
- }
76
- const normalizedSpecVersion = toCycloneDxSpecVersionString(specVersion);
77
- return (
78
- normalizedSpecVersion !== undefined &&
79
- PROTO_SUPPORTED_SPEC_VERSIONS.has(normalizedSpecVersion)
80
- );
81
- };
50
+ export const isProtoSupportedSpecVersion = (specVersion) =>
51
+ !hasProvidedSpecVersion(specVersion) || isSupportedSpecVersion(specVersion);
82
52
 
83
53
  /**
84
54
  * Assert that a spec version is supported by `@cdxgen/cdx-proto`.
@@ -94,56 +64,42 @@ export const assertProtoSupportedSpecVersion = (
94
64
  specVersion,
95
65
  operation = "protobuf operations",
96
66
  ) => {
97
- if (!hasProvidedSpecVersion(specVersion)) {
98
- return;
99
- }
100
- const normalizedSpecVersion = toCycloneDxSpecVersionString(specVersion);
101
67
  if (isProtoSupportedSpecVersion(specVersion)) {
102
68
  return;
103
69
  }
104
- const displaySpecVersion = normalizedSpecVersion || `${specVersion}`.trim();
105
70
  throw new Error(
106
- `CycloneDX ${displaySpecVersion} is not currently supported for ${operation}. @cdxgen/cdx-proto supports ${supportedSpecVersions.join(", ")} only.`,
71
+ `CycloneDX ${`${specVersion}`.trim()} is not currently supported for ${operation}. @cdxgen/cdx-proto supports ${supportedSpecVersions.join(", ")} only.`,
107
72
  );
108
73
  };
109
74
 
110
- const resolveBomMessage = (bomJson, specVersion = DEFAULT_SPEC_VERSION) => {
111
- if (isProtoMessageBom(bomJson)) {
112
- return bomJson;
113
- }
114
- const parsedBomJson =
115
- typeof bomJson === "string" || bomJson instanceof String
116
- ? JSON.parse(`${bomJson}`)
117
- : bomJson;
118
- if (
119
- parsedBomJson &&
120
- typeof parsedBomJson === "object" &&
121
- !Array.isArray(parsedBomJson)
122
- ) {
123
- if (hasExplicitSpecVersion(parsedBomJson)) {
124
- assertProtoSupportedSpecVersion(
125
- resolveExplicitSpecVersion(parsedBomJson),
126
- "protobuf serialization",
127
- );
128
- return parseBomJson(parsedBomJson, JSON_READ_OPTIONS);
75
+ const resolveExplicitInputSpecVersion = (bomJson) => {
76
+ if (bomJson && typeof bomJson === "object" && !Array.isArray(bomJson)) {
77
+ const explicitSpecVersion = bomJson.specVersion ?? bomJson.spec_version;
78
+ if (hasProvidedSpecVersion(explicitSpecVersion)) {
79
+ return explicitSpecVersion;
129
80
  }
130
- assertProtoSupportedSpecVersion(specVersion, "protobuf serialization");
131
- return decodeBomJson(specVersion, parsedBomJson, JSON_READ_OPTIONS);
132
81
  }
133
- return createBom(specVersion);
82
+ return undefined;
134
83
  };
135
84
 
136
85
  /**
137
- * Determine whether a path looks like a CycloneDX protobuf file.
86
+ * Coerce a BOM input into a decoded message via cdx-proto's `toBomMessage`
87
+ * (messages pass through, JSON strings are parsed, canonical JSON objects are
88
+ * decoded, anything else yields an empty BOM). The version is asserted first
89
+ * so an unsupported BOM keeps failing with cdxgen's operation-specific error
90
+ * rather than cdx-proto's generic one.
138
91
  *
139
- * @param {string} filePath File path
140
- * @returns {boolean} true when the path looks like a protobuf BOM file
92
+ * @param {string | Object} bomJson BOM Json, BOM Json string, or proto message
93
+ * @param {string | number} specVersion CycloneDX spec version fallback for BOMs without specVersion
94
+ * @param {string} operation Operation label used in the error message
95
+ * @returns {Object} Decoded BOM message
141
96
  */
142
- export const isProtoBomFile = (filePath) => {
143
- const normalizedPath = `${filePath || ""}`.toLowerCase();
144
- return PROTO_BOM_FILE_EXTENSIONS.some((extension) =>
145
- normalizedPath.endsWith(extension),
97
+ const toSerializableBomMessage = (bomJson, specVersion, operation) => {
98
+ assertProtoSupportedSpecVersion(
99
+ resolveExplicitInputSpecVersion(bomJson) ?? specVersion,
100
+ operation,
146
101
  );
102
+ return toBomMessage(bomJson, specVersion, JSON_READ_OPTIONS);
147
103
  };
148
104
 
149
105
  /**
@@ -159,7 +115,11 @@ export const writeBinary = (
159
115
  specVersion = DEFAULT_SPEC_VERSION,
160
116
  ) => {
161
117
  if (bomJson && binFile) {
162
- const bomMessage = resolveBomMessage(bomJson, specVersion);
118
+ const bomMessage = toSerializableBomMessage(
119
+ bomJson,
120
+ specVersion,
121
+ "protobuf serialization",
122
+ );
163
123
  safeWriteSync(binFile, encodeBomBinary(bomMessage, BINARY_WRITE_OPTIONS));
164
124
  }
165
125
  };
@@ -196,19 +156,27 @@ export const readBinary = (binFile, asJson, specVersion) => {
196
156
  * @returns {Object} Component and dependency counts with JSON/binary byte sizes and compression ratio
197
157
  */
198
158
  export const getBomStats = (bomJson, specVersion = DEFAULT_SPEC_VERSION) =>
199
- bomStats(resolveBomMessage(bomJson, specVersion));
159
+ bomStats(
160
+ toSerializableBomMessage(bomJson, specVersion, "protobuf statistics"),
161
+ );
200
162
 
201
163
  /**
202
164
  * Method to cross-convert a BOM between CycloneDX specification versions using
203
165
  * the protobuf schemas.
204
166
  *
205
- * Downgrades are lossy: fields that the target version does not define are
206
- * dropped and reported in `warnings` as field paths.
167
+ * cdx-proto's conversion is cardinality-safe: fields that change shape between
168
+ * versions (`metadata.licenses` and `evidence.identity`, singular in 1.5 and an
169
+ * array since 1.6) are reshaped automatically — wrapped on upgrade, collapsed
170
+ * to their first entry on downgrade — including `evidence.identity` on nested
171
+ * components, `metadata.component`, `metadata.tools.components[]`, and
172
+ * `formulation[].components[]`. Downgrades are otherwise lossy: fields the
173
+ * target version does not define are dropped and reported in `warnings` as
174
+ * field paths, alongside collapsed list siblings.
207
175
  *
208
- * This is the raw `@cdxgen/cdx-proto` conversion. It reshapes nothing that the
209
- * two schemas model with different cardinality, so a BOM crossing the 1.5/1.6
210
- * `evidence.identity` boundary must be normalized first. Prefer
211
- * `applySpecVersionCompatibility` from the postgen stage for that.
176
+ * Only the protobuf-supported versions (1.5–1.7) can be targeted here. For the
177
+ * full JSON-level normalization across every version cdxgen emits (component
178
+ * types, license attributes, 1.4/2.0 reshaping), prefer
179
+ * `applySpecVersionCompatibility` from the postgen stage.
212
180
  *
213
181
  * @param {string | Object} bomJson BOM Json, BOM Json string, or proto message
214
182
  * @param {string | number} targetSpecVersion Target CycloneDX spec version
@@ -221,11 +189,9 @@ export const convertBomSpecVersion = (
221
189
  specVersion = DEFAULT_SPEC_VERSION,
222
190
  ) => {
223
191
  assertProtoSupportedSpecVersion(targetSpecVersion, "protobuf conversion");
224
- const normalizedTargetSpecVersion =
225
- toCycloneDxSpecVersionString(targetSpecVersion) || `${targetSpecVersion}`;
226
192
  const { bom, warnings } = convertBom(
227
- resolveBomMessage(bomJson, specVersion),
228
- normalizedTargetSpecVersion,
193
+ toSerializableBomMessage(bomJson, specVersion, "protobuf conversion"),
194
+ targetSpecVersion,
229
195
  );
230
196
  return { bomJson: encodeBomJson(bom), warnings };
231
197
  };
@@ -1,3 +1,7 @@
1
+ // Kept local (not imported from `@cdxgen/cdx-proto/node`) because this module
2
+ // gates whether the optional protobuf dependency is loaded at all. The
3
+ // authoritative list is `protoBomFileExtensions` in `@cdxgen/cdx-proto/node`;
4
+ // keep the two in sync.
1
5
  const PROTO_BOM_FILE_EXTENSIONS = [".cdx", ".cdx.bin", ".proto"];
2
6
 
3
7
  /**
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { basename, dirname, join } from "node:path";
3
3
 
4
- import toml from "@iarna/toml";
4
+ import * as toml from "smol-toml";
5
5
 
6
6
  import { safeExistsSync } from "../core/fs.js";
7
7