@cdxgen/cdxgen 13.2.0 → 13.4.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 (311) hide show
  1. package/README.md +33 -14
  2. package/bin/cdxgen.js +310 -138
  3. package/bin/evinse.js +22 -0
  4. package/bin/repl.js +86 -0
  5. package/bin/sign.js +119 -15
  6. package/bin/tracebom.js +69 -5
  7. package/bin/validate.js +107 -19
  8. package/bin/verify.js +172 -63
  9. package/data/helpers/init.gradle +46 -21
  10. package/data/remediations.json +85 -0
  11. package/data/rules/dependency-sources.yaml +67 -0
  12. package/data/rules/scala-evidence.yaml +115 -0
  13. package/lib/audit/index.js +38 -6
  14. package/lib/cli/bomAssembly.js +105 -15
  15. package/lib/cli/cliOptions.js +93 -1
  16. package/lib/cli/index.js +156 -58
  17. package/lib/cli/jsBom.js +99 -53
  18. package/lib/cli/jvmBom.js +708 -147
  19. package/lib/cli/managedBom.js +415 -224
  20. package/lib/cli/nativeBom.js +264 -44
  21. package/lib/core/activity.js +12 -0
  22. package/lib/core/env.js +22 -15
  23. package/lib/core/fs.js +176 -7
  24. package/lib/core/httpClient.js +159 -24
  25. package/lib/core/metadataProperties.js +97 -0
  26. package/lib/core/pluginRun.js +184 -0
  27. package/lib/core/processTree.js +100 -0
  28. package/lib/core/projectConfig.js +31 -4
  29. package/lib/core/runState.js +39 -0
  30. package/lib/core/toml.js +72 -0
  31. package/lib/ecosystems/asarutils.js +4 -2
  32. package/lib/ecosystems/bunutils.js +274 -108
  33. package/lib/ecosystems/cargoCache.js +231 -8
  34. package/lib/ecosystems/caxa.js +434 -2
  35. package/lib/ecosystems/cmakeResolver.js +76 -21
  36. package/lib/ecosystems/cppBuildContext.js +631 -0
  37. package/lib/ecosystems/cppEvidence.js +399 -51
  38. package/lib/ecosystems/cppIncludeResolver.js +294 -0
  39. package/lib/ecosystems/denoutils.js +93 -66
  40. package/lib/ecosystems/ecosystems.js +1983 -793
  41. package/lib/ecosystems/golem.js +28 -12
  42. package/lib/ecosystems/gradleutils.js +356 -37
  43. package/lib/ecosystems/jsEvidence.js +104 -44
  44. package/lib/ecosystems/kosi.js +31 -7
  45. package/lib/ecosystems/npmutils.js +174 -9
  46. package/lib/ecosystems/parsers-dotnet.js +616 -159
  47. package/lib/ecosystems/parsers-elm.js +46 -28
  48. package/lib/ecosystems/parsers-gleam.js +3 -4
  49. package/lib/ecosystems/parsers-go.js +371 -70
  50. package/lib/ecosystems/parsers-js.js +263 -76
  51. package/lib/ecosystems/parsers-julia.js +2 -3
  52. package/lib/ecosystems/parsers-jvm.js +204 -12
  53. package/lib/ecosystems/parsers-misc.js +929 -216
  54. package/lib/ecosystems/parsers-python.js +167 -24
  55. package/lib/ecosystems/parsers-rust.js +28 -22
  56. package/lib/ecosystems/parsers-terraform.js +1573 -19
  57. package/lib/ecosystems/pythonutils.js +102 -6
  58. package/lib/ecosystems/remote/huggingface.js +3 -0
  59. package/lib/ecosystems/rubyutils.js +1492 -182
  60. package/lib/ecosystems/rusi.js +54 -10
  61. package/lib/ecosystems/sbtutils.js +243 -187
  62. package/lib/ecosystems/scalacliutils.js +342 -0
  63. package/lib/ecosystems/scalasem.js +2618 -0
  64. package/lib/ecosystems/terraformRegistry.js +288 -0
  65. package/lib/ecosystems/utils.js +2 -0
  66. package/lib/ecosystems/vendoredCode.js +156 -0
  67. package/lib/evinser/evinser.js +1008 -191
  68. package/lib/evinser/swiftsem.js +1914 -302
  69. package/lib/helpers/agenticToolutils.js +2 -2
  70. package/lib/helpers/bomSigner.js +1101 -222
  71. package/lib/inventory/aiCollector.js +4 -1
  72. package/lib/inventory/analyzer.js +110 -127
  73. package/lib/inventory/atomUtils.js +703 -43
  74. package/lib/inventory/auditCategories.js +7 -0
  75. package/lib/inventory/buildToolRateLimit.js +144 -0
  76. package/lib/inventory/cacheCommands.js +2 -1
  77. package/lib/inventory/cdxrs.js +103 -12
  78. package/lib/inventory/cmakeBuildDirs.js +254 -0
  79. package/lib/inventory/deps.js +501 -124
  80. package/lib/inventory/depsUtils.js +183 -11
  81. package/lib/inventory/display.js +4 -5
  82. package/lib/inventory/dosai.js +798 -57
  83. package/lib/inventory/dosaiParsers.js +310 -15
  84. package/lib/inventory/dynamic.js +90 -6
  85. package/lib/inventory/evidenceUtils.js +166 -113
  86. package/lib/inventory/fetchBatch.js +690 -55
  87. package/lib/inventory/fetchDiskCache.js +597 -0
  88. package/lib/inventory/forgeEnricher.js +14 -5
  89. package/lib/inventory/jvmLocalRepos.js +917 -0
  90. package/lib/inventory/mcpConfigParser.js +2 -3
  91. package/lib/inventory/parentSelection.js +146 -0
  92. package/lib/inventory/protobom.js +8 -1
  93. package/lib/inventory/purl.js +152 -3
  94. package/lib/inventory/rustFormulationParser.js +3 -4
  95. package/lib/inventory/sbomDocument.js +2 -70
  96. package/lib/inventory/scalaCoords.js +153 -0
  97. package/lib/inventory/source.js +45 -27
  98. package/lib/inventory/spdx.js +101 -0
  99. package/lib/inventory/traceRunner.js +295 -25
  100. package/lib/inventory/tracedCrypto.js +294 -0
  101. package/lib/managers/binary.js +115 -24
  102. package/lib/managers/docker.js +467 -46
  103. package/lib/managers/oci.js +63 -3
  104. package/lib/parsers/cmakeCache.js +69 -0
  105. package/lib/parsers/cmakeLists.js +466 -0
  106. package/lib/parsers/cmakePresets.js +400 -0
  107. package/lib/parsers/compileCommands.js +557 -0
  108. package/lib/parsers/largeJson.js +454 -0
  109. package/lib/parsers/npmrc.js +19 -1
  110. package/lib/parsers/strictJson.js +322 -0
  111. package/lib/parsers/terraformConfig.js +1073 -0
  112. package/lib/parsers/terraformSource.js +652 -0
  113. package/lib/server/server.js +23 -0
  114. package/lib/stages/postgen/introspection/markers.js +1 -1
  115. package/lib/stages/postgen/postgen.js +83 -16
  116. package/lib/stages/postgen/specVersionCompat.js +32 -0
  117. package/lib/validator/bomValidator.js +6 -16
  118. package/lib/validator/index.js +64 -11
  119. package/lib/validator/reporters/console.js +3 -0
  120. package/lib/validator/reporters/json.js +2 -0
  121. package/lib/validator/reporters/sarif.js +7 -0
  122. package/package.json +23 -23
  123. package/types/lib/audit/index.d.ts.map +1 -1
  124. package/types/lib/cli/bomAssembly.d.ts +2 -1
  125. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  126. package/types/lib/cli/cliOptions.d.ts +33 -1
  127. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  128. package/types/lib/cli/index.d.ts.map +1 -1
  129. package/types/lib/cli/jsBom.d.ts.map +1 -1
  130. package/types/lib/cli/jvmBom.d.ts +24 -1
  131. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  132. package/types/lib/cli/managedBom.d.ts +7 -4
  133. package/types/lib/cli/managedBom.d.ts.map +1 -1
  134. package/types/lib/cli/nativeBom.d.ts +3 -1
  135. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  136. package/types/lib/core/activity.d.ts.map +1 -1
  137. package/types/lib/core/env.d.ts +9 -0
  138. package/types/lib/core/env.d.ts.map +1 -1
  139. package/types/lib/core/fs.d.ts +55 -0
  140. package/types/lib/core/fs.d.ts.map +1 -1
  141. package/types/lib/core/httpClient.d.ts +13 -8
  142. package/types/lib/core/httpClient.d.ts.map +1 -1
  143. package/types/lib/core/metadataProperties.d.ts +39 -0
  144. package/types/lib/core/metadataProperties.d.ts.map +1 -0
  145. package/types/lib/core/pluginRun.d.ts +97 -0
  146. package/types/lib/core/pluginRun.d.ts.map +1 -0
  147. package/types/lib/core/processTree.d.ts +36 -0
  148. package/types/lib/core/processTree.d.ts.map +1 -0
  149. package/types/lib/core/projectConfig.d.ts +6 -2
  150. package/types/lib/core/projectConfig.d.ts.map +1 -1
  151. package/types/lib/core/runState.d.ts +29 -0
  152. package/types/lib/core/runState.d.ts.map +1 -0
  153. package/types/lib/core/toml.d.ts +20 -0
  154. package/types/lib/core/toml.d.ts.map +1 -0
  155. package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
  156. package/types/lib/ecosystems/bunutils.d.ts +12 -5
  157. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  158. package/types/lib/ecosystems/cargoCache.d.ts +12 -0
  159. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
  160. package/types/lib/ecosystems/caxa.d.ts +14 -0
  161. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  162. package/types/lib/ecosystems/cmakeResolver.d.ts +28 -3
  163. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  164. package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
  165. package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
  166. package/types/lib/ecosystems/cppEvidence.d.ts +17 -1
  167. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  168. package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
  169. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
  170. package/types/lib/ecosystems/denoutils.d.ts +9 -3
  171. package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
  172. package/types/lib/ecosystems/ecosystems.d.ts +62 -13
  173. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  174. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  175. package/types/lib/ecosystems/gradleutils.d.ts +70 -4
  176. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  177. package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
  178. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  179. package/types/lib/ecosystems/kosi.d.ts +13 -0
  180. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  181. package/types/lib/ecosystems/npmutils.d.ts +57 -0
  182. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  183. package/types/lib/ecosystems/parsers-dotnet.d.ts +118 -4
  184. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  185. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
  186. package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
  187. package/types/lib/ecosystems/parsers-go.d.ts +54 -2
  188. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  189. package/types/lib/ecosystems/parsers-js.d.ts +16 -5
  190. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  191. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
  192. package/types/lib/ecosystems/parsers-jvm.d.ts +46 -1
  193. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  194. package/types/lib/ecosystems/parsers-misc.d.ts +70 -4
  195. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  196. package/types/lib/ecosystems/parsers-python.d.ts +53 -3
  197. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  198. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  199. package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
  200. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
  201. package/types/lib/ecosystems/pythonutils.d.ts +27 -2
  202. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  203. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  204. package/types/lib/ecosystems/rubyutils.d.ts +164 -3
  205. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  206. package/types/lib/ecosystems/rusi.d.ts +13 -0
  207. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  208. package/types/lib/ecosystems/sbtutils.d.ts +88 -2
  209. package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
  210. package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
  211. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
  212. package/types/lib/ecosystems/scalasem.d.ts +311 -0
  213. package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
  214. package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
  215. package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
  216. package/types/lib/ecosystems/utils.d.ts +2 -2
  217. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  218. package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
  219. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
  220. package/types/lib/evinser/evinser.d.ts +132 -15
  221. package/types/lib/evinser/evinser.d.ts.map +1 -1
  222. package/types/lib/evinser/swiftsem.d.ts +280 -15
  223. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  224. package/types/lib/helpers/bomSigner.d.ts +182 -13
  225. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  226. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  227. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  228. package/types/lib/inventory/atomUtils.d.ts +219 -5
  229. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  230. package/types/lib/inventory/auditCategories.d.ts +3 -0
  231. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  232. package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
  233. package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
  234. package/types/lib/inventory/cacheCommands.d.ts +2 -1
  235. package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
  236. package/types/lib/inventory/cdxrs.d.ts +8 -3
  237. package/types/lib/inventory/cdxrs.d.ts.map +1 -1
  238. package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
  239. package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
  240. package/types/lib/inventory/deps.d.ts +59 -1
  241. package/types/lib/inventory/deps.d.ts.map +1 -1
  242. package/types/lib/inventory/depsUtils.d.ts +15 -0
  243. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  244. package/types/lib/inventory/display.d.ts.map +1 -1
  245. package/types/lib/inventory/dosai.d.ts +121 -9
  246. package/types/lib/inventory/dosai.d.ts.map +1 -1
  247. package/types/lib/inventory/dosaiParsers.d.ts +53 -7
  248. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  249. package/types/lib/inventory/dynamic.d.ts +10 -0
  250. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  251. package/types/lib/inventory/evidenceUtils.d.ts +19 -4
  252. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  253. package/types/lib/inventory/fetchBatch.d.ts +83 -14
  254. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  255. package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
  256. package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
  257. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  258. package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
  259. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
  260. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  261. package/types/lib/inventory/parentSelection.d.ts +34 -0
  262. package/types/lib/inventory/parentSelection.d.ts.map +1 -0
  263. package/types/lib/inventory/plugins.d.ts.map +1 -1
  264. package/types/lib/inventory/protobom.d.ts.map +1 -1
  265. package/types/lib/inventory/purl.d.ts +71 -2
  266. package/types/lib/inventory/purl.d.ts.map +1 -1
  267. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  268. package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
  269. package/types/lib/inventory/scalaCoords.d.ts +80 -0
  270. package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
  271. package/types/lib/inventory/source.d.ts.map +1 -1
  272. package/types/lib/inventory/spdx.d.ts +9 -0
  273. package/types/lib/inventory/spdx.d.ts.map +1 -1
  274. package/types/lib/inventory/traceRunner.d.ts +59 -1
  275. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  276. package/types/lib/inventory/tracedCrypto.d.ts +42 -0
  277. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
  278. package/types/lib/managers/binary.d.ts +30 -1
  279. package/types/lib/managers/binary.d.ts.map +1 -1
  280. package/types/lib/managers/docker.d.ts +29 -0
  281. package/types/lib/managers/docker.d.ts.map +1 -1
  282. package/types/lib/managers/oci.d.ts +17 -1
  283. package/types/lib/managers/oci.d.ts.map +1 -1
  284. package/types/lib/parsers/cmakeCache.d.ts +16 -0
  285. package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
  286. package/types/lib/parsers/cmakeLists.d.ts +77 -0
  287. package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
  288. package/types/lib/parsers/cmakePresets.d.ts +78 -0
  289. package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
  290. package/types/lib/parsers/compileCommands.d.ts +115 -0
  291. package/types/lib/parsers/compileCommands.d.ts.map +1 -0
  292. package/types/lib/parsers/largeJson.d.ts +52 -0
  293. package/types/lib/parsers/largeJson.d.ts.map +1 -0
  294. package/types/lib/parsers/npmrc.d.ts +5 -0
  295. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  296. package/types/lib/parsers/strictJson.d.ts +46 -0
  297. package/types/lib/parsers/strictJson.d.ts.map +1 -0
  298. package/types/lib/parsers/terraformConfig.d.ts +70 -0
  299. package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
  300. package/types/lib/parsers/terraformSource.d.ts +66 -0
  301. package/types/lib/parsers/terraformSource.d.ts.map +1 -0
  302. package/types/lib/server/server.d.ts.map +1 -1
  303. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  304. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  305. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  306. package/types/lib/validator/index.d.ts +18 -1
  307. package/types/lib/validator/index.d.ts.map +1 -1
  308. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  309. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  310. package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
  311. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
