@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
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
  }
@@ -857,7 +1025,8 @@ export const MAX_BUFFER =
857
1025
  export function getAllFiles(dirPath, pattern, options = {}) {
858
1026
  let ignoreList = [];
859
1027
  if (!options.noIgnore) {
860
- ignoreList.push("**/.hg/**", "**/.git/**");
1028
+ // .chen holds chen's AST cache, which atom writes into a C/C++ source tree
1029
+ ignoreList.push("**/.hg/**", "**/.git/**", "**/.chen/**");
861
1030
  // Only ignore node_modules if the caller is not looking for package.json
862
1031
  if (!pattern.includes("package.json") && !options.includeNodeModulesDir) {
863
1032
  ignoreList.push("**/node_modules/**");
@@ -1,4 +1,5 @@
1
1
  import { Buffer } from "node:buffer";
2
+ import { createHash } from "node:crypto";
2
3
  import process from "node:process";
3
4
  import { brotliDecompressSync, gunzipSync, inflateSync } from "node:zlib";
4
5
 
@@ -9,6 +10,8 @@ import {
9
10
  request as undiciRequest,
10
11
  } from "undici";
11
12
 
13
+ import { registerRunReset } from "./runState.js";
14
+
12
15
  // Environment variables here are read with raw `process.env` rather than
13
16
  // `readEnvironmentVariable`: that helper lives in activity.js, which imports
14
17
  // this module, so routing these through it would form an import cycle.
@@ -41,9 +44,8 @@ import {
41
44
  * `Content-Encoding` header, matching `got`'s transparent decoding.
42
45
  * - Proxy support via the standard `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY`
43
46
  * environment variables (through undici's `EnvHttpProxyAgent`).
44
- * - An in-memory GET response cache (enabled by default, disabled by setting
45
- * the `CDXGEN_NO_CACHE` environment variable) that replaces the got + Keyv
46
- * cache cdxgen previously relied on.
47
+ * - An in-memory GET response cache that replaces the got + Keyv cache cdxgen
48
+ * previously relied on. A request opts out with `cache: false`.
47
49
  * - Response objects exposing `statusCode`, `headers`, `body`, `rawBody`,
48
50
  * `url` and `request.options`.
49
51
  * - A lazily-resolved `.json()` method on the returned promise, mirroring
@@ -137,7 +139,12 @@ function mergeHeaders(base = {}, override = {}) {
137
139
  * `beforeError` arrays.
138
140
  */
139
141
  function mergeHooks(base = {}, override = {}) {
140
- const hookNames = ["beforeRequest", "afterResponse", "beforeError"];
142
+ const hookNames = [
143
+ "beforeRequest",
144
+ "beforeNetwork",
145
+ "afterResponse",
146
+ "beforeError",
147
+ ];
141
148
  const merged = {};
142
149
  for (const name of hookNames) {
143
150
  merged[name] = [...(base?.[name] || []), ...(override?.[name] || [])];
@@ -253,6 +260,11 @@ function decompressBody(rawBody, contentEncoding) {
253
260
  * Read and decode an undici response body according to the requested
254
261
  * `responseType`.
255
262
  *
263
+ * An error response often carries a body in another format than the one asked
264
+ * for, such as the HTML page a CDN sends with a 429. Its JSON body is decoded
265
+ * when it parses and kept as text otherwise, so the caller sees the status
266
+ * rather than a parse error.
267
+ *
256
268
  * @param {Object} undiciResponse Response returned by `undici.request`.
257
269
  * @param {string} [responseType] One of `"json"`, `"buffer"` or `"text"`.
258
270
  * @returns {Promise<{body: any, rawBody: Buffer}>} Decoded body and raw bytes.
@@ -272,7 +284,17 @@ async function readBody(undiciResponse, responseType) {
272
284
  body = rawBody;
273
285
  } else if (responseType === "json") {
274
286
  const text = rawBody.toString("utf-8");
275
- body = text.length ? JSON.parse(text) : undefined;
287
+ if (!text.length) {
288
+ body = undefined;
289
+ } else if (undiciResponse.statusCode >= 400) {
290
+ try {
291
+ body = JSON.parse(text);
292
+ } catch (_err) {
293
+ body = text;
294
+ }
295
+ } else {
296
+ body = JSON.parse(text);
297
+ }
276
298
  } else {
277
299
  body = rawBody.toString("utf-8");
278
300
  }
@@ -304,20 +326,100 @@ function resolveRequestBody(options) {
304
326
 
305
327
  // In-memory cache of successful GET responses, mirroring the HTTP cache cdxgen
306
328
  // historically kept via got + Keyv. Repeated metadata/license lookups during a
307
- // scan are served from here instead of hitting the network again. Set the
308
- // CDXGEN_NO_CACHE environment variable to "true" or "1" to disable it.
329
+ // scan are served from here instead of hitting the network again. It lasts for
330
+ // one run; `CDXGEN_NO_CACHE` and `--no-cache` turn off only the on-disk caches,
331
+ // because nothing gains from asking a registry the same question twice in one
332
+ // scan.
333
+ //
334
+ // Entries expire after an hour and the cache is bounded in size, least
335
+ // recently used first, so a long-running server neither serves stale registry
336
+ // documents nor grows without limit.
309
337
  const responseCache = new Map();
338
+ const RESPONSE_CACHE_TTL_MS = 60 * 60 * 1000;
339
+ const RESPONSE_CACHE_MAX_BYTES = 512 * 1024 * 1024;
340
+ let responseCacheLimits = {
341
+ ttlMs: RESPONSE_CACHE_TTL_MS,
342
+ maxBytes: RESPONSE_CACHE_MAX_BYTES,
343
+ };
344
+ let responseCacheBytes = 0;
345
+
346
+ /**
347
+ * Approximate the memory a cached response holds. The raw bytes are kept,
348
+ * plus the decoded body for text and JSON responses.
349
+ *
350
+ * @param {Object} response Response object.
351
+ * @returns {number} Size in bytes.
352
+ */
353
+ function cachedResponseBytes(response) {
354
+ const raw = response?.rawBody?.length || 0;
355
+ return response?.body === response?.rawBody ? raw : raw * 3;
356
+ }
310
357
 
311
358
  /**
312
- * Determine whether the in-memory HTTP response cache is disabled via the
313
- * CDXGEN_NO_CACHE environment variable. Evaluated per request so tests and
314
- * callers can toggle it at runtime.
359
+ * Read a cached response, dropping it when it has expired and marking it as
360
+ * recently used otherwise.
315
361
  *
316
- * @returns {boolean} True when caching should be skipped.
362
+ * @param {string} key Cache key.
363
+ * @returns {Object|undefined} The cached response.
317
364
  */
318
- export function isCacheDisabled() {
319
- const value = process.env.CDXGEN_NO_CACHE;
320
- return value === "true" || value === "1";
365
+ function cacheGet(key) {
366
+ const entry = responseCache.get(key);
367
+ if (!entry) {
368
+ return undefined;
369
+ }
370
+ responseCache.delete(key);
371
+ if (entry.expiresAt <= Date.now()) {
372
+ responseCacheBytes -= entry.bytes;
373
+ return undefined;
374
+ }
375
+ responseCache.set(key, entry);
376
+ return entry.response;
377
+ }
378
+
379
+ /**
380
+ * Store a response, evicting the least recently used entries beyond the size
381
+ * bound.
382
+ *
383
+ * @param {string} key Cache key.
384
+ * @param {Object} response Response object.
385
+ * @returns {void}
386
+ */
387
+ function cacheSet(key, response) {
388
+ const bytes = cachedResponseBytes(response);
389
+ if (bytes > responseCacheLimits.maxBytes) {
390
+ return;
391
+ }
392
+ const previous = responseCache.get(key);
393
+ if (previous) {
394
+ responseCache.delete(key);
395
+ responseCacheBytes -= previous.bytes;
396
+ }
397
+ responseCache.set(key, {
398
+ response,
399
+ bytes,
400
+ expiresAt: Date.now() + responseCacheLimits.ttlMs,
401
+ });
402
+ responseCacheBytes += bytes;
403
+ for (const [oldestKey, oldest] of responseCache) {
404
+ if (responseCacheBytes <= responseCacheLimits.maxBytes) {
405
+ break;
406
+ }
407
+ responseCache.delete(oldestKey);
408
+ responseCacheBytes -= oldest.bytes;
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Override the response cache bounds. Returns the previous bounds so tests can
414
+ * restore them. Not part of the public API.
415
+ *
416
+ * @param {{ttlMs?: number, maxBytes?: number}} limits New bounds.
417
+ * @returns {{ttlMs: number, maxBytes: number}} Previous bounds.
418
+ */
419
+ export function _setHttpCacheLimits(limits) {
420
+ const previous = responseCacheLimits;
421
+ responseCacheLimits = { ...responseCacheLimits, ...limits };
422
+ return previous;
321
423
  }
322
424
 
323
425
  /**
@@ -327,8 +429,11 @@ export function isCacheDisabled() {
327
429
  */
328
430
  export function clearHttpCache() {
329
431
  responseCache.clear();
432
+ responseCacheBytes = 0;
330
433
  }
331
434
 
435
+ registerRunReset("http:responses", clearHttpCache);
436
+
332
437
  // ---------------------------------------------------------------------------
333
438
  // Test cassette interceptor — the single network seam.
334
439
  //
@@ -397,15 +502,33 @@ export function clearHttpInterceptor() {
397
502
  }
398
503
 
399
504
  /**
400
- * Build the cache key for a request. Only the method and normalized URL are
401
- * used, matching got's default cache key behaviour.
505
+ * Build the cache key for a request.
506
+ *
507
+ * Besides the method and URL, the key carries the response type, which
508
+ * changes how the same bytes are decoded, and a digest of every request
509
+ * header. A response fetched with one credential is therefore never served to
510
+ * a request made with another credential or with none, and a JSON request
511
+ * never receives a text body. Header values only enter the key as a SHA-256
512
+ * digest, so no credential is held in it.
402
513
  *
403
514
  * @param {string} method Uppercased HTTP method.
404
515
  * @param {URL} url Request URL.
516
+ * @param {Object} [headers] Request headers as sent.
517
+ * @param {string} [responseType] Requested body decoding.
405
518
  * @returns {string} Cache key.
406
519
  */
407
- function cacheKeyFor(method, url) {
408
- return `${method}:${url.toString()}`;
520
+ function cacheKeyFor(method, url, headers = {}, responseType = undefined) {
521
+ const headerEntries = Object.entries(headers || {})
522
+ .filter(([, value]) => value !== undefined && value !== null)
523
+ .map(([name, value]) => [
524
+ name.toLowerCase(),
525
+ Array.isArray(value) ? value.join(", ") : String(value),
526
+ ])
527
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
528
+ const headerDigest = headerEntries.length
529
+ ? createHash("sha256").update(JSON.stringify(headerEntries)).digest("hex")
530
+ : "";
531
+ return `${method}:${responseType || "text"}:${url.toString()}:${headerDigest}`;
409
532
  }
410
533
 
411
534
  // The maximum number of redirects to follow when `followRedirect` is enabled.
@@ -587,11 +710,23 @@ async function doRequest(mergedOptions) {
587
710
  const { body, headers } = resolveRequestBody(options);
588
711
  const method = (options.method || "GET").toUpperCase();
589
712
 
590
- // Serve cacheable GET requests from the in-memory cache when enabled.
591
- const cacheable = method === "GET" && !isCacheDisabled();
592
- const cacheKey = cacheable ? cacheKeyFor(method, url) : undefined;
593
- if (cacheable && responseCache.has(cacheKey)) {
594
- return responseCache.get(cacheKey);
713
+ // Serve cacheable GET requests from the in-memory cache when enabled. A
714
+ // request opts out with `cache: false`, as token exchanges do.
715
+ const cacheable = method === "GET" && options.cache !== false;
716
+ const cacheKey = cacheable
717
+ ? cacheKeyFor(method, url, headers, options.responseType)
718
+ : undefined;
719
+ if (cacheable) {
720
+ const cached = cacheGet(cacheKey);
721
+ if (cached) {
722
+ return cached;
723
+ }
724
+ }
725
+
726
+ // Runs only when the request is about to reach the network, so a caller
727
+ // that rate-limits a host does not wait for a response already cached.
728
+ for (const hook of options.hooks?.beforeNetwork || []) {
729
+ await hook(options);
595
730
  }
596
731
 
597
732
  const timeoutMs = resolveTimeout(options.timeout);
@@ -652,7 +787,7 @@ async function doRequest(mergedOptions) {
652
787
 
653
788
  // Only successful responses are cached, matching HTTP cache semantics.
654
789
  if (cacheable && finalResponse.statusCode < 400) {
655
- responseCache.set(cacheKey, finalResponse);
790
+ cacheSet(cacheKey, finalResponse);
656
791
  }
657
792
  return finalResponse;
658
793
  }
@@ -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
+ }