@cdxgen/cdxgen 13.4.0 → 13.6.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 (305) hide show
  1. package/README.md +9 -9
  2. package/bin/cdxgen-secure.js +15 -0
  3. package/bin/cdxgen.js +29 -5
  4. package/bin/evinse.js +8 -0
  5. package/bin/hbom.js +15 -7
  6. package/bin/repl.js +29 -0
  7. package/bin/validate.js +6 -0
  8. package/data/cbomosdb-queries.json +13 -1
  9. package/data/component-tags.json +8 -0
  10. package/data/license-aliases.json +24 -2
  11. package/data/queries-darwin.json +25 -1
  12. package/data/queries-win.json +19 -1
  13. package/data/queries.json +41 -0
  14. package/data/remediations.json +407 -1
  15. package/data/rules/ai-bom.yaml +1 -1
  16. package/data/rules/brotli-decoder.yaml +107 -0
  17. package/data/rules/cbom-security.yaml +235 -4
  18. package/data/rules/go-binaries.yaml +46 -0
  19. package/data/rules/golem-go.yaml +80 -0
  20. package/data/rules/hbom-compliance.yaml +40 -22
  21. package/data/rules/hbom-performance.yaml +7 -1
  22. package/data/rules/host-topology.yaml +1 -1
  23. package/data/rules/obom-runtime.yaml +31 -2
  24. package/data/rules/php-runtime.yaml +143 -0
  25. package/data/rules/vscode-extensions.yaml +75 -0
  26. package/data/spdx-license-list.json +121 -7
  27. package/data/spdx-licenses.json +69 -0
  28. package/data/spdx.schema.json +845 -2
  29. package/lib/audit/index.js +14 -0
  30. package/lib/cli/bomAssembly.js +120 -6
  31. package/lib/cli/cliOptions.js +110 -3
  32. package/lib/cli/index.js +182 -6
  33. package/lib/cli/jsBom.js +153 -64
  34. package/lib/cli/jvmBom.js +363 -66
  35. package/lib/cli/managedBom.js +853 -87
  36. package/lib/cli/nativeBom.js +267 -33
  37. package/lib/core/boundedFile.js +344 -0
  38. package/lib/core/brotliProbe.js +182 -0
  39. package/lib/core/env.js +38 -8
  40. package/lib/core/fs.js +40 -19
  41. package/lib/core/httpClient.js +172 -46
  42. package/lib/core/lockPaths.js +38 -0
  43. package/lib/core/paths.js +24 -1
  44. package/lib/core/processTree.js +5 -0
  45. package/lib/core/projectConfig.js +1 -0
  46. package/lib/core/projectTooling.js +97 -0
  47. package/lib/core/propertySanitizer.js +72 -0
  48. package/lib/core/toolRegistry.js +170 -0
  49. package/lib/ecosystems/cppEvidence.js +263 -106
  50. package/lib/ecosystems/cppIncludeResolver.js +16 -5
  51. package/lib/ecosystems/ecosystems.js +25 -10
  52. package/lib/ecosystems/gemHostProbe.js +301 -0
  53. package/lib/ecosystems/golem.js +550 -6
  54. package/lib/ecosystems/gradleutils.js +858 -244
  55. package/lib/ecosystems/parsers-bazel.js +107 -6
  56. package/lib/ecosystems/parsers-dotnet.js +117 -18
  57. package/lib/ecosystems/parsers-go.js +1090 -367
  58. package/lib/ecosystems/parsers-js.js +505 -22
  59. package/lib/ecosystems/parsers-jvm.js +259 -17
  60. package/lib/ecosystems/parsers-misc.js +438 -64
  61. package/lib/ecosystems/parsers-python.js +100 -12
  62. package/lib/ecosystems/pythonutils.js +128 -2
  63. package/lib/ecosystems/rubyutils.js +544 -34
  64. package/lib/ecosystems/rusi.js +10 -0
  65. package/lib/ecosystems/scalacliutils.js +4 -1
  66. package/lib/ecosystems/scalasem.js +798 -151
  67. package/lib/ecosystems/vendoredCode.js +237 -3
  68. package/lib/ecosystems/venvProbe.js +360 -0
  69. package/lib/evinser/evinser.js +472 -61
  70. package/lib/evinser/swiftsem.js +68 -5
  71. package/lib/helpers/agenticToolutils.js +413 -93
  72. package/lib/helpers/vsixutils.js +1574 -315
  73. package/lib/inventory/agentSkillSpec.js +17 -0
  74. package/lib/inventory/apkInstalledDb.js +243 -0
  75. package/lib/inventory/atomUtils.js +110 -0
  76. package/lib/inventory/auditCategories.js +3 -3
  77. package/lib/inventory/brotliDecoder.js +142 -0
  78. package/lib/inventory/buildToolRateLimit.js +1 -1
  79. package/lib/inventory/cacheDir.js +52 -9
  80. package/lib/inventory/cbomutils.js +56 -7
  81. package/lib/inventory/certificates.js +2371 -0
  82. package/lib/inventory/communityAiConfigParser.js +4 -9
  83. package/lib/inventory/deps.js +41 -4
  84. package/lib/inventory/display.js +200 -3
  85. package/lib/inventory/dosai.js +639 -63
  86. package/lib/inventory/dotnetRuntime.js +217 -0
  87. package/lib/inventory/dynamic.js +45 -3
  88. package/lib/inventory/envcontext.js +475 -18
  89. package/lib/inventory/evidenceUtils.js +447 -23
  90. package/lib/inventory/fetchBatch.js +1 -1
  91. package/lib/inventory/formulationParsers.js +32 -3
  92. package/lib/inventory/goBuildInfo.js +140 -0
  93. package/lib/inventory/gtfobins.js +13 -0
  94. package/lib/inventory/hbom.js +67 -7
  95. package/lib/inventory/hbomAnalysis.js +16 -0
  96. package/lib/inventory/hostRuntimeProbe.js +686 -0
  97. package/lib/inventory/hostTopology.js +21 -0
  98. package/lib/inventory/jvmLocalRepos.js +9 -2
  99. package/lib/inventory/jvmToolEnv.js +29 -0
  100. package/lib/inventory/licenseEnhancer.js +12 -0
  101. package/lib/inventory/mavenCentralMirror.js +534 -0
  102. package/lib/inventory/nodeBuildInfo.js +218 -0
  103. package/lib/inventory/nodeHostProbe.js +202 -0
  104. package/lib/inventory/osPackagePurl.js +244 -0
  105. package/lib/inventory/osPackageResolver.js +271 -87
  106. package/lib/inventory/osinfo.js +79 -31
  107. package/lib/inventory/osqueryTransform.js +114 -5
  108. package/lib/inventory/phpBuildInfo.js +438 -0
  109. package/lib/inventory/phpHostProbe.js +185 -0
  110. package/lib/inventory/rootfsCerts.js +426 -0
  111. package/lib/inventory/rubyBuildInfo.js +972 -0
  112. package/lib/inventory/rubyHostProbe.js +218 -0
  113. package/lib/inventory/scalaCoords.js +4 -3
  114. package/lib/inventory/scalaDedupe.js +440 -0
  115. package/lib/inventory/source.js +256 -5
  116. package/lib/inventory/spdx.js +144 -0
  117. package/lib/inventory/traceRunner.js +5 -1
  118. package/lib/inventory/tracedCrypto.js +129 -0
  119. package/lib/managers/binary.js +866 -186
  120. package/lib/managers/docker.js +245 -12
  121. package/lib/parsers/email.js +277 -0
  122. package/lib/parsers/largeJson.js +62 -13
  123. package/lib/parsers/vscodeExtensionId.js +183 -0
  124. package/lib/server/server.js +198 -2
  125. package/lib/stages/postgen/annotator.js +17 -1
  126. package/lib/stages/postgen/introspection/reflect.js +4 -1
  127. package/lib/stages/postgen/postgen.js +5 -5
  128. package/lib/stages/postgen/ruleEngine.js +71 -6
  129. package/lib/stages/postgen/specVersionCompat.js +30 -0
  130. package/lib/stages/pregen/envAudit.js +1 -0
  131. package/lib/stages/pregen/pregen.js +231 -62
  132. package/lib/validator/bomValidator.js +32 -6
  133. package/lib/validator/generated/runtime.mjs +13 -0
  134. package/lib/validator/generated/validate-1.6.mjs +27 -8
  135. package/lib/validator/generated/validate-1.7.mjs +32 -13
  136. package/lib/validator/generated/validate-2.0.mjs +31 -12
  137. package/lib/validator/index.js +1 -1
  138. package/package.json +19 -18
  139. package/types/bin/cdxgen-secure.d.ts +3 -0
  140. package/types/bin/cdxgen-secure.d.ts.map +1 -0
  141. package/types/bin/repl.d.ts.map +1 -1
  142. package/types/lib/audit/index.d.ts.map +1 -1
  143. package/types/lib/cli/bomAssembly.d.ts +22 -3
  144. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  145. package/types/lib/cli/cliOptions.d.ts +43 -0
  146. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  147. package/types/lib/cli/index.d.ts.map +1 -1
  148. package/types/lib/cli/jsBom.d.ts +1 -17
  149. package/types/lib/cli/jsBom.d.ts.map +1 -1
  150. package/types/lib/cli/jvmBom.d.ts +20 -0
  151. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  152. package/types/lib/cli/managedBom.d.ts.map +1 -1
  153. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  154. package/types/lib/core/boundedFile.d.ts +70 -0
  155. package/types/lib/core/boundedFile.d.ts.map +1 -0
  156. package/types/lib/core/brotliProbe.d.ts +37 -0
  157. package/types/lib/core/brotliProbe.d.ts.map +1 -0
  158. package/types/lib/core/env.d.ts +22 -2
  159. package/types/lib/core/env.d.ts.map +1 -1
  160. package/types/lib/core/fs.d.ts.map +1 -1
  161. package/types/lib/core/httpClient.d.ts +24 -0
  162. package/types/lib/core/httpClient.d.ts.map +1 -1
  163. package/types/lib/core/lockPaths.d.ts +19 -0
  164. package/types/lib/core/lockPaths.d.ts.map +1 -0
  165. package/types/lib/core/processTree.d.ts +2 -0
  166. package/types/lib/core/processTree.d.ts.map +1 -1
  167. package/types/lib/core/projectConfig.d.ts.map +1 -1
  168. package/types/lib/core/projectTooling.d.ts +41 -0
  169. package/types/lib/core/projectTooling.d.ts.map +1 -0
  170. package/types/lib/core/propertySanitizer.d.ts +26 -0
  171. package/types/lib/core/propertySanitizer.d.ts.map +1 -1
  172. package/types/lib/core/toolRegistry.d.ts +36 -0
  173. package/types/lib/core/toolRegistry.d.ts.map +1 -0
  174. package/types/lib/ecosystems/cppEvidence.d.ts +40 -13
  175. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  176. package/types/lib/ecosystems/cppIncludeResolver.d.ts +2 -1
  177. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -1
  178. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  179. package/types/lib/ecosystems/gemHostProbe.d.ts +28 -0
  180. package/types/lib/ecosystems/gemHostProbe.d.ts.map +1 -0
  181. package/types/lib/ecosystems/golem.d.ts +81 -1
  182. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  183. package/types/lib/ecosystems/gradleutils.d.ts +57 -21
  184. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  185. package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -1
  186. package/types/lib/ecosystems/parsers-dotnet.d.ts +40 -4
  187. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  188. package/types/lib/ecosystems/parsers-go.d.ts +85 -13
  189. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  190. package/types/lib/ecosystems/parsers-js.d.ts +17 -20
  191. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  192. package/types/lib/ecosystems/parsers-jvm.d.ts +28 -2
  193. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  194. package/types/lib/ecosystems/parsers-misc.d.ts +37 -58
  195. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  196. package/types/lib/ecosystems/parsers-python.d.ts +14 -3
  197. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  198. package/types/lib/ecosystems/pythonutils.d.ts +19 -1
  199. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  200. package/types/lib/ecosystems/rubyutils.d.ts +144 -12
  201. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  202. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  203. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  204. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  205. package/types/lib/ecosystems/vendoredCode.d.ts +26 -0
  206. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -1
  207. package/types/lib/ecosystems/venvProbe.d.ts +40 -0
  208. package/types/lib/ecosystems/venvProbe.d.ts.map +1 -0
  209. package/types/lib/evinser/evinser.d.ts +88 -8
  210. package/types/lib/evinser/evinser.d.ts.map +1 -1
  211. package/types/lib/evinser/swiftsem.d.ts +10 -0
  212. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  213. package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
  214. package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
  215. package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
  216. package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
  217. package/types/lib/inventory/atomUtils.d.ts +33 -0
  218. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  219. package/types/lib/inventory/brotliDecoder.d.ts +58 -0
  220. package/types/lib/inventory/brotliDecoder.d.ts.map +1 -0
  221. package/types/lib/inventory/cacheDir.d.ts +12 -4
  222. package/types/lib/inventory/cacheDir.d.ts.map +1 -1
  223. package/types/lib/inventory/cbomutils.d.ts +17 -0
  224. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  225. package/types/lib/inventory/certificates.d.ts +205 -0
  226. package/types/lib/inventory/certificates.d.ts.map +1 -0
  227. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  228. package/types/lib/inventory/deps.d.ts +2 -1
  229. package/types/lib/inventory/deps.d.ts.map +1 -1
  230. package/types/lib/inventory/display.d.ts +18 -0
  231. package/types/lib/inventory/display.d.ts.map +1 -1
  232. package/types/lib/inventory/dosai.d.ts +162 -7
  233. package/types/lib/inventory/dosai.d.ts.map +1 -1
  234. package/types/lib/inventory/dotnetRuntime.d.ts +44 -0
  235. package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -0
  236. package/types/lib/inventory/dynamic.d.ts +6 -1
  237. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  238. package/types/lib/inventory/envcontext.d.ts +92 -8
  239. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  240. package/types/lib/inventory/evidenceUtils.d.ts +29 -10
  241. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  242. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  243. package/types/lib/inventory/goBuildInfo.d.ts +10 -0
  244. package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
  245. package/types/lib/inventory/gtfobins.d.ts.map +1 -1
  246. package/types/lib/inventory/hbom.d.ts.map +1 -1
  247. package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
  248. package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
  249. package/types/lib/inventory/hostRuntimeProbe.d.ts +44 -0
  250. package/types/lib/inventory/hostRuntimeProbe.d.ts.map +1 -0
  251. package/types/lib/inventory/hostTopology.d.ts.map +1 -1
  252. package/types/lib/inventory/jvmToolEnv.d.ts +12 -0
  253. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  254. package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -1
  255. package/types/lib/inventory/mavenCentralMirror.d.ts +170 -0
  256. package/types/lib/inventory/mavenCentralMirror.d.ts.map +1 -0
  257. package/types/lib/inventory/nodeBuildInfo.d.ts +95 -0
  258. package/types/lib/inventory/nodeBuildInfo.d.ts.map +1 -0
  259. package/types/lib/inventory/nodeHostProbe.d.ts +15 -0
  260. package/types/lib/inventory/nodeHostProbe.d.ts.map +1 -0
  261. package/types/lib/inventory/osPackagePurl.d.ts +99 -0
  262. package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
  263. package/types/lib/inventory/osPackageResolver.d.ts +44 -2
  264. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  265. package/types/lib/inventory/osinfo.d.ts +31 -6
  266. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  267. package/types/lib/inventory/osqueryTransform.d.ts +51 -0
  268. package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
  269. package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
  270. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
  271. package/types/lib/inventory/phpHostProbe.d.ts +35 -0
  272. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
  273. package/types/lib/inventory/rootfsCerts.d.ts +27 -0
  274. package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
  275. package/types/lib/inventory/rubyBuildInfo.d.ts +271 -0
  276. package/types/lib/inventory/rubyBuildInfo.d.ts.map +1 -0
  277. package/types/lib/inventory/rubyHostProbe.d.ts +47 -0
  278. package/types/lib/inventory/rubyHostProbe.d.ts.map +1 -0
  279. package/types/lib/inventory/source.d.ts +38 -0
  280. package/types/lib/inventory/source.d.ts.map +1 -1
  281. package/types/lib/inventory/spdx.d.ts +15 -0
  282. package/types/lib/inventory/spdx.d.ts.map +1 -1
  283. package/types/lib/inventory/traceRunner.d.ts +4 -1
  284. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  285. package/types/lib/inventory/tracedCrypto.d.ts +22 -0
  286. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
  287. package/types/lib/managers/binary.d.ts +10 -0
  288. package/types/lib/managers/binary.d.ts.map +1 -1
  289. package/types/lib/managers/docker.d.ts +24 -0
  290. package/types/lib/managers/docker.d.ts.map +1 -1
  291. package/types/lib/parsers/email.d.ts +19 -0
  292. package/types/lib/parsers/email.d.ts.map +1 -0
  293. package/types/lib/parsers/largeJson.d.ts +17 -3
  294. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  295. package/types/lib/server/server.d.ts +8 -0
  296. package/types/lib/server/server.d.ts.map +1 -1
  297. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  298. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
  299. package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
  300. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  301. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  302. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  303. package/types/lib/stages/pregen/pregen.d.ts +26 -9
  304. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  305. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -3,13 +3,9 @@ import { createHash } from "node:crypto";
