@cdxgen/cdxgen 13.1.0 → 13.3.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 (283) hide show
  1. package/README.md +32 -15
  2. package/bin/cdxgen.js +243 -122
  3. package/bin/evinse.js +7 -0
  4. package/bin/sign.js +88 -14
  5. package/bin/tracebom.js +64 -4
  6. package/bin/validate.js +29 -7
  7. package/bin/verify.js +79 -39
  8. package/data/crypto-oid.json +88 -12
  9. package/data/remediations.json +47 -0
  10. package/data/rules/ai-bom.yaml +78 -0
  11. package/data/rules/cargo-rust.yaml +132 -0
  12. package/data/rules/mcp-servers.yaml +122 -0
  13. package/lib/audit/targets.js +6 -1
  14. package/lib/cli/bomAssembly.js +117 -15
  15. package/lib/cli/cliOptions.js +93 -1
  16. package/lib/cli/index.js +235 -5
  17. package/lib/cli/jsBom.js +108 -26
  18. package/lib/cli/jvmBom.js +68 -12
  19. package/lib/cli/managedBom.js +264 -41
  20. package/lib/cli/nativeBom.js +923 -15
  21. package/lib/core/activity.js +120 -2
  22. package/lib/core/env.js +44 -7
  23. package/lib/core/fs.js +174 -6
  24. package/lib/core/metadataProperties.js +97 -0
  25. package/lib/core/projectConfig.js +31 -4
  26. package/lib/core/userAgent.js +151 -0
  27. package/lib/ecosystems/bunutils.js +269 -101
  28. package/lib/ecosystems/cargoCache.js +364 -0
  29. package/lib/ecosystems/caxa.js +447 -7
  30. package/lib/ecosystems/cmakeResolver.js +66 -0
  31. package/lib/ecosystems/cppEvidence.js +104 -33
  32. package/lib/ecosystems/ecosystems.js +260 -99
  33. package/lib/ecosystems/golem.js +11 -1
  34. package/lib/ecosystems/gradleutils.js +185 -9
  35. package/lib/ecosystems/jsEvidence.js +19 -20
  36. package/lib/ecosystems/kosi.js +1263 -0
  37. package/lib/ecosystems/npmutils.js +110 -9
  38. package/lib/ecosystems/parsers-crystal.js +179 -0
  39. package/lib/ecosystems/parsers-dotnet.js +120 -49
  40. package/lib/ecosystems/parsers-elm.js +816 -0
  41. package/lib/ecosystems/parsers-gleam.js +1 -1
  42. package/lib/ecosystems/parsers-go.js +47 -9
  43. package/lib/ecosystems/parsers-js.js +171 -50
  44. package/lib/ecosystems/parsers-julia.js +268 -0
  45. package/lib/ecosystems/parsers-lua.js +218 -0
  46. package/lib/ecosystems/parsers-misc.js +919 -161
  47. package/lib/ecosystems/parsers-nim.js +221 -0
  48. package/lib/ecosystems/parsers-opam.js +180 -0
  49. package/lib/ecosystems/parsers-perl.js +277 -0
  50. package/lib/ecosystems/parsers-python.js +27 -15
  51. package/lib/ecosystems/parsers-r.js +148 -0
  52. package/lib/ecosystems/parsers-rebar.js +411 -0
  53. package/lib/ecosystems/parsers-rust.js +954 -33
  54. package/lib/ecosystems/parsers-spack.js +204 -0
  55. package/lib/ecosystems/parsers-stack.js +264 -0
  56. package/lib/ecosystems/parsers-terraform.js +194 -0
  57. package/lib/ecosystems/rubyutils.js +1426 -166
  58. package/lib/ecosystems/rusi.js +158 -7
  59. package/lib/evinser/evinser.js +547 -129
  60. package/lib/evinser/swiftsem.js +1914 -302
  61. package/lib/helpers/agenticToolutils.js +925 -0
  62. package/lib/helpers/bomSigner.js +652 -221
  63. package/lib/inventory/agentFormulationParser.js +57 -4
  64. package/lib/inventory/aiCollector.js +504 -10
  65. package/lib/inventory/aiInventory.js +12 -0
  66. package/lib/inventory/analyzer.js +488 -30
  67. package/lib/inventory/astWorker.js +6 -0
  68. package/lib/inventory/atomUtils.js +568 -31
  69. package/lib/inventory/auditCategories.js +8 -0
  70. package/lib/inventory/cbomutils.js +30 -0
  71. package/lib/inventory/cdxrs.js +110 -14
  72. package/lib/inventory/communityAiConfigParser.js +215 -2
  73. package/lib/inventory/depsUtils.js +183 -11
  74. package/lib/inventory/display.js +4 -5
  75. package/lib/inventory/dosai.js +627 -13
  76. package/lib/inventory/dynamic.js +39 -1
  77. package/lib/inventory/envcontext.js +9 -6
  78. package/lib/inventory/evidenceUtils.js +26 -5
  79. package/lib/inventory/fetchBatch.js +190 -19
  80. package/lib/inventory/fetchRate.js +74 -11
  81. package/lib/inventory/forgeEnricher.js +3 -2
  82. package/lib/inventory/jvmToolEnv.js +6 -5
  83. package/lib/inventory/mcpConfigParser.js +166 -2
  84. package/lib/inventory/mcpDiscovery.js +11 -0
  85. package/lib/inventory/mcpServerManifest.js +389 -0
  86. package/lib/inventory/mcpbBundle.js +419 -0
  87. package/lib/inventory/plugins.js +3 -0
  88. package/lib/inventory/protobom.js +62 -89
  89. package/lib/inventory/protobomLoader.js +4 -0
  90. package/lib/inventory/purl.js +152 -3
  91. package/lib/inventory/rustFormulationParser.js +1 -1
  92. package/lib/inventory/sbomDocument.js +2 -70
  93. package/lib/inventory/source.js +47 -23
  94. package/lib/inventory/stylePackageRefs.js +41 -0
  95. package/lib/inventory/svelteUtils.js +524 -0
  96. package/lib/inventory/traceRunner.js +210 -21
  97. package/lib/managers/binary.js +115 -24
  98. package/lib/managers/docker.js +440 -20
  99. package/lib/managers/oci.js +39 -1
  100. package/lib/parsers/cmakeLists.js +452 -0
  101. package/lib/parsers/huggingfaceUtils.js +15 -7
  102. package/lib/parsers/largeJson.js +454 -0
  103. package/lib/parsers/npmrc.js +19 -1
  104. package/lib/parsers/onnx.js +294 -0
  105. package/lib/parsers/safetensors.js +114 -0
  106. package/lib/stages/postgen/introspection/markers.js +29 -6
  107. package/lib/stages/postgen/postgen.js +237 -16
  108. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  109. package/lib/validator/bomValidator.js +6 -16
  110. package/lib/validator/index.js +48 -10
  111. package/lib/validator/reporters/console.js +3 -0
  112. package/lib/validator/reporters/json.js +1 -0
  113. package/lib/validator/reporters/sarif.js +7 -0
  114. package/package.json +27 -27
  115. package/types/lib/audit/targets.d.ts.map +1 -1
  116. package/types/lib/cli/bomAssembly.d.ts +2 -1
  117. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  118. package/types/lib/cli/cliOptions.d.ts +33 -1
  119. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  120. package/types/lib/cli/index.d.ts +2 -2
  121. package/types/lib/cli/index.d.ts.map +1 -1
  122. package/types/lib/cli/jsBom.d.ts +23 -0
  123. package/types/lib/cli/jsBom.d.ts.map +1 -1
  124. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  125. package/types/lib/cli/managedBom.d.ts +33 -0
  126. package/types/lib/cli/managedBom.d.ts.map +1 -1
  127. package/types/lib/cli/nativeBom.d.ts +88 -8
  128. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  129. package/types/lib/core/activity.d.ts.map +1 -1
  130. package/types/lib/core/env.d.ts +21 -0
  131. package/types/lib/core/env.d.ts.map +1 -1
  132. package/types/lib/core/fs.d.ts +55 -0
  133. package/types/lib/core/fs.d.ts.map +1 -1
  134. package/types/lib/core/metadataProperties.d.ts +39 -0
  135. package/types/lib/core/metadataProperties.d.ts.map +1 -0
  136. package/types/lib/core/projectConfig.d.ts +6 -2
  137. package/types/lib/core/projectConfig.d.ts.map +1 -1
  138. package/types/lib/core/userAgent.d.ts +56 -0
  139. package/types/lib/core/userAgent.d.ts.map +1 -0
  140. package/types/lib/ecosystems/bunutils.d.ts +12 -5
  141. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  142. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  143. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  144. package/types/lib/ecosystems/caxa.d.ts +14 -0
  145. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  146. package/types/lib/ecosystems/cmakeResolver.d.ts +23 -0
  147. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  148. package/types/lib/ecosystems/cppEvidence.d.ts +2 -2
  149. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  150. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  151. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  152. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  153. package/types/lib/ecosystems/gradleutils.d.ts +40 -4
  154. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  155. package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
  156. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  157. package/types/lib/ecosystems/kosi.d.ts +86 -0
  158. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  159. package/types/lib/ecosystems/npmutils.d.ts +42 -0
  160. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  161. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  162. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  163. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  164. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  165. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  166. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  167. package/types/lib/ecosystems/parsers-go.d.ts +10 -0
  168. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  169. package/types/lib/ecosystems/parsers-js.d.ts +10 -4
  170. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  171. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  172. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  173. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  174. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  175. package/types/lib/ecosystems/parsers-misc.d.ts +90 -3
  176. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  177. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  178. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  179. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  180. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  181. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  182. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  183. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  184. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  185. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  186. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  187. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  188. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  189. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  190. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  191. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  192. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  193. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  194. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  195. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  196. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  197. package/types/lib/ecosystems/rubyutils.d.ts +143 -2
  198. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  199. package/types/lib/ecosystems/rusi.d.ts +13 -0
  200. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  201. package/types/lib/evinser/evinser.d.ts +81 -2
  202. package/types/lib/evinser/evinser.d.ts.map +1 -1
  203. package/types/lib/evinser/swiftsem.d.ts +280 -15
  204. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  205. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  206. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  207. package/types/lib/helpers/bomSigner.d.ts +106 -13
  208. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  209. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  210. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  211. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  212. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  213. package/types/lib/inventory/atomUtils.d.ts +145 -3
  214. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  215. package/types/lib/inventory/auditCategories.d.ts +4 -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/cdxrs.d.ts +8 -3
  219. package/types/lib/inventory/cdxrs.d.ts.map +1 -1
  220. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  221. package/types/lib/inventory/depsUtils.d.ts +15 -0
  222. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  223. package/types/lib/inventory/display.d.ts.map +1 -1
  224. package/types/lib/inventory/dosai.d.ts +87 -1
  225. package/types/lib/inventory/dosai.d.ts.map +1 -1
  226. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  227. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  228. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  229. package/types/lib/inventory/fetchBatch.d.ts +34 -0
  230. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  231. package/types/lib/inventory/fetchRate.d.ts +9 -6
  232. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  233. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  234. package/types/lib/inventory/jvmToolEnv.d.ts +5 -4
  235. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  236. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  237. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  238. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  239. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  240. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  241. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  242. package/types/lib/inventory/plugins.d.ts.map +1 -1
  243. package/types/lib/inventory/protobom.d.ts +18 -13
  244. package/types/lib/inventory/protobom.d.ts.map +1 -1
  245. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  246. package/types/lib/inventory/purl.d.ts +71 -2
  247. package/types/lib/inventory/purl.d.ts.map +1 -1
  248. package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
  249. package/types/lib/inventory/source.d.ts.map +1 -1
  250. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  251. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  252. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  253. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  254. package/types/lib/inventory/traceRunner.d.ts +47 -1
  255. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  256. package/types/lib/managers/binary.d.ts +30 -1
  257. package/types/lib/managers/binary.d.ts.map +1 -1
  258. package/types/lib/managers/docker.d.ts +29 -0
  259. package/types/lib/managers/docker.d.ts.map +1 -1
  260. package/types/lib/managers/oci.d.ts +14 -0
  261. package/types/lib/managers/oci.d.ts.map +1 -1
  262. package/types/lib/parsers/cmakeLists.d.ts +76 -0
  263. package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
  264. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  265. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  266. package/types/lib/parsers/largeJson.d.ts +52 -0
  267. package/types/lib/parsers/largeJson.d.ts.map +1 -0
  268. package/types/lib/parsers/npmrc.d.ts +5 -0
  269. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  270. package/types/lib/parsers/onnx.d.ts +28 -0
  271. package/types/lib/parsers/onnx.d.ts.map +1 -0
  272. package/types/lib/parsers/safetensors.d.ts +22 -0
  273. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  274. package/types/lib/stages/postgen/introspection/markers.d.ts +3 -2
  275. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -1
  276. package/types/lib/stages/postgen/postgen.d.ts +33 -0
  277. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  278. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  279. package/types/lib/validator/index.d.ts +10 -1
  280. package/types/lib/validator/index.d.ts.map +1 -1
  281. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  282. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  283. package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