@@ -7,7 +7,6 @@ import { Purl } from "@cdxgen/cdx-purl";
7
7
  import { coerce, diff, prerelease } from "semver";
8
8
 
9
9
  import {
10
- cdxgenAgent,
11
10
  createDryRunError,
12
11
  DEBUG_MODE,
13
12
  hasDangerousUnicode,
@@ -24,12 +23,14 @@ import {
24
23
  } from "../core/fs.js";
25
24
  import { thoughtLog } from "../core/logger.js";
26
25
  import { isValidDriveRoot, isWin } from "../core/paths.js";
26
+ import { registerRunReset } from "../core/runState.js";
27
27
  import {
28
+ gatedGet,
28
29
  prefetchEnabled,
29
30
  prefetchedResponse,
30
31
  prefetchJson,
31
- withHostRateLimit,
32
32
  } from "./fetchBatch.js";
33
+ import { canonicalPurlFromLooseString } from "./purl.js";
33
34
 
34
35
  /** Raised when a caller omits a registry helper that the purl type requires. */
35
36
  class MissingRegistryHelperError extends Error {}
@@ -954,6 +955,36 @@ function packageNameForLookup(purlObj) {
954
955
  return `${namespace}/${purlObj.name}`;
955
956
  }
956
957
 
958
+ /**
959
+ * Read a purl supplied as a scan source.
960
+ *
961
+ * People type purls the way their package manager prints versions and URLs:
962
+ * `pkg:cargo/wasi@0.11.0+wasi-snapshot-preview1` or
963
+ * `pkg:npm/foo?vcs_url=git+https://...`. cdx-purl requires those `+` to be
964
+ * escaped, so a purl that does not parse strictly is split on its separators
965
+ * and rebuilt with every component escaped once. The rebuilt purl is then
966
+ * parsed strictly, so validation always inspects cdx-purl's reading of it.
967
+ *
968
+ * @param {string} purlString Package URL as supplied
969
+ * @returns {Purl|undefined} Parsed purl, or undefined when none can be recovered
970
+ */
971
+ function parsePurlSourceInput(purlString) {
972
+ try {
973
+ return Purl.parse(purlString);
974
+ } catch {
975
+ // Fall through to the structural rebuild.
976
+ }
977
+ const canonical = canonicalPurlFromLooseString(purlString);
978
+ if (!canonical) {
979
+ return undefined;
980
+ }
981
+ try {
982
+ return Purl.parse(canonical);
983
+ } catch {
984
+ return undefined;
985
+ }
986
+ }
987
+
957
988
  /**
958
989
  * Validate package URL source input and return an error object when invalid.
959
990
  *
@@ -964,10 +995,8 @@ export function validatePurlSource(purlString) {
964
995
  if (!maybePurlSource(purlString)) {
965
996
  return null;
966
997
  }
967
- let purlObj;
968
- try {
969
- purlObj = Purl.parse(purlString);
970
- } catch (_err) {
998
+ const purlObj = parsePurlSourceInput(purlString);
999
+ if (!purlObj) {
971
1000
  return {
972
1001
  status: 400,
973
1002
  error: "Invalid purl source",
@@ -1136,6 +1165,8 @@ export function resetGitUrlSourcePrefetch() {
1136
1165
  gitUrlSourcePrefetch = new Map();
1137
1166
  }
1138
1167
 
1168
+ registerRunReset("source:registry-documents", resetGitUrlSourcePrefetch);
1169
+
1139
1170
  /**
1140
1171
  * Prefetch the registry documents a list of purls will be resolved from.
1141
1172
  *
@@ -1189,12 +1220,10 @@ async function fetchRegistryDocument(lookup) {
1189
1220
  // whose policy is one request per second however the request was reached.
1190
1221
  return (
1191
1222
  prefetchedResponse(gitUrlSourcePrefetch, lookup.url) ||
1192
- (await withHostRateLimit(lookup.url, () =>
1193
- cdxgenAgent.get(lookup.url, {
1194
- ...(lookup.accept ? { headers: { Accept: lookup.accept } } : {}),
1195
- responseType: "json",
1196
- }),
1197
- ))
1223
+ (await gatedGet(lookup.url, {
1224
+ ...(lookup.accept ? { headers: { Accept: lookup.accept } } : {}),
1225
+ responseType: "json",
1226
+ }))
1198
1227
  );
1199
1228
  }
1200
1229
 
@@ -1209,24 +1238,13 @@ function parsePurlForSourceResolution(purlString) {
1209
1238
  if (!maybePurlSource(purlString) || validatePurlSource(purlString)) {
1210
1239
  return undefined;
1211
1240
  }
1212
- let purlObj;
1213
- try {
1214
- // Some purls in the wild use unencoded + in qualifier values (e.g.
1215
- // git+https:// in vcs_url). cdx-purl requires percent-encoding; normalize
1216
- // the qualifier section before parsing so real-world purls resolve.
1217
- const qIdx = purlString.indexOf("?");
1218
- const safePurlString =
1219
- qIdx >= 0
1220
- ? purlString.slice(0, qIdx + 1) +
1221
- purlString.slice(qIdx + 1).replaceAll("+", "%2B")
1222
- : purlString;
1223
- purlObj = Purl.parse(safePurlString);
1224
- } catch (err) {
1241
+ // Purls in the wild leave `+` unescaped in versions and in qualifier values
1242
+ // such as `vcs_url=git+https://`; the shared reader rebuilds them.
1243
+ const purlObj = parsePurlSourceInput(purlString);
1244
+ if (!purlObj) {
1225
1245
  if (DEBUG_MODE) {
1226
- const errorMessage = err?.message || String(err);
1227
1246
  thoughtLog("Unable to resolve repository URL for purl.", {
1228
1247
  purlString,
1229
- errorMessage,
1230
1248
  });
1231
1249
  }
1232
1250
  return undefined;
@@ -343,12 +343,113 @@ export function findLicenseId(name) {
343
343
  : name;
344
344
  }
345
345
 
346
+ /**
347
+ * Identify a license from the title and opening of its full text. A license
348
+ * text quotes other licenses (the GNU GPL's text names the GNU Lesser GPL),
349
+ * so the substring scan of `guessLicenseId` alone can name the wrong one.
350
+ *
351
+ * @param {string} content License file contents
352
+ * @returns {string|undefined} SPDX license id or expression
353
+ */
354
+ export function licenseIdFromText(content) {
355
+ if (typeof content !== "string" || !content.trim()) {
356
+ return undefined;
357
+ }
358
+ const head = content.slice(0, 4000).replace(/\s+/g, " ").trim().toUpperCase();
359
+ const version = (prefix) => {
360
+ const at = head.indexOf(prefix);
361
+ if (at < 0) {
362
+ return undefined;
363
+ }
364
+ const tail = head.slice(at + prefix.length, at + prefix.length + 60);
365
+ const match = tail.match(/VERSION (\d(?:\.\d)?)/);
366
+ return match?.[1];
367
+ };
368
+ // the first lines name the license; quotations of others come later, so
369
+ // of the GNU licenses the one named first is the one the text is
370
+ const title = head.slice(0, 200);
371
+ const gnu = [
372
+ ["GNU AFFERO GENERAL PUBLIC LICENSE", { 3: "AGPL-3.0-only" }],
373
+ [
374
+ "GNU LESSER GENERAL PUBLIC LICENSE",
375
+ { 3: "LGPL-3.0-only", 2.1: "LGPL-2.1-only", 2: "LGPL-2.0-only" },
376
+ ],
377
+ [
378
+ "GNU LIBRARY GENERAL PUBLIC LICENSE",
379
+ { 2.1: "LGPL-2.1-only", 2: "LGPL-2.0-only" },
380
+ ],
381
+ [
382
+ "GNU GENERAL PUBLIC LICENSE",
383
+ { 3: "GPL-3.0-only", 2: "GPL-2.0-only", 1: "GPL-1.0-only" },
384
+ ],
385
+ ]
386
+ .map(([name, ids]) => ({ name, ids, at: title.indexOf(name) }))
387
+ .filter((t) => t.at >= 0)
388
+ .sort((a, b) => a.at - b.at)[0];
389
+ if (gnu) {
390
+ return gnu.ids[version(gnu.name)];
391
+ }
392
+ if (head.includes("APACHE LICENSE") && head.includes("VERSION 2.0")) {
393
+ return head.includes("LLVM EXCEPTION")
394
+ ? "Apache-2.0 WITH LLVM-exception"
395
+ : "Apache-2.0";
396
+ }
397
+ if (
398
+ title.includes("MOZILLA PUBLIC LICENSE") &&
399
+ head.includes("VERSION 2.0")
400
+ ) {
401
+ return "MPL-2.0";
402
+ }
403
+ if (title.includes("BOOST SOFTWARE LICENSE - VERSION 1.0")) {
404
+ return "BSL-1.0";
405
+ }
406
+ if (
407
+ head.includes(
408
+ "THIS IS FREE AND UNENCUMBERED SOFTWARE RELEASED INTO THE PUBLIC DOMAIN",
409
+ )
410
+ ) {
411
+ return "Unlicense";
412
+ }
413
+ if (head.includes("PERMISSION IS HEREBY GRANTED, FREE OF CHARGE")) {
414
+ return "MIT";
415
+ }
416
+ if (
417
+ head.includes(
418
+ "PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED",
419
+ )
420
+ ) {
421
+ return "ISC";
422
+ }
423
+ if (
424
+ head.includes("THIS SOFTWARE IS PROVIDED 'AS-IS'") &&
425
+ head.includes(
426
+ "PERMISSION IS GRANTED TO ANYONE TO USE THIS SOFTWARE FOR ANY PURPOSE",
427
+ )
428
+ ) {
429
+ return "Zlib";
430
+ }
431
+ if (head.includes("REDISTRIBUTION AND USE IN SOURCE AND BINARY FORMS")) {
432
+ if (head.includes("ALL ADVERTISING MATERIALS MENTIONING")) {
433
+ return "BSD-4-Clause";
434
+ }
435
+ return head.includes("NEITHER THE NAME") ||
436
+ head.includes("THE NAME OF THE AUTHOR MAY NOT BE USED")
437
+ ? "BSD-3-Clause"
438
+ : "BSD-2-Clause";
439
+ }
440
+ return undefined;
441
+ }
442
+
346
443
  /**
347
444
  * Method to guess the spdx license id from license contents
348
445
  *
349
446
  * @param {string} content License file contents
350
447
  */
351
448
  export function guessLicenseId(content) {
449
+ const fromText = licenseIdFromText(content);
450
+ if (fromText) {
451
+ return fromText;
452
+ }
352
453
  content = content.replace(/\n/g, " ");
353
454
  for (const l of licenseMapping) {
354
455
  for (const j in l.names) {
@@ -20,6 +20,17 @@ try {
20
20
  SaferExec = undefined;
21
21
  }
22
22
 
23
+ /**
24
+ * Whether the @cdxgen/safer-exec tracing runtime could be loaded. When false,
25
+ * dynamic tracing cannot run at all, and every trace would otherwise exit 0
26
+ * with a valid but empty BOM, hiding the broken installation (#4378).
27
+ *
28
+ * @returns {boolean} True when the tracing runtime is available
29
+ */
30
+ export function isSaferExecAvailable() {
31
+ return Boolean(SaferExec);
32
+ }
33
+
23
34
  const __filename = fileURLToPath(import.meta.url);
24
35
  const __dirname = dirname(__filename);
25
36
 
@@ -66,6 +77,7 @@ export function parseCommand(cmdStr) {
66
77
  export function resolveSaferExecBinary() {
67
78
  const currentPlatform = platform();
68
79
  const currentArch = arch();
80
+ const isWindows = currentPlatform === "win32";
69
81
  let pkgName = "";
70
82
 
71
83
  if (currentPlatform === "darwin") {
@@ -80,6 +92,14 @@ export function resolveSaferExecBinary() {
80
92
  } else if (currentArch === "arm64") {
81
93
  pkgName = "@cdxgen/safer-exec-linux-arm64";
82
94
  }
95
+ } else if (isWindows) {
96
+ // The Windows packages ship the MXC executor (wxc-exec.exe) next to the
97
+ // engine, which is where the engine looks for it.
98
+ if (currentArch === "x64") {
99
+ pkgName = "@cdxgen/safer-exec-windows-amd64";
100
+ } else if (currentArch === "arm64") {
101
+ pkgName = "@cdxgen/safer-exec-windows-arm64";
102
+ }
83
103
  }
84
104
 
85
105
  if (!pkgName) {
@@ -105,8 +125,15 @@ export function resolveSaferExecBinary() {
105
125
  }
106
126
 
107
127
  for (const modulesDir of searchDirs) {
108
- // Direct structure under node_modules
109
- const directPath = join(modulesDir, pkgName, "bin", "safer-exec");
128
+ // Direct structure under node_modules. The Go runtime inside the
129
+ // platform package is bin/safer-exec-rt(.exe); "safer-exec" is the JS
130
+ // dispatcher's npm bin name, not a file on disk.
131
+ const directPath = join(
132
+ modulesDir,
133
+ pkgName,
134
+ "bin",
135
+ isWindows ? "safer-exec-rt.exe" : "safer-exec-rt",
136
+ );
110
137
  let realDirectPath;
111
138
  try {
112
139
  realDirectPath = realpathSync(directPath);
@@ -114,10 +141,12 @@ export function resolveSaferExecBinary() {
114
141
  realDirectPath = directPath;
115
142
  }
116
143
  if (existsSync(realDirectPath)) {
117
- try {
118
- chmodSync(realDirectPath, 0o755);
119
- } catch (_err) {
120
- // ignore
144
+ if (!isWindows) {
145
+ try {
146
+ chmodSync(realDirectPath, 0o755);
147
+ } catch (_err) {
148
+ // ignore
149
+ }
121
150
  }
122
151
  return realDirectPath;
123
152
  }
@@ -157,7 +186,19 @@ export function resolveSaferExecBinary() {
157
186
  * @param {boolean} [options.traceExec] - Log every child process spawned
158
187
  * @param {string[]} [options.allowExec] - Executables the command is allowed to run
159
188
  * @param {string[]} [options.blockExec] - Executables to block from running
160
- * @returns {Promise<{libPaths: string[], httpAccessEntries: Object[]}>} Collected libraries and HTTP URLs
189
+ * @param {boolean} [options.proxyEgress] - Route egress through safer-exec's hostname-pinning proxy (network stays enabled; only allowHosts are reachable)
190
+ * @param {boolean} [options.allowLoopback] - Allow loopback connections
191
+ * @param {boolean} [options.dryRun] - Deny every filesystem and network side effect and collect a dry-run report
192
+ * @param {string} [options.policy] - Named safer-exec ecosystem policy (npm, pypi, maven, ...)
193
+ * @param {string} [options.policyFile] - Path to a safer-exec JSON policy file
194
+ * @param {boolean} [options.blockInterpreters] - Block interpreters with sandbox or task-port exemptions (macOS)
195
+ * @param {boolean} [options.denyPersistenceWrites] - Deny writes to auto-execution and persistence locations
196
+ * @param {boolean} [options.privateTmp] - Private tmpfs /tmp and /var/tmp (Linux)
197
+ * @param {string} [options.protectHome] - Home isolation: read-only, tmpfs, or off (Linux)
198
+ * @returns {Promise<{libPaths: string[], httpAccessEntries: Object[], egressEntries?: Object[], dryRunSummary?: Object}>} Collected libraries, HTTP URLs, proxy egress decisions, and the dry-run summary
199
+ * @throws {Error} When the @cdxgen/safer-exec tracing runtime is not installed
200
+ * (for example after pruning it from a standalone payload), since the traced
201
+ * command could never run and the BOM would silently be empty
161
202
  */
162
203
  export async function executeAndTrace(commandStr, workingDir, options = {}) {
163
204
  const emptyResult = { libPaths: [], httpAccessEntries: [] };
@@ -174,11 +215,23 @@ export async function executeAndTrace(commandStr, workingDir, options = {}) {
174
215
  );
175
216
 
176
217
  if (!SaferExec) {
177
- return emptyResult;
218
+ throw new Error(
219
+ "Dynamic tracing requires the @cdxgen/safer-exec package, which could not be loaded from this installation. Re-install with optional dependencies enabled, or use the official tracebom standalone binary.",
220
+ );
221
+ }
222
+
223
+ const exec = new SaferExec();
224
+ // Policies are applied first so explicit options below take precedence, and
225
+ // outside the try so an unknown policy or unreadable policy file fails the
226
+ // run instead of producing an empty BOM.
227
+ if (options.policy) {
228
+ exec.applyPolicy(options.policy);
229
+ }
230
+ if (options.policyFile) {
231
+ exec.applyPolicyFile(options.policyFile);
178
232
  }
179
233
 
180
234
  try {
181
- const exec = new SaferExec();
182
235
  if (workingDir) {
183
236
  exec.workingDir(workingDir);
184
237
  }
@@ -192,9 +245,49 @@ export async function executeAndTrace(commandStr, workingDir, options = {}) {
192
245
  exec.traceLibraries().suppressLibLoadStderr(true).enableAudit();
193
246
 
194
247
  // Apply sandbox options
195
- if (options.disableNetwork !== false && !options.traceHTTPURLs) {
248
+ // The egress proxy runs on the host side and must be reachable, so it
249
+ // needs the network enabled (an isolated namespace would bypass it).
250
+ if (
251
+ options.disableNetwork !== false &&
252
+ !options.traceHTTPURLs &&
253
+ !options.proxyEgress
254
+ ) {
196
255
  exec.disableNetwork();
197
256
  }
257
+ if (options.proxyEgress) {
258
+ if (!options.allowHosts?.length && !options.allowUrls?.length) {
259
+ console.warn(
260
+ "[cdxgen trace] egress proxy enabled without --allow-host: every outbound connection will be denied.",
261
+ );
262
+ }
263
+ exec.proxyEgress();
264
+ }
265
+ if (options.allowLoopback) {
266
+ exec.allowLoopback();
267
+ }
268
+ if (options.dryRun) {
269
+ exec.enableDryRun();
270
+ }
271
+ if (platform() === "win32" && options.blockInterpreters) {
272
+ // safer-exec reports --private-tmp and --protect-home on Windows when a
273
+ // grant defeats them, but --block-interpreters is macOS-only and stays
274
+ // silent (cdxgen/safer-exec#4).
275
+ console.warn(
276
+ "[cdxgen trace] --block-interpreters has no effect on Windows.",
277
+ );
278
+ }
279
+ if (options.blockInterpreters) {
280
+ exec.blockInterpreters();
281
+ }
282
+ if (options.denyPersistenceWrites) {
283
+ exec.denyPersistenceWrites();
284
+ }
285
+ if (options.privateTmp) {
286
+ exec.privateTmp();
287
+ }
288
+ if (options.protectHome && options.protectHome !== "off") {
289
+ exec.protectHome(options.protectHome);
290
+ }
198
291
  if (options.readPaths?.length) {
199
292
  exec.readPaths(options.readPaths);
200
293
  }
@@ -306,25 +399,86 @@ export async function executeAndTrace(commandStr, workingDir, options = {}) {
306
399
  }
307
400
 
308
401
  const result = await exec.run(cmd, args);
309
- if (result && result.exitCode !== 0 && result.stderr) {
310
- if (result.stderr.includes("[safer-exec] Error:")) {
311
- console.error(
312
- "Tracing launcher execution failed:",
313
- result.stderr.trim(),
402
+ const stderr = result?.stderr || "";
403
+ const { warnings, error: engineError } = engineMessages(stderr);
404
+ // Windows records DLL loads through an ETW session, which only an
405
+ // elevated process (or a Performance Log Users member) may start.
406
+ const etwDenied =
407
+ platform() === "win32" &&
408
+ warnings.some(
409
+ (line) =>
410
+ line.includes("DLL tracing:") &&
411
+ line.includes("administrator rights"),
412
+ );
413
+ // run() captures the engine's stderr, so a control the sandbox could not
414
+ // apply would otherwise read like one it did.
415
+ for (const line of warnings) {
416
+ if (etwDenied && line.includes("DLL tracing:")) {
417
+ continue;
418
+ }
419
+ console.warn(line);
420
+ }
421
+ if (etwDenied) {
422
+ console.warn(
423
+ "[cdxgen trace] library tracing on Windows needs an elevated prompt (or membership in the Performance Log Users group); the BOM has no library components. Egress services from --proxy-egress are still recorded.",
424
+ );
425
+ }
426
+ if (result && result.exitCode !== 0) {
427
+ if (engineError) {
428
+ console.error(engineError);
429
+ if (options.strict) {
430
+ // Under --strict the sandbox refused the run; an empty BOM would
431
+ // read as a command that loaded nothing.
432
+ throw Object.assign(
433
+ new Error(`Tracing refused under --strict: ${engineError}`),
434
+ { strictRefusal: true },
435
+ );
436
+ }
437
+ } else if (stderr.includes("[safer-exec] Error:")) {
438
+ console.error("Tracing launcher execution failed:", stderr.trim());
439
+ } else {
440
+ // A failing command usually means a partial trace (for example a
441
+ // sandbox denial), so the BOM may be incomplete.
442
+ console.warn(
443
+ `[cdxgen trace] traced command exited with code ${result.exitCode}; the BOM may be incomplete.`,
314
444
  );
315
445
  }
316
- } else if (options.traceHTTPURLs && result?.stderr) {
317
- // Surface eBPF/http-trace warnings even when exit code is 0
318
- const stderr = result.stderr || "";
446
+ }
447
+ if (options.traceHTTPURLs) {
448
+ // eBPF URL tracing needs CAP_BPF/CAP_PERFMON. Surface why it was not
449
+ // available whatever the command's exit code, since the BOM otherwise
450
+ // silently lacks services.
451
+ const httpLines = stderr
452
+ .split("\n")
453
+ .filter(
454
+ (line) =>
455
+ line.includes("http-trace") ||
456
+ line.includes("httptrace") ||
457
+ line.includes("SSL/TLS libraries"),
458
+ );
319
459
  if (
320
- stderr.includes("http-trace") ||
321
- stderr.includes("httptrace") ||
322
- stderr.includes("SSL/TLS libraries")
460
+ httpLines.some((line) => /warning|not supported|failed/i.test(line))
323
461
  ) {
324
- console.warn("[cdxgen trace] HTTP URL tracing warning:", stderr.trim());
462
+ console.warn(
463
+ "[cdxgen trace] HTTP URL tracing unavailable or degraded (it needs root or CAP_BPF and CAP_PERFMON):",
464
+ httpLines.join("\n"),
465
+ );
325
466
  }
326
467
  }
327
468
 
469
+ // Without an event source (strace on Linux) a dry run still denies every
470
+ // side effect but reports nothing, which must not read as "no activity".
471
+ let dryRunSummary = result?.dryRun?.summary;
472
+ if (
473
+ options.dryRun &&
474
+ result?.stderr?.includes("dry-run cannot capture attempted operations")
475
+ ) {
476
+ console.warn(
477
+ "[cdxgen trace] dry run could not capture attempted operations on this host (install strace or relax ptrace_scope); the BOM records no counts.",
478
+ );
479
+ dryRunSummary = { captured: false };
480
+ }
481
+
328
482
  // Read and parse CBOM file if temp/explicit path was used
329
483
  let cryptoComponents = [];
330
484
  const cbomPathToRead = options.cbom || tempCbomPath;
@@ -377,6 +531,8 @@ export async function executeAndTrace(commandStr, workingDir, options = {}) {
377
531
  httpAccessEntries: [...collectedUrls, ...urls],
378
532
  cryptoEntries: [...collectedCrypto, ...cryptoLogs],
379
533
  cryptoComponents,
534
+ egressEntries: parseEgressEntries(result.auditLog),
535
+ dryRunSummary,
380
536
  };
381
537
  }
382
538
 
@@ -385,13 +541,119 @@ export async function executeAndTrace(commandStr, workingDir, options = {}) {
385
541
  httpAccessEntries: collectedUrls,
386
542
  cryptoEntries: collectedCrypto,
387
543
  cryptoComponents,
544
+ egressEntries: [],
545
+ dryRunSummary,
388
546
  };
389
547
  } catch (err) {
548
+ if (err?.strictRefusal) {
549
+ throw err;
550
+ }
390
551
  console.error("Tracing command execution failed:", err);
391
552
  }
392
553
  return emptyResult;
393
554
  }
394
555
 
556
+ /**
557
+ * Picks the safer-exec engine's own lines out of the stderr it shares with the
558
+ * traced command. Warnings start with "safer-exec: warning:"; when the engine
559
+ * stops a run, its error is the last other "safer-exec: " line it prints.
560
+ *
561
+ * @param {string} stderr Captured stderr of a safer-exec run
562
+ * @returns {{warnings: string[], error: string|undefined}} Unique engine warnings, and the engine's error if any
563
+ */
564
+ export function engineMessages(stderr) {
565
+ const lines = String(stderr || "")
566
+ .split(/\r?\n/)
567
+ .filter((line) => line.startsWith("safer-exec: "));
568
+ const warnings = [
569
+ ...new Set(lines.filter((line) => line.startsWith("safer-exec: warning:"))),
570
+ ];
571
+ const others = lines.filter(
572
+ (line) => !line.startsWith("safer-exec: warning:"),
573
+ );
574
+ return { warnings, error: others.at(-1) };
575
+ }
576
+
577
+ /**
578
+ * Returns the sorted, de-duplicated parameter names of a query string.
579
+ *
580
+ * @param {string} query - Raw query string, with or without a leading "?"
581
+ * @returns {string} Comma-separated parameter names, or "" when there are none
582
+ */
583
+ function queryParameterNames(query) {
584
+ if (!query) {
585
+ return "";
586
+ }
587
+ const names = new Set();
588
+ for (const pair of String(query).replace(/^\?/, "").split("&")) {
589
+ const eqIdx = pair.indexOf("=");
590
+ const name = (eqIdx >= 0 ? pair.substring(0, eqIdx) : pair).trim();
591
+ if (name) {
592
+ names.add(name);
593
+ }
594
+ }
595
+ return Array.from(names).sort().join(",");
596
+ }
597
+
598
+ /**
599
+ * Extracts the egress proxy's decisions from a safer-exec audit log.
600
+ * "proxy-connect" targets are "host:port"; "proxy-violation" targets are
601
+ * "CONNECT host:port" or "METHOD http://host[:port]/path". Only the authority
602
+ * is kept, never a path or query.
603
+ *
604
+ * @param {Object[]} auditLog - safer-exec audit entries
605
+ * @returns {{host: string, port: number, protocol: string, decision: string}[]} Unique egress decisions
606
+ */
607
+ export function parseEgressEntries(auditLog) {
608
+ const seen = new Set();
609
+ const entries = [];
610
+ for (const e of auditLog || []) {
611
+ if (e?.type !== "proxy-connect" && e?.type !== "proxy-violation") {
612
+ continue;
613
+ }
614
+ const target = String(e.target || "");
615
+ let authority = target;
616
+ let protocol = "https";
617
+ if (e.type === "proxy-connect") {
618
+ protocol = e.details === "http" ? "http" : "https";
619
+ } else {
620
+ const spaceIdx = target.indexOf(" ");
621
+ authority = spaceIdx >= 0 ? target.substring(spaceIdx + 1) : target;
622
+ const schemeIdx = authority.indexOf("://");
623
+ if (schemeIdx >= 0) {
624
+ protocol = authority.substring(0, schemeIdx);
625
+ authority = authority.substring(schemeIdx + 3);
626
+ const slashIdx = authority.indexOf("/");
627
+ if (slashIdx >= 0) {
628
+ authority = authority.substring(0, slashIdx);
629
+ }
630
+ }
631
+ }
632
+ let host = authority;
633
+ let port = protocol === "http" ? 80 : 443;
634
+ const colonIdx = authority.lastIndexOf(":");
635
+ if (colonIdx > 0 && !authority.endsWith("]")) {
636
+ const parsedPort = Number.parseInt(authority.substring(colonIdx + 1), 10);
637
+ if (!Number.isNaN(parsedPort)) {
638
+ port = parsedPort;
639
+ host = authority.substring(0, colonIdx);
640
+ }
641
+ }
642
+ host = host.replace(/^\[|\]$/g, "").toLowerCase();
643
+ if (!host) {
644
+ continue;
645
+ }
646
+ const decision = e.type === "proxy-connect" ? "allowed" : "denied";
647
+ const key = `${decision}|${protocol}|${host}|${port}`;
648
+ if (seen.has(key)) {
649
+ continue;
650
+ }
651
+ seen.add(key);
652
+ entries.push({ host, port, protocol, decision });
653
+ }
654
+ return entries;
655
+ }
656
+
395
657
  /**
396
658
  * Groups HTTP access entries into a CycloneDX services-ready map.
397
659
  * Each unique (host, port, protocol) combination becomes a service.
@@ -409,7 +671,9 @@ export function groupHttpEntriesToServices(httpAccessEntries) {
409
671
  properties: [],
410
672
  };
411
673
  }
412
- const endpoint = `https://${entry.host}${entry.port && entry.port !== 443 ? `:${entry.port}` : ""}${entry.path || "/"}`;
674
+ const scheme = entry.protocol === "http" ? "http" : "https";
675
+ const defaultPort = scheme === "http" ? 80 : 443;
676
+ const endpoint = `${scheme}://${entry.host}${entry.port && entry.port !== defaultPort ? `:${entry.port}` : ""}${entry.path || "/"}`;
413
677
  servicesMap[serviceName].endpoints.add(endpoint);
414
678
  if (entry.method) {
415
679
  const methodProp = {
@@ -424,8 +688,14 @@ export function groupHttpEntriesToServices(httpAccessEntries) {
424
688
  servicesMap[serviceName].properties.push(methodProp);
425
689
  }
426
690
  }
427
- if (entry.query) {
428
- const queryProp = { name: "cdx:dynamic:httpQuery", value: entry.query };
691
+ // Query values can carry tokens and signatures, so only the parameter
692
+ // names are recorded.
693
+ const queryParams = queryParameterNames(entry.query);
694
+ if (queryParams) {
695
+ const queryProp = {
696
+ name: "cdx:dynamic:httpQueryParams",
697
+ value: queryParams,
698
+ };
429
699
  if (
430
700
  !servicesMap[serviceName].properties.some(
431
701
  (p) => p.name === queryProp.name && p.value === queryProp.value,