3
3
  import process from "node:process";
4
4
  import { brotliDecompressSync, gunzipSync, inflateSync } from "node:zlib";
5
5
 
6
- import {
7
- Agent,
8
- EnvHttpProxyAgent,
9
- interceptors,
10
- request as undiciRequest,
11
- } from "undici";
6
+ import { Agent, EnvHttpProxyAgent, request as undiciRequest } from "undici";
12
7
 
8
+ import { readBrotliDecoder } from "./brotliProbe.js";
13
9
  import { registerRunReset } from "./runState.js";
14
10
 
15
11
  // Environment variables here are read with raw `process.env` rather than
@@ -213,6 +209,75 @@ export function resolveTimeout(timeout) {
213
209
  return undefined;
214
210
  }
215
211
 
212
+ let runtimeBrotliVerdict;
213
+
214
+ /**
215
+ * Set or clear the cached runtime Brotli verdict. Exported for unit tests only.
216
+ *
217
+ * @param {boolean|undefined} verdict verdict to use, or undefined to re-check
218
+ */
219
+ export function _setRuntimeBrotliVerdict(verdict) {
220
+ runtimeBrotliVerdict = verdict;
221
+ }
222
+
223
+ /**
224
+ * Whether the Brotli decoder of the runtime cdxgen runs on carries the
225
+ * CVE-2026-111497 fix, read once from the decoder itself: the node binary, or
226
+ * the shared libbrotlidec a `--shared-brotli` build (Homebrew, distro
227
+ * packages) loads; under Bun, the bun binary, which carries its own copy.
228
+ * process.versions.brotli cannot answer this, since Node.js backports the fix
229
+ * without changing it. Deno decodes Brotli in Rust, which was never affected.
230
+ *
231
+ * On Node.js cdxgen never asks for `br`, so a Brotli response comes only from
232
+ * a server that sends it unasked. When the decoder cannot be shown to be
233
+ * patched, such a body is left undecoded rather than handed to a decoder an
234
+ * attacker can write past. Bun decodes in its transport, so there the client
235
+ * stops asking for `br` instead.
236
+ *
237
+ * @param {Object} [runtime] the runtime to judge, for tests
238
+ * @returns {boolean} true when Brotli bodies may be decoded
239
+ */
240
+ export function runtimeBrotliDecoderIsPatched(runtime = undefined) {
241
+ if (!runtime && runtimeBrotliVerdict !== undefined) {
242
+ return runtimeBrotliVerdict;
243
+ }
244
+ const subject = runtime || {
245
+ isDeno: globalThis.Deno?.version?.deno !== undefined,
246
+ execPath: process.execPath,
247
+ sharedBrotli: [true, "true"].includes(
248
+ process.config?.variables?.node_shared_brotli,
249
+ ),
250
+ sharedObjects: () => process.report?.getReport?.()?.sharedObjects || [],
251
+ };
252
+ let verdict = false;
253
+ if (subject.isDeno) {
254
+ verdict = true;
255
+ } else {
256
+ const decoderFile = subject.sharedBrotli
257
+ ? subject
258
+ .sharedObjects()
259
+ .find((path) =>
260
+ `${path}`.split(/[\\/]/).pop().startsWith("libbrotlidec"),
261
+ )
262
+ : subject.execPath;
263
+ verdict = Boolean(
264
+ decoderFile &&
265
+ readBrotliDecoder(decoderFile)?.c?.blockSwitchFix === "true",
266
+ );
267
+ }
268
+ if (!runtime) {
269
+ runtimeBrotliVerdict = verdict;
270
+ if (!verdict) {
271
+ console.warn(
272
+ isBun
273
+ ? "The Brotli decoder in this Bun is not patched for CVE-2026-111497, or could not be checked, so cdxgen no longer asks servers for Brotli-encoded responses. Bun still decodes one a server sends unasked; upgrade Bun to a release with a patched Brotli."
274
+ : "The Brotli decoder of this runtime is not patched for CVE-2026-111497, or could not be checked, so Brotli-encoded responses are left undecoded. Upgrade Node.js to 22.23.4, 24.22.1 or 26.12.1 or later (and the shared brotli library on Homebrew or distro builds).",
275
+ );
276
+ }
277
+ }
278
+ return verdict;
279
+ }
280
+
216
281
  /**
217
282
  * Decompress a response body buffer according to its `Content-Encoding`. undici
218
283
  * performs no automatic decompression, so we replicate the behaviour `got` gave
@@ -240,6 +305,9 @@ function decompressBody(rawBody, contentEncoding) {
240
305
  } else if (encoding === "deflate") {
241
306
  buffer = inflateSync(buffer);
242
307
  } else if (encoding === "br") {
308
+ if (!runtimeBrotliDecoderIsPatched()) {
309
+ return buffer;
310
+ }
243
311
  buffer = brotliDecompressSync(buffer);
244
312
  } else if (encoding === "identity") {
245
313
  // no-op
@@ -301,6 +369,30 @@ async function readBody(undiciResponse, responseType) {
301
369
  return { body, rawBody };
302
370
  }
303
371
 
372
+ /**
373
+ * The headers to put on the wire. Bun's bundled undici asks for br by default
374
+ * and decodes it with Bun's own decoder before this client sees the body, so
375
+ * the check in decompressBody never runs there. Where that decoder is not
376
+ * patched, ask only for the encodings this client decodes itself. A server
377
+ * that sends br unasked is still decoded by Bun; only upgrading Bun closes
378
+ * that. The cache key is built from the caller's headers, not these.
379
+ *
380
+ * @param {Object} headers Request headers.
381
+ * @returns {Object} Headers to send.
382
+ */
383
+ function transportHeaders(headers) {
384
+ if (
385
+ !isBun ||
386
+ Object.keys(headers).some(
387
+ (key) => key.toLowerCase() === "accept-encoding",
388
+ ) ||
389
+ runtimeBrotliDecoderIsPatched()
390
+ ) {
391
+ return headers;
392
+ }
393
+ return { ...headers, "accept-encoding": "gzip, deflate" };
394
+ }
395
+
304
396
  /**
305
397
  * Convert a body into the `[body, headers]` pair to hand to undici. When the
306
398
  * `json` option is present it is serialized and a JSON `content-type` is added
@@ -534,20 +626,13 @@ function cacheKeyFor(method, url, headers = {}, responseType = undefined) {
534
626
  // The maximum number of redirects to follow when `followRedirect` is enabled.
535
627
  const MAX_REDIRECTIONS = 10;
536
628
 
537
- // Bun ships its own bundled implementation of the `undici` module which, as of
538
- // Bun 1.2, does not expose the `Agent.prototype.compose` composition API nor the
539
- // `interceptors.redirect` interceptor that undici v7 uses for redirect handling
540
- // (see cdxgen issue #4245). Detect the Bun runtime so we can fall back to plain
541
- // dispatchers and follow redirects manually instead of crashing at module load.
629
+ // Bun ships its own bundled implementation of the `undici` module, which lacks
630
+ // `EnvHttpProxyAgent` (see cdxgen issue #4245).
542
631
  const isBun = typeof globalThis.Bun !== "undefined";
543
632
 
544
- // Reusable pooled dispatchers. On Node.js, undici composes redirect handling as
545
- // an interceptor (there is no per-request `maxRedirections` option in undici v7),
546
- // so we pre-build the small set of dispatcher variants we need instead of
547
- // allocating a fresh Agent per request (which would defeat connection pooling).
548
- // On Bun, `compose`/`interceptors.redirect` are unavailable, so the redirect
549
- // dispatchers are just the plain agents and redirects are handled in
550
- // `requestWithRedirects` below.
633
+ // Reusable pooled dispatchers, one per TLS verification setting, so requests
634
+ // share connections instead of allocating a fresh Agent each time. Redirects
635
+ // are followed in `requestWithRedirects` below on every runtime.
551
636
  // Whether standard proxy environment variables are set. When they are, requests
552
637
  // must be routed through the proxy. Because this client always passes an
553
638
  // explicit per-request `dispatcher`, it overrides Node's global (potentially
@@ -584,51 +669,72 @@ const defaultAgent = createBaseAgent();
584
669
  const insecureAgent = createBaseAgent({
585
670
  connect: { rejectUnauthorized: false },
586
671
  });
587
- const redirectDispatcher = isBun
588
- ? defaultAgent
589
- : defaultAgent.compose(
590
- interceptors.redirect({ maxRedirections: MAX_REDIRECTIONS }),
591
- );
592
- const insecureRedirectDispatcher = isBun
593
- ? insecureAgent
594
- : insecureAgent.compose(
595
- interceptors.redirect({ maxRedirections: MAX_REDIRECTIONS }),
596
- );
597
672
 
598
673
  /**
599
- * Pick the undici dispatcher matching the request's redirect and TLS
600
- * verification preferences.
674
+ * Pick the undici dispatcher matching the request's TLS verification
675
+ * preference. Redirects are followed by `requestWithRedirects`, not by the
676
+ * dispatcher.
601
677
  *
602
- * @param {boolean} followRedirect Whether HTTP redirects should be followed.
603
678
  * @param {boolean} insecure Whether TLS certificate verification is disabled.
604
679
  * @returns {import("undici").Dispatcher} The dispatcher to use for the request.
605
680
  */