@@ -3,7 +3,7 @@ import process from "node:process";
3
3
 
4
4
  import { createHttpClient, getDefaultHttpTimeoutMs } from "./httpClient.js";
5
5
  import { traceLog } from "./logger.js";
6
- import { CDXGEN_VERSION } from "./state.js";
6
+ import { getUserAgent } from "./userAgent.js";
7
7
 
8
8
  /** True when CDXGEN_DEBUG_MODE or SCAN_DEBUG_MODE requests debug/verbose output. */
9
9
  export const DEBUG_MODE =
@@ -85,6 +85,30 @@ const LOCKFILE_ACTIVITY_HINTS = new Map([
85
85
  label: "Bundler lockfile",
86
86
  },
87
87
  ],
88
+ [
89
+ "cpanfile.snapshot",
90
+ {
91
+ classification: "lockfile",
92
+ ecosystem: "cpan",
93
+ label: "Carton snapshot",
94
+ },
95
+ ],
96
+ [
97
+ "manifest.toml",
98
+ {
99
+ classification: "lockfile",
100
+ ecosystem: "gleam",
101
+ label: "Gleam or Julia lockfile",
102
+ },
103
+ ],
104
+ [
105
+ "nimble.lock",
106
+ {
107
+ classification: "lockfile",
108
+ ecosystem: "nim",
109
+ label: "Nimble lockfile",
110
+ },
111
+ ],
88
112
  [
89
113
  "package-lock.json",
90
114
  { classification: "lockfile", ecosystem: "npm", label: "npm lockfile" },
@@ -125,6 +149,62 @@ const LOCKFILE_ACTIVITY_HINTS = new Map([
125
149
  label: "PEP 751 lockfile",
126
150
  },
127
151
  ],
152
+ [
153
+ "renv.lock",
154
+ {
155
+ classification: "lockfile",
156
+ ecosystem: "cran",
157
+ label: "renv lockfile",
158
+ },
159
+ ],
160
+ [
161
+ "rebar.lock",
162
+ {
163
+ classification: "lockfile",
164
+ ecosystem: "hex",
165
+ label: "rebar3 lockfile",
166
+ },
167
+ ],
168
+ [
169
+ "shard.lock",
170
+ {
171
+ classification: "lockfile",
172
+ ecosystem: "crystal",
173
+ label: "Crystal shards lockfile",
174
+ },
175
+ ],
176
+ [
177
+ "spack.lock",
178
+ {
179
+ classification: "lockfile",
180
+ ecosystem: "spack",
181
+ label: "Spack lockfile",
182
+ },
183
+ ],
184
+ [
185
+ "stack.lock",
186
+ {
187
+ classification: "lockfile",
188
+ ecosystem: "hackage",
189
+ label: "Stack lockfile",
190
+ },
191
+ ],
192
+ [
193
+ "stack.yaml.lock",
194
+ {
195
+ classification: "lockfile",
196
+ ecosystem: "hackage",
197
+ label: "Stack lockfile",
198
+ },
199
+ ],
200
+ [
201
+ ".terraform.lock.hcl",
202
+ {
203
+ classification: "lockfile",
204
+ ecosystem: "terraform",
205
+ label: "Terraform provider lockfile",
206
+ },
207
+ ],
128
208
  [
129
209
  "uv.lock",
130
210
  { classification: "lockfile", ecosystem: "python", label: "uv lockfile" },
@@ -167,6 +247,18 @@ const MANIFEST_ACTIVITY_HINTS = new Map([
167
247
  "pom.xml",
168
248
  { classification: "manifest", ecosystem: "maven", label: "Maven manifest" },
169
249
  ],
250
+ [
251
+ "cpanfile",
252
+ { classification: "manifest", ecosystem: "cpan", label: "CPAN manifest" },
253
+ ],
254
+ [
255
+ "project.toml",
256
+ {
257
+ classification: "manifest",
258
+ ecosystem: "julia",
259
+ label: "Julia project manifest",
260
+ },
261
+ ],
170
262
  [
171
263
  "pyproject.toml",
172
264
  {
@@ -942,10 +1034,35 @@ function inferNetworkIntent(requestUrl) {
942
1034
  return "metadata-fetch";
943
1035
  }
944
1036
 
1037
+ /**
1038
+ * Set the request's user-agent to the value resolved for its destination host,
1039
+ * so that a `CDXGEN_USER_AGENT_<REGISTRY>` override reaches the registry it
1040
+ * names. A user-agent a caller set explicitly is left alone; only the client's
1041
+ * own default, which carries no host context, is replaced.
1042
+ *
1043
+ * @param {Object} options Request options being prepared by the HTTP client.
1044
+ * @returns {void}
1045
+ */
1046
+ function applyUserAgent(options) {
1047
+ const headers = options.headers || (options.headers = {});
1048
+ const existingKey = Object.keys(headers).find(
1049
+ (name) => name.toLowerCase() === "user-agent",
1050
+ );
1051
+ if (existingKey && headers[existingKey] !== getUserAgent()) {
1052
+ return;
1053
+ }
1054
+ if (existingKey) {
1055
+ delete headers[existingKey];
1056
+ }
1057
+ headers["user-agent"] = getUserAgent(options.url?.hostname);
1058
+ }
1059
+
945
1060
  /** The shared got-compatible HTTP client for all outbound requests, enforcing dry-run, host-allow-list, and secure-mode policies via beforeRequest/afterResponse/beforeError hooks. */
946
1061
  export const cdxgenAgent = createHttpClient({
947
1062
  headers: {
948
- "user-agent": `@CycloneDX/cdxgen ${CDXGEN_VERSION}`,
1063
+ // Per-host overrides are applied in beforeRequest; this is the fallback for
1064
+ // any code path that reads the client's configured headers directly.
1065
+ "user-agent": getUserAgent(),
949
1066
  },
950
1067
  // Bound every request so an unresponsive host cannot hang a scan. Overridable
951
1068
  // via CDXGEN_HTTP_TIMEOUT_MS; individual calls may pass a smaller `timeout`.
@@ -959,6 +1076,7 @@ export const cdxgenAgent = createHttpClient({
959
1076
  (options) => {
960
1077
  const networkIntent =
961
1078
  options.context?.activityIntent || inferNetworkIntent(options.url);
1079
+ applyUserAgent(options);
962
1080
  const allowedHostsEnv = readEnvironmentVariable("CDXGEN_ALLOWED_HOSTS");
963
1081
  const hostAllowed = isAllowedHttpHost(
964
1082
  options.url.hostname,
package/lib/core/env.js CHANGED
@@ -256,6 +256,19 @@ if (readEnvironmentVariable("CARGO_CMD")) {
256
256
  CARGO_CMD = readEnvironmentVariable("CARGO_CMD");
257
257
  }
258
258
 
259
+ /**
260
+ * Target triple the cargo resolve graph is filtered to, so that a
261
+ * cross-compiled project can be described for the triple it ships on rather
262
+ * than for the union of every triple its lock file covers.
263
+ *
264
+ * `CDXGEN_CARGO_TARGET` is cdxgen's own name for it; `CARGO_BUILD_TARGET` is
265
+ * cargo's standard variable, which a cross-compiling build already sets.
266
+ */
267
+ export const CARGO_TARGET =
268
+ readEnvironmentVariable("CDXGEN_CARGO_TARGET") ||
269
+ readEnvironmentVariable("CARGO_BUILD_TARGET") ||
270
+ undefined;
271
+
259
272
  /** Resolved Clojure CLI executable command (CLJ_CMD env var, or "clj"). */
260
273
  export let CLJ_CMD = "clj";
261
274
  if (readEnvironmentVariable("CLJ_CMD")) {
@@ -307,6 +320,7 @@ export const PROJECT_TYPE_ALIASES = {
307
320
  "java24",
308
321
  "java25",
309
322
  "java26",
323
+ "java27",
310
324
  "groovy",
311
325
  "kotlin",
312
326
  "kt",
@@ -339,6 +353,9 @@ export const PROJECT_TYPE_ALIASES = {
339
353
  "nodejs20",
340
354
  "nodejs22",
341
355
  "nodejs23",
356
+ "nodejs24",
357
+ "nodejs25",
358
+ "nodejs26",
342
359
  "node",
343
360
  "node8",
344
361
  "node10",
@@ -349,11 +366,16 @@ export const PROJECT_TYPE_ALIASES = {
349
366
  "node20",
350
367
  "node22",
351
368
  "node23",
369
+ "node24",
370
+ "node25",
371
+ "node26",
352
372
  "js",
353
373
  "javascript",
354
374
  "typescript",
355
375
  "ts",
356
376
  "tsx",
377
+ "svelte",
378
+ "sveltekit",
357
379
  "yarn",
358
380
  "rush",
359
381
  "bun",
@@ -361,6 +383,7 @@ export const PROJECT_TYPE_ALIASES = {
361
383
  ],
362
384
  mcp: ["mcp"],
363
385
  "ai-skill": ["ai-skill", "skill", "skills"],
386
+ agentic: ["agentic", "agentic-cli", "ai-tools", "ai-cli"],
364
387
  py: [
365
388
  "py",
366
389
  "python",
@@ -372,6 +395,8 @@ export const PROJECT_TYPE_ALIASES = {
372
395
  "python311",
373
396
  "python312",
374
397
  "python313",
398
+ "python314",
399
+ "python315",
375
400
  "pixi",
376
401
  "pip",
377
402
  "poetry",
@@ -398,6 +423,8 @@ export const PROJECT_TYPE_ALIASES = {
398
423
  "dotnet7",
399
424
  "dotnet8",
400
425
  "dotnet9",
426
+ "dotnet10",
427
+ "dotnet11",
401
428
  "dotnet-framework",
402
429
  "dotnet-framework47",
403
430
  "dotnet-framework48",
@@ -417,8 +444,19 @@ export const PROJECT_TYPE_ALIASES = {
417
444
  "hmiproj",
418
445
  ],
419
446
  dart: ["dart", "flutter", "pub"],
420
- haskell: ["haskell", "hackage", "cabal"],
447
+ haskell: ["haskell", "hackage", "cabal", "stack", "stackage"],
421
448
  elixir: ["elixir", "hex", "mix"],
449
+ erlang: ["erlang", "rebar", "rebar3"],
450
+ julia: ["julia"],
451
+ r: ["r", "cran", "renv"],
452
+ ocaml: ["ocaml", "opam", "dune"],
453
+ elm: ["elm"],
454
+ crystal: ["crystal", "shards"],
455
+ nim: ["nim", "nimble"],
456
+ lua: ["lua", "luarocks"],
457
+ perl: ["perl", "cpan", "carton", "cpanfile"],
458
+ spack: ["spack"],
459
+ terraform: ["terraform", "opentofu", "tf"],
422
460
  c: ["c", "cpp", "c++", "conan", "collider"],
423
461
  clojure: ["clojure", "edn", "clj", "leiningen"],
424
462
  github: ["github", "actions"],
@@ -453,7 +491,7 @@ export const PROJECT_TYPE_ALIASES = {
453
491
  "visionos",
454
492
  ],
455
493
  binary: ["binary", "blint"],
456
- oci: ["docker", "oci", "container", "podman", "rootfs", "oci-dir"],
494
+ oci: ["docker", "image", "oci", "container", "podman", "rootfs", "oci-dir"],
457
495
  cocoa: ["cocoa", "cocoapods", "objective-c", "swift", "ios"],
458
496
  scala: ["scala", "scala3", "sbt", "mill"],
459
497
  nix: ["nix", "nixos", "flake"],
@@ -605,11 +643,10 @@ export function hasAnyProjectType(projectTypes, options, defaultStatus = true) {
605
643
  const allProjectTypes = [...projectTypes];
606
644
  // Convert the project types into base types
607
645
  const baseProjectTypes = [];
608
- // Support for arbitrary versioned ruby type
609
- if (
610
- options.projectType?.length &&
611
- projectTypes.filter((p) => p.startsWith("ruby")).length
612
- ) {
646
+ // Support for arbitrary versioned ruby type: a requested ruby3.4.1 is a
647
+ // ruby project. The test is on the requested types; testing the queried
648
+ // ones made every scan a ruby scan whenever "ruby" was asked about.
649
+ if (options.projectType?.some((p) => p.startsWith("ruby"))) {
613
650
  baseProjectTypes.push("ruby");
614
651
  }
615
652
  // Versioned JVM build tool types such as maven3.9.9 or gradle8.14 are java
package/lib/core/fs.js CHANGED
@@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import {
5
5
  accessSync,
6
+ closeSync,
6
7
  copyFileSync,
7
8
  createReadStream,
8
9
  existsSync,
@@ -10,10 +11,14 @@ import {
10
11
  lstatSync,
11
12
  mkdirSync,
12
13
  mkdtempSync,
14
+ openSync,
13
15
  readFileSync,
16
+ renameSync,
14
17
  rmSync,
18
+ statSync,
15
19
  unlinkSync,
16
20
  writeFileSync,
21
+ writeSync,
17
22
  } from "node:fs";
18
23
  import { tmpdir } from "node:os";
19
24
  import path, { basename, dirname, join, resolve } from "node:path";
@@ -144,6 +149,53 @@ export function safeIsExecutableSync(filePath) {
144
149
  * @returns {void}
145
150
  */
146
151
  export function safeWriteSync(filePath, data, options) {
152
+ if (isWriteBlocked(filePath)) {
153
+ return undefined;
154
+ }
155
+ writeFileSync(filePath, data, options);
156
+ recordFilesystemActivity("write", filePath, "completed");
157
+ }
158
+
159
+ /**
160
+ * Permission- and dry-run-aware write of a sequence of chunks, for content too
161
+ * large to join into one string first. The chunks go to a temporary file next
162
+ * to the target, which is renamed into place once complete, so a chunk may be
163
+ * read from the file being replaced and a failed write leaves the target as it
164
+ * was.
165
+ *
166
+ * @param {string} filePath File path to write.
167
+ * @param {Iterable<string|Buffer>} chunks Chunks written in order.
168
+ * @returns {void}
169
+ */
170
+ export function safeWriteChunksSync(filePath, chunks) {
171
+ if (isWriteBlocked(filePath)) {
172
+ return undefined;
173
+ }
174
+ const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
175
+ const fd = openSync(tempPath, "w");
176
+ try {
177
+ for (const chunk of chunks) {
178
+ const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
179
+ let written = 0;
180
+ while (written < bytes.length) {
181
+ written += writeSync(fd, bytes, written);
182
+ }
183
+ }
184
+ closeSync(fd);
185
+ renameSync(tempPath, filePath);
186
+ } catch (err) {
187
+ try {
188
+ closeSync(fd);
189
+ } catch {
190
+ // Already closed before the rename failed.
191
+ }
192
+ rmSync(tempPath, { force: true });
193
+ throw err;
194
+ }
195
+ recordFilesystemActivity("write", filePath, "completed");
196
+ }
197
+
198
+ function isWriteBlocked(filePath) {
147
199
  if (isDryRun) {
148
200
  recordFilesystemActivity(
149
201
  "write",
@@ -151,7 +203,7 @@ export function safeWriteSync(filePath, data, options) {
151
203
  "blocked",
152
204
  "Dry run mode blocks filesystem writes.",
153
205
  );
154
- return undefined;
206
+ return true;
155
207
  }
156
208
  if (!hasWritePermission(filePath)) {
157
209
  if (DEBUG_MODE) {
@@ -163,10 +215,9 @@ export function safeWriteSync(filePath, data, options) {
163
215
  "blocked",
164
216
  "cdxgen lacks write permission for this path.",
165
217
  );
166
- return undefined;
218
+ return true;
167
219
  }
168
- writeFileSync(filePath, data, options);
169
- recordFilesystemActivity("write", filePath, "completed");
220
+ return false;
170
221
  }
171
222
 
172
223
  /**
@@ -483,6 +534,116 @@ const VERSION_PROBE_ARGS = new Set(["--version", "-version", "version"]);
483
534
  const POSIX_SHELL_METACHARACTERS = /[;&|<>$`\\\n\r]/;
484
535
  const WINDOWS_SHELL_METACHARACTERS = /[&|<>^%\n\r]/;
485
536
 
537
+ /**
538
+ * Quote one argument of a shell command line that is built as a single
539
+ * string. The shell flag exists so cmd.exe can resolve bare command names,
540
+ * `.cmd` shims and scripts, and every argument is data (paths, flags) that
541
+ * must arrive as one token whatever it contains: joined unquoted, an
542
+ * argument with spaces splits into several and hands the tool the wrong
543
+ * tokens.
544
+ *
545
+ * Windows follows the quoting rules cmd.exe and the MSVC runtimes share:
546
+ * wrap in double quotes, doubling the backslashes that precede a quote and
547
+ * escaping the quote itself. Inside double quotes cmd.exe already treats
548
+ * `&`, `|`, `<`, `>`, `^` and parentheses as literals, and the
549
+ * shell-metacharacter block above refuses `%` and the rest before this
550
+ * runs. A POSIX shell gets single quotes, which no expansion can leave.
551
+ *
552
+ * The same quoting is applied to the command itself when it names an
553
+ * existing file under a path with spaces; a command the shell must parse
554
+ * (a bare name, `sh -c`) never names a file as a whole and stays verbatim.
555
+ *
556
+ * @param {string} arg Argument to quote
557
+ * @param {boolean} [forWindows] Quote for cmd.exe rather than a POSIX shell; defaults to the current platform
558
+ * @returns {string} The argument, quoted when the shell needs it
559
+ */
560
+ export function shellQuoteArgument(arg, forWindows = isWin) {
561
+ const value = `${arg}`;
562
+ if (forWindows) {
563
+ if (value === "") {
564
+ return '""';
565
+ }
566
+ if (!/[\s"]/.test(value)) {
567
+ return value;
568
+ }
569
+ // Every backslash run that precedes a quote doubles, each quote escapes,
570
+ // and the trailing run doubles for the closing quote this returns. A
571
+ // character scan keeps it linear; the equivalent `(\\*)"` pattern is
572
+ // polynomial on runs of backslashes that no quote ends.
573
+ let quoted = "";
574
+ let backslashes = 0;
575
+ for (const ch of value) {
576
+ if (ch === "\\") {
577
+ backslashes++;
578
+ quoted += ch;
579
+ } else if (ch === '"') {
580
+ quoted += "\\".repeat(backslashes) + '\\"';
581
+ backslashes = 0;
582
+ } else {
583
+ quoted += ch;
584
+ backslashes = 0;
585
+ }
586
+ }
587
+ quoted += "\\".repeat(backslashes);
588
+ return `"${quoted}"`;
589
+ }
590
+ if (value === "") {
591
+ return "''";
592
+ }
593
+ if (!/[^-\w@%+=:,./]/.test(value)) {
594
+ return value;
595
+ }
596
+ return `'${value.replaceAll("'", `'\\''`)}'`;
597
+ }
598
+
599
+ /**
600
+ * Whether the path names an existing regular file, without throwing on the
601
+ * many values that are not paths at all (a bare command name, a command with
602
+ * arguments, anything the shell is meant to parse).
603
+ *
604
+ * @param {string} file Candidate path
605
+ * @returns {boolean} True when a regular file exists at the path
606
+ */
607
+ function isFileNoThrow(file) {
608
+ try {
609
+ return statSync(`${file}`).isFile();
610
+ } catch (_err) {
611
+ return false;
612
+ }
613
+ }
614
+
615
+ /**
616
+ * Build the single command string a shell spawn runs. Every argument is
617
+ * quoted for the platform shell, so an argument containing spaces stays one
618
+ * token and cannot alter the command, and a command that is itself a file
619
+ * under a path with spaces (`C:\Program Files\...\node.exe`) is quoted too;
620
+ * a command the shell must parse (`sh -c`, a bare name) never names an
621
+ * existing file as a whole and is left verbatim.
622
+ *
623
+ * The one exception is a caller that composes shell fragments itself (sbt
624
+ * command lines embed their own quoting for the shell to strip): it passes
625
+ * `rawShellArgs` and keeps the verbatim join, under the shell-metacharacter
626
+ * block in safeSpawnSync like every other argument.
627
+ *
628
+ * @param {string} command The executable
629
+ * @param {string[]|undefined} args Arguments, when the caller passed any
630
+ * @param {boolean} [rawShellArgs] Join the arguments verbatim instead of quoting them
631
+ * @returns {string} The command line the shell will run
632
+ */
633
+ export function joinShellCommand(command, args, rawShellArgs = false) {
634
+ let commandToken = `${command}`;
635
+ if (/\s/.test(commandToken) && isFileNoThrow(commandToken)) {
636
+ commandToken = shellQuoteArgument(commandToken);
637
+ }
638
+ if (!args?.length) {
639
+ return commandToken;
640
+ }
641
+ return [
642
+ commandToken,
643
+ ...(rawShellArgs ? args : args.map((arg) => shellQuoteArgument(arg))),
644
+ ].join(" ");
645
+ }
646
+
486
647
  function hasShellMetacharacters(value) {
487
648
  if (value === undefined || value === null) {
488
649
  return false;
@@ -696,9 +857,12 @@ export function safeSpawnSync(command, args, options) {
696
857
  }
697
858
  }
698
859
  }
860
+ // Read before the option is stripped below: a caller composing shell
861
+ // fragments itself opts out of argument quoting with this flag.
862
+ const rawShellArgs = options?.cdxgenRawShellArgs === true;
699
863
  if (!options) {
700
864
  options = {};
701
- } else if (options.cdxgenActivity) {
865
+ } else if (options.cdxgenActivity || options.cdxgenRawShellArgs) {
702
866
  options = {
703
867
  ...options,
704
868
  };
@@ -706,6 +870,9 @@ export function safeSpawnSync(command, args, options) {
706
870
  if (options.cdxgenActivity) {
707
871
  delete options.cdxgenActivity;
708
872
  }
873
+ if (options.cdxgenRawShellArgs) {
874
+ delete options.cdxgenRawShellArgs;
875
+ }
709
876
  if (options.shell === true) {
710
877
  const unsafeShellToken = getUnsafeShellToken(command, args);
711
878
  if (unsafeShellToken !== undefined) {
@@ -817,8 +984,9 @@ export function safeSpawnSync(command, args, options) {
817
984
  commandsExecuted.add(command);
818
985
  // Fix for DEP0190 warning
819
986
  if (options?.shell === true) {
987
+ const joined = joinShellCommand(command, args, rawShellArgs);
988
+ command = joined;
820
989
  if (args?.length) {
821
- command = `${command} ${args.join(" ")}`;
822
990
  args = undefined;
823
991
  }
824
992
  }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The spellings under which a config file may list custom metadata
3
+ * properties: those of the `--metadata-property` flag, and the plural that
4
+ * the CDXGEN_METADATA_PROPERTIES environment variable maps to.
5
+ */
6
+ export const METADATA_PROPERTY_CONFIG_KEYS = [
7
+ "metadata-property",
8
+ "metadataProperty",
9
+ "metadata-properties",
10
+ "metadataProperties",
11
+ ];
12
+
13
+ // Namespaces whose properties cdxgen itself asserts: `cdx:` is its registered
14
+ // taxonomy, and `internal:` carries its own bookkeeping
15
+ const RESERVED_METADATA_PROPERTY_PREFIXES = ["cdx:", "internal:"];
16
+
17
+ /**
18
+ * Report whether a property name belongs to a namespace cdxgen reserves for
19
+ * the facts it establishes itself, such as `cdx:bom:componentSrcFiles`.
20
+ *
21
+ * @param {string} name Property name
22
+ * @returns {boolean} true for a reserved name
23
+ */
24
+ export function isReservedMetadataPropertyName(name) {
25
+ return RESERVED_METADATA_PROPERTY_PREFIXES.some((prefix) =>
26
+ `${name}`.startsWith(prefix),
27
+ );
28
+ }
29
+
30
+ /**
31
+ * Normalize user-supplied metadata properties into `[{name, value}]`
32
+ * (discussion 4391).
33
+ *
34
+ * Accepted shapes:
35
+ * - a `name=value` string, or a JSON document holding any of the shapes below
36
+ * — an environment variable
37
+ * - an array of `name=value` strings or `{name, value}` objects — the
38
+ * `--metadata-property` flag and a JSON config file
39
+ * - a plain object mapping a name to a value, or to a list of values for a
40
+ * property that repeats — a JSON document or a YAML config file
41
+ *
42
+ * Values must be strings, numbers, or booleans. An entry without a name, a
43
+ * `name=value` string without a separator, and an empty or structured value
44
+ * are dropped rather than guessed at: a property with a mangled name or an
45
+ * `[object Object]` value is worse than none.
46
+ *
47
+ * @param {unknown} input Properties in any accepted shape
48
+ * @returns {{name: string, value: string}[]} Normalized properties
49
+ */
50
+ export function parseMetadataProperties(input) {
51
+ const properties = [];
52
+ if (typeof input === "string") {
53
+ let parsed;
54
+ try {
55
+ parsed = JSON.parse(input);
56
+ } catch (_err) {
57
+ parsed = undefined;
58
+ }
59
+ input =
60
+ parsed && typeof parsed === "object"
61
+ ? parsed
62
+ : [typeof parsed === "string" ? parsed : input];
63
+ }
64
+ const append = (name, value) => {
65
+ if (typeof name !== "string" && typeof name !== "number") {
66
+ return;
67
+ }
68
+ const propName = `${name}`.trim();
69
+ if (
70
+ !propName.length ||
71
+ !["string", "number", "boolean"].includes(typeof value) ||
72
+ !`${value}`.length
73
+ ) {
74
+ return;
75
+ }
76
+ properties.push({ name: propName, value: `${value}` });
77
+ };
78
+ if (Array.isArray(input)) {
79
+ for (const entry of input) {
80
+ if (typeof entry === "string") {
81
+ const separator = entry.indexOf("=");
82
+ if (separator > 0) {
83
+ append(entry.slice(0, separator), entry.slice(separator + 1));
84
+ }
85
+ } else if (entry && typeof entry === "object") {
86
+ append(entry.name, entry.value);
87
+ }
88
+ }
89
+ } else if (input && typeof input === "object") {
90
+ for (const [name, value] of Object.entries(input)) {
91
+ for (const avalue of Array.isArray(value) ? value : [value]) {
92
+ append(name, avalue);
93
+ }
94
+ }
95
+ }
96
+ return properties;
97
+ }
@@ -1,5 +1,10 @@
1
1
  import path from "node:path";
2
2
 
3
+ import {
4
+ isReservedMetadataPropertyName,
5
+ METADATA_PROPERTY_CONFIG_KEYS,
6
+ parseMetadataProperties,
7
+ } from "./metadataProperties.js";
3
8
  import { toCamel } from "./paths.js";
4
9
 
5
10
  /**
@@ -87,19 +92,41 @@ function isContainedPath(configDir, value) {
87
92
  * whenever that tree is. Path options are therefore confined to the config's
88
93
  * own directory - a scanned project may choose where in itself the BOM lands,
89
94
  * but not write outside it - and options that redirect data off the host or
90
- * widen what cdxgen executes are announced on the console.
95
+ * widen what cdxgen executes are announced on the console. A project may
96
+ * describe itself with custom metadata properties, but not in the namespaces
97
+ * cdxgen reserves for the facts it establishes, so a scanned tree cannot pass
98
+ * its own claims off as cdxgen's.
91
99
  *
92
100
  * @param {Object} config parsed config file contents
93
101
  * @param {string} configDir directory the config file was read from
94
- * @returns {{config: Object, rejected: string[], announced: string[]}} sanitized config and what it did
102
+ * @returns {{config: Object, rejected: string[], announced: string[], reservedProperties: string[]}} sanitized config and what it did
95
103
  */
96
104
  export function sanitizeProjectConfig(config, configDir) {
97
105
  if (!config || typeof config !== "object") {
98
- return { announced: [], config: {}, rejected: [] };
106
+ return { announced: [], config: {}, rejected: [], reservedProperties: [] };
99
107
  }
100
108
  const sanitized = { ...config };
101
109
  const rejected = [];
102
110
  const announced = [];
111
+ const reservedProperties = [];
112
+ for (const key of METADATA_PROPERTY_CONFIG_KEYS) {
113
+ if (sanitized[key] === undefined) {
114
+ continue;
115
+ }
116
+ const properties = [];
117
+ for (const aprop of parseMetadataProperties(sanitized[key])) {
118
+ if (isReservedMetadataPropertyName(aprop.name)) {
119
+ reservedProperties.push(aprop.name);
120
+ } else {
121
+ properties.push(aprop);
122
+ }
123
+ }
124
+ if (properties.length) {
125
+ sanitized[key] = properties;
126
+ } else {
127
+ delete sanitized[key];
128
+ }
129
+ }
103
130
  for (const option of PATH_OPTIONS) {
104
131
  for (const key of optionSpellings(option)) {
105
132
  if (sanitized[key] === undefined) {
@@ -118,5 +145,5 @@ export function sanitizeProjectConfig(config, configDir) {
118
145
  }
119
146
  }
120
147
  }
121
- return { announced, config: sanitized, rejected };
148
+ return { announced, config: sanitized, rejected, reservedProperties };
122
149
  }