606
- function selectDispatcher(followRedirect, insecure) {
607
- if (insecure) {
608
- return followRedirect ? insecureRedirectDispatcher : insecureAgent;
609
- }
610
- return followRedirect ? redirectDispatcher : defaultAgent;
681
+ function selectDispatcher(insecure) {
682
+ return insecure ? insecureAgent : defaultAgent;
611
683
  }
612
684
 
613
685
  // HTTP status codes that represent a redirect with a `location` header.
614
686
  const REDIRECT_STATUS_CODES = new Set([301, 302, 303, 307, 308]);
615
687
 
688
+ // Request headers that may follow a redirect to a different origin. Everything
689
+ // else, such as Authorization, cookies and registry API keys like X-Api-Key or
690
+ // PRIVATE-TOKEN, stays with the origin the caller addressed.
691
+ const CROSS_ORIGIN_REDIRECT_HEADERS = new Set([
692
+ "accept",
693
+ "accept-encoding",
694
+ "accept-language",
695
+ "user-agent",
696
+ ]);
697
+
698
+ /**
699
+ * Keep only the request headers that may be sent to a different origin.
700
+ *
701
+ * @param {Object} headers Request headers.
702
+ * @returns {Object} Headers safe to send cross-origin.
703
+ */
704
+ function crossOriginRedirectHeaders(headers = {}) {
705
+ const kept = {};
706
+ for (const [name, value] of Object.entries(headers)) {
707
+ if (CROSS_ORIGIN_REDIRECT_HEADERS.has(name.toLowerCase())) {
708
+ kept[name] = value;
709
+ }
710
+ }
711
+ return kept;
712
+ }
713
+
616
714
  /**
617
- * Issue an undici request, following redirects manually when running under Bun.
715
+ * Issue an undici request and follow redirects one hop at a time.
618
716
  *
619
- * On Node.js the redirect-following interceptor baked into the dispatcher does
620
- * the work, so this simply forwards to `undiciRequest`. On Bun (where the
621
- * interceptor API is unavailable) we loop on 3xx responses, draining each
622
- * intermediate body and resolving relative `location` headers, up to
717
+ * Redirects are followed here rather than by undici's redirect interceptor so
718
+ * that every hop goes through `beforeRedirect`, which runs the same host
719
+ * allowlist, protocol and dry-run checks as the first request, and so that
720
+ * credentials are not forwarded to another origin. Each intermediate body is
721
+ * drained and relative `location` headers are resolved, up to
623
722
  * `MAX_REDIRECTIONS` hops.
624
723
  *
625
724
  * @param {URL} url Request URL.
626
725
  * @param {Object} requestOptions Options passed to `undiciRequest`.
627
726
  * @param {boolean} followRedirect Whether redirects should be followed.
727
+ * @param {function(URL): Promise<void>} beforeRedirect Called with the next URL
728
+ * before each hop; throws to stop the request.
628
729
  * @returns {Promise<Object>} The final undici response.
629
730
  */
630
- async function requestWithRedirects(url, requestOptions, followRedirect) {
631
- if (!isBun || !followRedirect) {
731
+ async function requestWithRedirects(
732
+ url,
733
+ requestOptions,
734
+ followRedirect,
735
+ beforeRedirect,
736
+ ) {
737
+ if (!followRedirect) {
632
738
  return await undiciRequest(url, requestOptions);
633
739
  }
634
740
  let currentUrl = url;
@@ -649,7 +755,14 @@ async function requestWithRedirects(url, requestOptions, followRedirect) {
649
755
  } catch {
650
756
  // Ignore drain failures; the socket will be discarded.
651
757
  }
652
- currentUrl = new URL(location, currentUrl);
758
+ const nextUrl = new URL(location, currentUrl);
759
+ await beforeRedirect(nextUrl);
760
+ if (nextUrl.origin !== currentUrl.origin) {
761
+ requestOptions.headers = crossOriginRedirectHeaders(
762
+ requestOptions.headers,
763
+ );
764
+ }
765
+ currentUrl = nextUrl;
653
766
  redirections += 1;
654
767
  // 303, and 301/302 for anything other than GET/HEAD, must switch to GET
655
768
  // without a body, matching how undici's redirect interceptor behaves.
@@ -736,11 +849,23 @@ async function doRequest(mergedOptions) {
736
849
  const insecure = options.https?.rejectUnauthorized === false;
737
850
  const requestOptions = {
738
851
  method,
739
- headers,
852
+ headers: transportHeaders(headers),
740
853
  body: method === "GET" || method === "HEAD" ? undefined : body,
741
- dispatcher: selectDispatcher(followRedirect, insecure),
854
+ dispatcher: selectDispatcher(insecure),
742
855
  signal,
743
856
  };
857
+ // A redirect target must pass the same beforeRequest checks, such as the
858
+ // host allowlist, as the URL the caller asked for.
859
+ const beforeRedirect = async (nextUrl) => {
860
+ const hopOptions = {
861
+ ...options,
862
+ url: nextUrl,
863
+ context: { ...options.context },
864
+ };
865
+ for (const hook of options.hooks?.beforeRequest || []) {
866
+ await hook(hopOptions);
867
+ }
868
+ };
744
869
 
745
870
  let undiciResponse;
746
871
  try {
@@ -748,6 +873,7 @@ async function doRequest(mergedOptions) {
748
873
  url,
749
874
  requestOptions,
750
875
  followRedirect,
876
+ beforeRedirect,
751
877
  );
752
878
  } catch (err) {
753
879
  const requestError = new RequestError(err, options);
@@ -0,0 +1,38 @@
1
+ import { isAbsolute, relative, resolve, win32 } from "node:path";
2
+
3
+ /**
4
+ * Containment check for a path taken from a lockfile (issue #3816). A crafted
5
+ * npm lockfile can name `file:`/`link:` specifiers or link targets built from
6
+ * `..` chains or absolute paths, so a parser that is about to list such an
7
+ * entry checks the resolved path against the project root first.
8
+ *
9
+ * The candidate is resolved against `projectRoot`, so both relative values
10
+ * and the absolute paths arborist already normalised are accepted. The root
11
+ * itself counts as inside: a lockfile may legitimately link the project to
12
+ * its own directory.
13
+ *
14
+ * @param {string} candidate Path to check, absolute or relative to projectRoot
15
+ * @param {string} projectRoot Project root the path must stay inside; a falsy
16
+ * root means the current working directory, mirroring `node:path`
17
+ *
18
+ * @returns {Boolean} True when the path is inside projectRoot
19
+ */
20
+ export function isPathInsideProject(candidate, projectRoot) {
21
+ if (typeof candidate !== "string" || !candidate) {
22
+ return false;
23
+ }
24
+ if (candidate.includes("\0")) {
25
+ return false;
26
+ }
27
+ const rootAbs = resolve(projectRoot || process.cwd());
28
+ const rel = relative(rootAbs, resolve(rootAbs, candidate));
29
+ if (!rel) {
30
+ return true;
31
+ }
32
+ // relative() returns an absolute path when the two sit on different
33
+ // Windows drives.
34
+ if (isAbsolute(rel) || win32.isAbsolute(rel)) {
35
+ return false;
36
+ }
37
+ return rel !== ".." && !rel.startsWith("../") && !rel.startsWith("..\\");
38
+ }
package/lib/core/paths.js CHANGED
@@ -14,7 +14,7 @@
14
14
  * `https://` URL, and `fileURLToPath` throws `ERR_INVALID_URL_SCHEME` on those.
15
15
  */
16
16
 
17
- import { platform } from "node:os";
17
+ import { homedir, platform } from "node:os";
18
18
  import { dirname } from "node:path";
19
19
  import { fileURLToPath } from "node:url";
20
20
 
@@ -68,3 +68,26 @@ export function isValidDriveRoot(root) {
68
68
  export function toCamel(str) {
69
69
  return str.replace(/-([a-z])/g, (_, g) => g.toUpperCase());
70
70
  }
71
+
72
+ /**
73
+ * Rewrite an absolute path under the user's home directory to a `~`-relative
74
+ * form so the emitted BOM does not carry the local username.
75
+ *
76
+ * @param {string} filePath Absolute path
77
+ * @param {string} [home] Home directory, defaulting to the current user's
78
+ * @returns {string} Home-relative path when applicable, else the input
79
+ */
80
+ export function redactHomePath(filePath, home = homedir()) {
81
+ const value = String(filePath || "");
82
+ if (!home || !value.startsWith(home)) {
83
+ return value;
84
+ }
85
+ // Require a path boundary so a sibling home such as /Users/alice-backup is
86
+ // not rewritten as if it sat under /Users/alice.
87
+ const remainder = value.slice(home.length);
88
+ if (remainder && !/^[/\\]/u.test(remainder)) {
89
+ return value;
90
+ }
91
+ const rest = remainder.replace(/^[/\\]/u, "");
92
+ return rest ? `~/${rest.replaceAll("\\", "/")}` : "~";
93
+ }
@@ -37,6 +37,8 @@ export function formatDuration(ms) {
37
37
  * its end: once the root is gone its pid can be handed to an unrelated
38
38
  * process, whose children would otherwise look like the run's. Elsewhere this
39
39
  * does nothing: an orphan is re-parented and can no longer be told apart.
40
+ * It returns once the stopped processes have exited, or after ten seconds,
41
+ * so the caller can remove files the run left half-written.
40
42
  *
41
43
  * @param {number|undefined} rootPid pid of the process cdxgen spawned
42
44
  * @param {number} startedAt Epoch milliseconds at which the run started
@@ -77,6 +79,9 @@ export function reapProcessTree(
77
79
  " }",
78
80
  "}",
79
81
  "foreach ($child in $found) { Stop-Process -Id $child -Force -ErrorAction SilentlyContinue }",
82
+ // Stop-Process returns before the process exits, and a process that is
83
+ // still exiting keeps its files open, so wait for the exits.
84
+ "if ($found.Count) { Wait-Process -Id $found.ToArray() -Timeout 10 -ErrorAction SilentlyContinue }",
80
85
  "$found -join ','",
81
86
  ].join("\n");
82
87
  const result = safeSpawnSync(
@@ -45,6 +45,7 @@ const ANNOUNCED_OPTIONS = new Set([
45
45
  "api-key",
46
46
  "include-formulation",
47
47
  "install-deps",
48
+ "introspect",
48
49
  "deep",
49
50
  "tea-publish",
50
51
  "tea-fetch",
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Policy for running the scanned project's own build tooling.
3
+ *
4
+ * Build tools such as Maven, Gradle, Poetry, MSBuild, SwiftPM, Leiningen and
5
+ * CocoaPods read configuration or run code that ships with the project
6
+ * (`.mvn/jvm.config`, `.poetry/plugins`, `Directory.Build.rsp`, `Package.swift`,
7
+ * `project.clj`, podspecs). Running them on an untrusted project runs that
8
+ * project's code, so they belong to the same surface as dependency
9
+ * installation and are skipped whenever installation is disabled.
10
+ */
11
+ import { isSecureMode, readEnvironmentVariable } from "./activity.js";
12
+ import { recordDegradation } from "./buildLedger.js";
13
+
14
+ /**
15
+ * Whether secure mode is on right now. `isSecureMode` is read once when the
16
+ * modules load, but the `cdxgen-secure` command turns secure mode on later,
17
+ * by setting CDXGEN_SECURE_MODE before the scan starts.
18
+ *
19
+ * @returns {boolean} true when secure mode is on
20
+ */
21
+ export function isSecureModeActive() {
22
+ return (
23
+ isSecureMode ||
24
+ ["true", "1"].includes(readEnvironmentVariable("CDXGEN_SECURE_MODE"))
25
+ );
26
+ }
27
+
28
+ /**
29
+ * Whether the scan may run the scanned project's own build tooling, such as
30
+ * wrapper scripts or build definitions that Maven, Gradle, sbt, Mill, Poetry,
31
+ * PDM, MSBuild, SwiftPM, Leiningen and CocoaPods evaluate.
32
+ *
33
+ * Tooling follows `options.installDeps`, which defaults to off in secure mode
34
+ * when a caller does not set it. Introspection mode exists to observe the
35
+ * project's own toolchain, so it keeps that tooling on even when dependency
36
+ * installation is disabled, as long as the operator asked for it rather than
37
+ * a config file in the scanned project. Secure mode still wins:
38
+ * CDXGEN_INTROSPECT alone must not make a secure-mode scan run the scanned
39
+ * project's code.
40
+ *
41
+ * @param {Object} [options] CLI options
42
+ * @returns {boolean} true when project tooling may run
43
+ */
44
+ export function isProjectToolingAllowed(options) {
45
+ const secureMode = isSecureModeActive();
46
+ const installDeps = options?.installDeps ?? !secureMode;
47
+ if (installDeps !== false) {
48
+ return true;
49
+ }
50
+ return (
51
+ options?.introspect === true &&
52
+ options?.introspectFromConfigFile !== true &&
53
+ !secureMode
54
+ );
55
+ }
56
+
57
+ // The skips already reported for each scan, keyed by the scan's options.
58
+ // Callers without options share one key.
59
+ const reportedSkips = new WeakMap();
60
+ const noOptionsKey = {};
61
+
62
+ /**
63
+ * Tell the user that a tool was skipped because project tooling is not
64
+ * allowed, and record the matching degradation for the fidelity report.
65
+ * Each skip is reported once per scan, however many manifests it applies to.
66
+ * A skip that only costs evidence, not BOM content, passes no remediation id
67
+ * and is only shown.
68
+ *
69
+ * @param {Object} options CLI options of the scan
70
+ * @param {string|null} remediationId Remediation id from
71
+ * data/remediations.json, or null for a message only
72
+ * @param {string} message Console message shown to the user
73
+ * @param {Object} [fields] Degradation fields (ecosystem, tool, impact, detail)
74
+ * @returns {void}
75
+ */
76
+ export function reportProjectToolingSkipped(
77
+ options,
78
+ remediationId,
79
+ message,
80
+ fields,
81
+ ) {
82
+ const key = options && typeof options === "object" ? options : noOptionsKey;
83
+ let reported = reportedSkips.get(key);
84
+ if (!reported) {
85
+ reported = new Set();
86
+ reportedSkips.set(key, reported);
87
+ }
88
+ const skipKey = remediationId || message;
89
+ if (reported.has(skipKey)) {
90
+ return;
91
+ }
92
+ reported.add(skipKey);
93
+ console.log(message);
94
+ if (remediationId) {
95
+ recordDegradation(remediationId, fields);
96
+ }
97
+ }
@@ -26,6 +26,15 @@ const JSON_PROPERTY_NAMES = new Set([
26
26
  ]);
27
27
  const URL_PATTERN = /https?:\/\/[^\s<>"'),\]}]+/giu;
28
28
  const MAX_STRUCTURE_DEPTH = 6;
29
+ // What an RFC 3986 path may hold as it is: unreserved characters, sub-delims,
30
+ // ":", "@", "/" and well-formed percent-escapes. A query or fragment may also
31
+ // hold "?". Everything else is percent-encoded.
32
+ const URI_PATH_UNSAFE =
33
+ /%(?![0-9A-Fa-f]{2})|[^A-Za-z0-9\-._~!$&'()*+,;=:@/%]/gu;
34
+ const URI_QUERY_UNSAFE =
35
+ /%(?![0-9A-Fa-f]{2})|[^A-Za-z0-9\-._~!$&'()*+,;=:@/?%]/gu;
36
+ const URI_SCHEME_AUTHORITY = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/?#]*/u;
37
+ const UTF8_ENCODER = new TextEncoder();
29
38
 
30
39
  function sanitizeUrlForBom(value) {
31
40
  const input = String(value || "").trim();
@@ -146,6 +155,69 @@ export function sanitizeBomUrl(value) {
146
155
  return sanitizeUrlForBom(value);
147
156
  }
148
157
 
158
+ function percentEncodeUnsafe(text, unsafe) {
159
+ return text.replace(unsafe, (character) =>
160
+ Array.from(
161
+ UTF8_ENCODER.encode(character),
162
+ (byte) => `%${byte.toString(16).toUpperCase().padStart(2, "0")}`,
163
+ ).join(""),
164
+ );
165
+ }
166
+
167
+ /**
168
+ * Percent-encode what an RFC 3986 URI reference cannot hold as it is, so a
169
+ * route template or URL can be emitted where CycloneDX expects an
170
+ * iri-reference, such as `services[].endpoints[]`. The braces of a route
171
+ * template, spaces, `<>`, `[]` outside an IPv6 host, `|`, `^`, non-ASCII
172
+ * characters and a `%` that starts no escape are encoded:
173
+ * `/users/{id}` becomes `/users/%7Bid%7D`. The scheme and authority of an
174
+ * absolute URL are kept as written, the query and fragment stay a query and
175
+ * fragment, and existing escapes are kept, so encoding twice changes
176
+ * nothing. Nothing is removed; strip credentials with {@link sanitizeBomUrl}
177
+ * first.
178
+ *
179
+ * @param {unknown} value Route template, path or URL
180
+ * @returns {string} The encoded reference
181
+ */
182
+ export function encodeUriReference(value) {
183
+ const text = String(value ?? "");
184
+ const authority = URI_SCHEME_AUTHORITY.exec(text)?.[0] ?? "";
185
+ const rest = text.slice(authority.length);
186
+ const queryStart = rest.indexOf("?");
187
+ const fragmentStart = rest.indexOf("#");
188
+ const pathEnd = Math.min(
189
+ queryStart < 0 ? rest.length : queryStart,
190
+ fragmentStart < 0 ? rest.length : fragmentStart,
191
+ );
192
+ let encoded =
193
+ authority + percentEncodeUnsafe(rest.slice(0, pathEnd), URI_PATH_UNSAFE);
194
+ if (pathEnd === queryStart) {
195
+ const queryEnd = fragmentStart > queryStart ? fragmentStart : rest.length;
196
+ encoded += `?${percentEncodeUnsafe(rest.slice(queryStart + 1, queryEnd), URI_QUERY_UNSAFE)}`;
197
+ }
198
+ if (fragmentStart >= 0) {
199
+ encoded += `#${percentEncodeUnsafe(rest.slice(fragmentStart + 1), URI_QUERY_UNSAFE)}`;
200
+ }
201
+ return encoded;
202
+ }
203
+
204
+ /**
205
+ * Whether a value carries a token shape the BOM sanitizer redacts: cloud
206
+ * access keys, bearer tokens, GitHub, Slack and npm tokens, JWTs and the
207
+ * like. Callers that cannot splice a `[redacted]` marker into the value, such
208
+ * as a URL path segment, use it to drop the value instead.
209
+ *
210
+ * @param {unknown} value Value to check
211
+ * @returns {boolean} true when the value holds an inline credential
212
+ */
213
+ export function hasInlineCredential(value) {
214
+ const text = String(value ?? "");
215
+ return INLINE_CREDENTIAL_PATTERNS.some((pattern) => {
216
+ pattern.lastIndex = 0;
217
+ return pattern.test(text);
218
+ });
219
+ }
220
+
149
221
  /**
150
222
  * Sanitize a property value before serializing it into BOM properties.
151
223
  *