@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
@@ -229,6 +229,35 @@ function getDockerCreds(registry) {
229
229
  return readEnvironmentVariable("DOCKER_AUTH");
230
230
  }
231
231
 
232
+ /**
233
+ * Check whether a token realm URL belongs to the registry itself.
234
+ *
235
+ * A registry's 401 response names the realm, so a hostile registry picks the
236
+ * URL cdxgen will call. Credentials picked up for the registry must therefore
237
+ * only be attached when the realm resolves to the same host and port; public
238
+ * delegating realms (docker.io to auth.docker.io) are still usable, just
239
+ * anonymously.
240
+ *
241
+ * @param {string} registry Registry host, optionally with a port
242
+ * @param {URL} realmUrl Parsed realm URL from the WWW-Authenticate header
243
+ * @returns {boolean} true when host and port match the registry
244
+ */
245
+ export function isSameRegistryHost(registry, realmUrl) {
246
+ try {
247
+ const registryUrl = new URL(
248
+ registry.startsWith("localhost")
249
+ ? `http://${registry}`
250
+ : `https://${registry}`,
251
+ );
252
+ return (
253
+ registryUrl.hostname === realmUrl.hostname &&
254
+ registryUrl.port === realmUrl.port
255
+ );
256
+ } catch (_e) {
257
+ return false;
258
+ }
259
+ }
260
+
232
261
  async function getOciToken(registry, repository, scope) {
233
262
  const creds = getDockerCreds(registry);
234
263
  const scheme = registry.startsWith("localhost") ? "http" : "https";
@@ -254,12 +283,24 @@ async function getOciToken(registry, repository, scope) {
254
283
  if (params.service)
255
284
  reqUrl.searchParams.set("service", params.service);
256
285
  reqUrl.searchParams.set("scope", `repository:${repository}:${scope}`);
286
+ // Tokens expire within minutes, so a cached one would be replayed
287
+ // after it stopped working.
257
288
  const options = {
289
+ cache: false,
258
290
  responseType: "json",
259
291
  timeout: getOciTimeoutMs(),
260
292
  };
261
- if (creds) {
293
+ // The realm comes from the registry's own 401 response, so a
294
+ // hostile registry chooses this URL. Never forward the stored
295
+ // registry credentials to a different host; an anonymous cross-host
296
+ // token request (how public registries such as docker.io delegate
297
+ // to auth.docker.io) is still allowed.
298
+ if (creds && isSameRegistryHost(registry, reqUrl)) {
262
299
  options.headers = { Authorization: `Basic ${creds}` };
300
+ } else if (creds) {
301
+ console.warn(
302
+ `Skipping credential forwarding to the token realm ${reqUrl.host} for registry ${registry}: the host differs.`,
303
+ );
263
304
  }
264
305
  const tokenRes = await cdxgenAgent.get(reqUrl.toString(), options);
265
306
  if (
@@ -372,11 +413,18 @@ async function pullBlob(registry, repository, digest, token) {
372
413
  *
373
414
  * @param {string} image OCI image reference (e.g. `"registry.example.com/org/app:tag"`)
374
415
  * @param {string} [platform] OCI platform string (e.g. `"linux/amd64"`); no-op for JS implementation
416
+ * @param {Object} [options]
417
+ * @param {function(Buffer): void} [options.checkBytes] Called with the raw bytes of the BOM layer that is about to be returned. An error it throws is rethrown, and no other referrer is tried.
375
418
  * @returns {Promise<Object|undefined>} Parsed CycloneDX BOM JSON object, or `undefined` if not found
376
419
  */
377
- export async function getBomWithOras(image, _platform = undefined) {
420
+ export async function getBomWithOras(
421
+ image,
422
+ _platform = undefined,
423
+ options = {},
424
+ ) {
378
425
  const { registry, repository, reference } = parseImageRef(image);
379
426
  const token = await getOciToken(registry, repository, "pull");
427
+ let rejection;
380
428
 
381
429
  try {
382
430
  const targetManifest = await fetchManifest(
@@ -450,9 +498,10 @@ export async function getBomWithOras(image, _platform = undefined) {
450
498
  bomLayer.digest,
451
499
  token,
452
500
  );
501
+ const text = blob.toString("utf8");
453
502
  let bomJson;
454
503
  try {
455
- bomJson = JSON.parse(blob.toString("utf8"));
504
+ bomJson = JSON.parse(text);
456
505
  } catch {
457
506
  // Defensive: the mediaType claimed JSON but the layer did not parse
458
507
  // (truncated/corrupt blob). Skip it and try the next candidate.
@@ -471,6 +520,14 @@ export async function getBomWithOras(image, _platform = undefined) {
471
520
  }
472
521
 
473
522
  if (isCycloneDxBom(bomJson)) {
523
+ if (options.checkBytes) {
524
+ try {
525
+ options.checkBytes(blob);
526
+ } catch (err) {
527
+ rejection = err;
528
+ break;
529
+ }
530
+ }
474
531
  return bomJson;
475
532
  }
476
533
  }
@@ -479,6 +536,9 @@ export async function getBomWithOras(image, _platform = undefined) {
479
536
  `Unable to pull the SBOM attachment for ${image} natively! ${e.message}`,
480
537
  );
481
538
  }
539
+ if (rejection) {
540
+ throw rejection;
541
+ }
482
542
  return undefined;
483
543
  }
484
544
 
@@ -197,3 +197,72 @@ export function resolveCmakeCacheFacts(map) {
197
197
  fetchContentBase,
198
198
  };
199
199
  }
200
+
201
+ /** CMake's compiler ids, mapped to the family names the C/C++ toolchain uses. */
202
+ const CMAKE_COMPILER_FAMILIES = {
203
+ GNU: "gcc",
204
+ Clang: "clang",
205
+ AppleClang: "apple-clang",
206
+ MSVC: "msvc",
207
+ IntelLLVM: "icx",
208
+ Intel: "icc",
209
+ NVIDIA: "nvcc",
210
+ NVHPC: "nvhpc",
211
+ PGI: "nvhpc",
212
+ };
213
+
214
+ /**
215
+ * Parse a compiler description CMake writes when it configures a build
216
+ * (`CMakeFiles/<cmake version>/CMake<LANG>Compiler.cmake`): the compiler's
217
+ * path, CMake's id for it and its version, all identified by CMake itself.
218
+ *
219
+ * @param {string} text File contents
220
+ * @returns {{language: string, compiler: string, family: string, id: string, version: string}|null}
221
+ * `null` when the text names no compiler
222
+ */
223
+ export function parseCmakeCompilerFile(text) {
224
+ if (!text || typeof text !== "string") {
225
+ return null;
226
+ }
227
+ const values = new Map();
228
+ for (const rawLine of text.split(/\r\n|\r|\n/)) {
229
+ const line = rawLine.trim();
230
+ if (!line.startsWith("set(CMAKE_") || !line.endsWith(")")) {
231
+ continue;
232
+ }
233
+ const body = line.slice(4, -1).trim();
234
+ const space = body.search(/\s/);
235
+ if (space < 0) {
236
+ continue;
237
+ }
238
+ const key = body.slice(0, space);
239
+ let value = body.slice(space).trim();
240
+ if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
241
+ value = value.slice(1, -1);
242
+ }
243
+ if (!values.has(key)) {
244
+ values.set(key, value);
245
+ }
246
+ }
247
+ for (const [key, compiler] of values) {
248
+ const match = key.match(/^CMAKE_([A-Z]+)_COMPILER$/);
249
+ if (!match || !compiler) {
250
+ continue;
251
+ }
252
+ const language = match[1];
253
+ const id = values.get(`CMAKE_${language}_COMPILER_ID`) || "";
254
+ const variant = values.get(`CMAKE_${language}_COMPILER_FRONTEND_VARIANT`);
255
+ let family = CMAKE_COMPILER_FAMILIES[id] || id.toLowerCase() || "unknown";
256
+ if (id === "Clang" && variant === "MSVC") {
257
+ family = "clang-cl";
258
+ }
259
+ return {
260
+ language,
261
+ compiler,
262
+ family,
263
+ id,
264
+ version: values.get(`CMAKE_${language}_COMPILER_VERSION`) || "",
265
+ };
266
+ }
267
+ return null;
268
+ }
@@ -0,0 +1,466 @@
1
+ /**
2
+ * Pure parser for CMake language files (`CMakeLists.txt`, `*.cmake`).
3
+ *
4
+ * `parseCmakeCommands` splits a file into command invocations the way CMake
5
+ * reads it: command names are case-insensitive, an invocation may span several
6
+ * lines, and arguments may be quoted, bracketed (`[[...]]`) or interleaved with
7
+ * comments. `cmakeFetchDependencies` reads the commands that download a
8
+ * dependency at configure time: `FetchContent_Declare`, `ExternalProject_Add`
9
+ * and the CPM.cmake `CPMAddPackage` family.
10
+ *
11
+ * This module is layer 1 (text in, data out): no filesystem or subprocess
12
+ * access. `lib/ecosystems/parsers-misc.js` turns the facts into components.
13
+ */
14
+
15
+ /**
16
+ * Length of the bracket opener (`[[`, `[=[`, `[==[`, ...) at `text[start]`, or 0.
17
+ *
18
+ * @param {string} text CMake source
19
+ * @param {number} start Index of a `[`
20
+ * @returns {number} Opener length including both brackets, or 0
21
+ */
22
+ function bracketOpenLength(text, start) {
23
+ if (text[start] !== "[") {
24
+ return 0;
25
+ }
26
+ let i = start + 1;
27
+ while (text[i] === "=") {
28
+ i++;
29
+ }
30
+ return text[i] === "[" ? i - start + 1 : 0;
31
+ }
32
+
33
+ /**
34
+ * Index just past the bracket closer matching an opener of `openLength`.
35
+ *
36
+ * @param {string} text CMake source
37
+ * @param {number} contentStart First index after the opener
38
+ * @param {number} openLength Opener length (`[==[` is 4)
39
+ * @returns {number} Index after the closer, or the text length when unclosed
40
+ */
41
+ function bracketCloseEnd(text, contentStart, openLength) {
42
+ const closer = `]${"=".repeat(openLength - 2)}]`;
43
+ const end = text.indexOf(closer, contentStart);
44
+ return end === -1 ? text.length : end + closer.length;
45
+ }
46
+
47
+ /**
48
+ * Index of the end of the comment starting at `text[start]` (a `#`).
49
+ *
50
+ * @param {string} text CMake source
51
+ * @param {number} start Index of the `#`
52
+ * @returns {number} Index of the first character after the comment
53
+ */
54
+ function skipComment(text, start) {
55
+ const openLength = bracketOpenLength(text, start + 1);
56
+ if (openLength) {
57
+ return bracketCloseEnd(text, start + 1 + openLength, openLength);
58
+ }
59
+ const eol = text.indexOf("\n", start);
60
+ return eol === -1 ? text.length : eol;
61
+ }
62
+
63
+ function isIdentifierStart(c) {
64
+ return (c >= "A" && c <= "Z") || (c >= "a" && c <= "z") || c === "_";
65
+ }
66
+
67
+ function isIdentifierPart(c) {
68
+ return isIdentifierStart(c) || (c >= "0" && c <= "9");
69
+ }
70
+
71
+ function isSpace(c) {
72
+ return c === " " || c === "\t" || c === "\r" || c === "\n";
73
+ }
74
+
75
+ /**
76
+ * Split CMake source into command invocations.
77
+ *
78
+ * @param {string} text Contents of a CMake file
79
+ * @returns {{name: string, args: string[], line: number}[]} Invocations in
80
+ * source order. `name` is lower-cased; `args` have their quotes and bracket
81
+ * delimiters removed; `line` is the 1-based line of the command name.
82
+ */
83
+ export function parseCmakeCommands(text) {
84
+ const commands = [];
85
+ if (!text || typeof text !== "string") {
86
+ return commands;
87
+ }
88
+ let line = 1;
89
+ let i = 0;
90
+ const advanceTo = (end) => {
91
+ for (let k = i; k < end; k++) {
92
+ if (text[k] === "\n") {
93
+ line++;
94
+ }
95
+ }
96
+ i = end;
97
+ };
98
+ while (i < text.length) {
99
+ const c = text[i];
100
+ if (c === "#") {
101
+ advanceTo(skipComment(text, i));
102
+ continue;
103
+ }
104
+ if (!isIdentifierStart(c) || (i > 0 && isIdentifierPart(text[i - 1]))) {
105
+ advanceTo(i + 1);
106
+ continue;
107
+ }
108
+ let end = i;
109
+ while (end < text.length && isIdentifierPart(text[end])) {
110
+ end++;
111
+ }
112
+ const name = text.slice(i, end).toLowerCase();
113
+ const commandLine = line;
114
+ let open = end;
115
+ while (text[open] === " " || text[open] === "\t") {
116
+ open++;
117
+ }
118
+ if (text[open] !== "(") {
119
+ advanceTo(end);
120
+ continue;
121
+ }
122
+ advanceTo(open + 1);
123
+ const args = [];
124
+ let depth = 1;
125
+ let current = "";
126
+ let hasToken = false;
127
+ const flush = () => {
128
+ if (hasToken) {
129
+ args.push(current);
130
+ }
131
+ current = "";
132
+ hasToken = false;
133
+ };
134
+ while (i < text.length && depth > 0) {
135
+ const ch = text[i];
136
+ if (ch === "#") {
137
+ flush();
138
+ advanceTo(skipComment(text, i));
139
+ continue;
140
+ }
141
+ if (ch === '"') {
142
+ let k = i + 1;
143
+ let value = "";
144
+ while (k < text.length && text[k] !== '"') {
145
+ if (text[k] === "\\" && k + 1 < text.length) {
146
+ value += text[k + 1];
147
+ k += 2;
148
+ continue;
149
+ }
150
+ value += text[k];
151
+ k++;
152
+ }
153
+ current += value;
154
+ hasToken = true;
155
+ advanceTo(Math.min(k + 1, text.length));
156
+ continue;
157
+ }
158
+ const openLength = bracketOpenLength(text, i);
159
+ if (openLength && !hasToken) {
160
+ const contentStart = i + openLength;
161
+ const closeEnd = bracketCloseEnd(text, contentStart, openLength);
162
+ const contentEnd = Math.max(contentStart, closeEnd - openLength);
163
+ current += text.slice(contentStart, contentEnd);
164
+ hasToken = true;
165
+ advanceTo(closeEnd);
166
+ continue;
167
+ }
168
+ if (ch === "(") {
169
+ depth++;
170
+ } else if (ch === ")") {
171
+ depth--;
172
+ if (depth === 0) {
173
+ flush();
174
+ advanceTo(i + 1);
175
+ break;
176
+ }
177
+ }
178
+ if (isSpace(ch)) {
179
+ flush();
180
+ } else {
181
+ current += ch;
182
+ hasToken = true;
183
+ }
184
+ advanceTo(i + 1);
185
+ }
186
+ commands.push({ name, args, line: commandLine });
187
+ }
188
+ return commands;
189
+ }
190
+
191
+ // Upper bound on an expanded value; a set() chain that doubles a variable can
192
+ // otherwise grow it beyond the maximum string length.
193
+ const MAX_EXPANDED_LENGTH = 65536;
194
+
195
+ /**
196
+ * Replace `${VAR}` references with the values of simple `set()` calls. A
197
+ * reference to an unknown variable is left in place, so callers can tell a
198
+ * resolved value from an unresolved one. Expansion stops at the previous
199
+ * round once the text would exceed 64 KiB.
200
+ *
201
+ * @param {string} value Argument text
202
+ * @param {Map<string,string>} variables Known variable values
203
+ * @returns {string} The expanded text
204
+ */
205
+ export function expandCmakeVariables(value, variables) {
206
+ let result = value;
207
+ for (let round = 0; round < 5 && result.includes("${"); round++) {
208
+ let next = "";
209
+ let i = 0;
210
+ while (i < result.length) {
211
+ if (next.length > MAX_EXPANDED_LENGTH) {
212
+ return result;
213
+ }
214
+ const start = result.indexOf("${", i);
215
+ if (start === -1) {
216
+ next += result.slice(i);
217
+ break;
218
+ }
219
+ const end = result.indexOf("}", start + 2);
220
+ if (end === -1) {
221
+ next += result.slice(i);
222
+ break;
223
+ }
224
+ const key = result.slice(start + 2, end);
225
+ next += result.slice(i, start);
226
+ next += variables.has(key)
227
+ ? variables.get(key)
228
+ : result.slice(start, end + 1);
229
+ i = end + 1;
230
+ }
231
+ if (next === result) {
232
+ break;
233
+ }
234
+ result = next;
235
+ }
236
+ return result;
237
+ }
238
+
239
+ /**
240
+ * The variables a file sets to a single value: `set(FOO_VERSION 1.2.3)` and
241
+ * `set(FOO_VERSION "1.2.3" CACHE STRING "...")`. Later definitions win.
242
+ *
243
+ * @param {{name: string, args: string[]}[]} commands Parsed commands
244
+ * @returns {Map<string,string>} Variable values
245
+ */
246
+ export function cmakeSetVariables(commands) {
247
+ const variables = new Map();
248
+ for (const command of commands) {
249
+ if (command.name !== "set" || command.args.length < 2) {
250
+ continue;
251
+ }
252
+ const [key, value, ...rest] = command.args;
253
+ if (
254
+ rest.length === 0 ||
255
+ rest[0] === "CACHE" ||
256
+ rest[0] === "PARENT_SCOPE"
257
+ ) {
258
+ // An appending set(FOO "${FOO} ...") of an undefined FOO starts from an
259
+ // empty value in CMake; dropping the self-reference also keeps it from
260
+ // being substituted into itself on every later expansion round.
261
+ variables.set(
262
+ key,
263
+ expandCmakeVariables(value, variables).replaceAll(`\${${key}}`, ""),
264
+ );
265
+ }
266
+ }
267
+ return variables;
268
+ }
269
+
270
+ /**
271
+ * The value following `keyword` in a keyword-style argument list, skipping the
272
+ * leading positional arguments.
273
+ *
274
+ * @param {string[]} args Command arguments
275
+ * @param {string} keyword Keyword to look up, e.g. `GIT_TAG`
276
+ * @param {number} from First index that may hold a keyword
277
+ * @returns {string|undefined} The value, or undefined when absent
278
+ */
279
+ function keywordValue(args, keyword, from) {
280
+ for (let i = from; i < args.length - 1; i++) {
281
+ if (args[i] === keyword) {
282
+ return args[i + 1];
283
+ }
284
+ }
285
+ return undefined;
286
+ }
287
+
288
+ const HOST_SHORTHANDS = {
289
+ gh: "https://github.com/",
290
+ gl: "https://gitlab.com/",
291
+ bb: "https://bitbucket.org/",
292
+ };
293
+
294
+ /**
295
+ * Read a CPM.cmake shorthand such as `gh:fmtlib/fmt#7.1.3`,
296
+ * `gh:nlohmann/json@3.11.2` or `https://example.com/x.git@1.0#v1.0`.
297
+ *
298
+ * @param {string} spec The single CPMAddPackage argument
299
+ * @returns {{name: string, gitRepository?: string, url?: string, gitTag?: string, version?: string}|undefined}
300
+ */
301
+ function parseCpmShorthand(spec) {
302
+ let rest = spec;
303
+ let gitTag;
304
+ let version;
305
+ const hash = rest.lastIndexOf("#");
306
+ if (hash !== -1) {
307
+ gitTag = rest.slice(hash + 1);
308
+ rest = rest.slice(0, hash);
309
+ }
310
+ const colon = rest.indexOf(":");
311
+ const scheme = colon === -1 ? "" : rest.slice(0, colon);
312
+ let repository;
313
+ if (HOST_SHORTHANDS[scheme]) {
314
+ let path = rest.slice(colon + 1);
315
+ const at = path.lastIndexOf("@");
316
+ if (at !== -1) {
317
+ version = path.slice(at + 1);
318
+ path = path.slice(0, at);
319
+ }
320
+ repository = `${HOST_SHORTHANDS[scheme]}${path}`;
321
+ } else if (scheme === "https" || scheme === "http") {
322
+ const lastSlash = rest.lastIndexOf("/");
323
+ const at = rest.lastIndexOf("@");
324
+ if (at > lastSlash) {
325
+ version = rest.slice(at + 1);
326
+ rest = rest.slice(0, at);
327
+ }
328
+ repository = rest;
329
+ } else {
330
+ return undefined;
331
+ }
332
+ const name = repository
333
+ .split("/")
334
+ .filter(Boolean)
335
+ .pop()
336
+ ?.replace(/\.git$/, "");
337
+ if (!name) {
338
+ return undefined;
339
+ }
340
+ return {
341
+ name,
342
+ gitRepository: repository,
343
+ gitTag: gitTag || (version ? `v${version}` : undefined),
344
+ version,
345
+ };
346
+ }
347
+
348
+ /**
349
+ * The dependencies a CMake file downloads at configure time.
350
+ *
351
+ * `FetchContent_Declare(<name> ...)` and `ExternalProject_Add(<name> ...)` are
352
+ * read for `GIT_REPOSITORY`/`GIT_TAG` and `URL`/`URL_HASH`/`URL_MD5`.
353
+ * `CPMAddPackage`, `CPMFindPackage` and `CPMDeclarePackage` are read in both
354
+ * the shorthand form and the keyword form (`NAME`, `VERSION`,
355
+ * `GITHUB_REPOSITORY`, `GITLAB_REPOSITORY`, `BITBUCKET_REPOSITORY`,
356
+ * `GIT_REPOSITORY`, `GIT_TAG`, `URL`, `URL_HASH`). `${VAR}` references are
357
+ * expanded with the file's simple `set()` values; anything still unresolved
358
+ * stays as written.
359
+ *
360
+ * @param {string} text Contents of a CMake file
361
+ * @returns {{kind: string, name: string, line: number, gitRepository?: string, gitTag?: string, url?: string, urlHash?: string, version?: string, madeAvailable: boolean}[]}
362
+ * `kind` is `fetch`, `external-project` or `cpm`; `madeAvailable` is true
363
+ * when a `FetchContent_MakeAvailable` in the same file names the dependency.
364
+ */
365
+ export function cmakeFetchDependencies(text) {
366
+ const commands = parseCmakeCommands(text);
367
+ const variables = cmakeSetVariables(commands);
368
+ const expand = (value) =>
369
+ value === undefined ? undefined : expandCmakeVariables(value, variables);
370
+ const madeAvailable = new Set();
371
+ for (const command of commands) {
372
+ if (command.name === "fetchcontent_makeavailable") {
373
+ for (const arg of command.args) {
374
+ madeAvailable.add(expand(arg).toLowerCase());
375
+ }
376
+ }
377
+ }
378
+ const deps = [];
379
+ for (const command of commands) {
380
+ const args = command.args.map(expand);
381
+ let dep;
382
+ if (
383
+ (command.name === "fetchcontent_declare" ||
384
+ command.name === "externalproject_add") &&
385
+ args.length
386
+ ) {
387
+ const url = keywordValue(args, "URL", 1);
388
+ const md5 = keywordValue(args, "URL_MD5", 1);
389
+ dep = {
390
+ kind:
391
+ command.name === "fetchcontent_declare"
392
+ ? "fetch"
393
+ : "external-project",
394
+ name: args[0],
395
+ gitRepository: keywordValue(args, "GIT_REPOSITORY", 1),
396
+ gitTag: keywordValue(args, "GIT_TAG", 1),
397
+ url: url?.split(";")[0],
398
+ urlHash: keywordValue(args, "URL_HASH", 1) || (md5 && `MD5=${md5}`),
399
+ };
400
+ } else if (
401
+ command.name === "cpmaddpackage" ||
402
+ command.name === "cpmfindpackage" ||
403
+ command.name === "cpmdeclarepackage"
404
+ ) {
405
+ const positional = command.name === "cpmdeclarepackage" ? 1 : 0;
406
+ if (args.length === positional + 1) {
407
+ const shorthand = parseCpmShorthand(args[positional]);
408
+ if (shorthand) {
409
+ dep = {
410
+ kind: "cpm",
411
+ ...shorthand,
412
+ name: positional ? args[0] : shorthand.name,
413
+ };
414
+ }
415
+ } else if (args.length > positional) {
416
+ const name = positional ? args[0] : keywordValue(args, "NAME", 0);
417
+ const version = keywordValue(args, "VERSION", positional);
418
+ const github = keywordValue(args, "GITHUB_REPOSITORY", positional);
419
+ const gitlab = keywordValue(args, "GITLAB_REPOSITORY", positional);
420
+ const bitbucket = keywordValue(
421
+ args,
422
+ "BITBUCKET_REPOSITORY",
423
+ positional,
424
+ );
425
+ const hosted =
426
+ (github && `${HOST_SHORTHANDS.gh}${github}`) ||
427
+ (gitlab && `${HOST_SHORTHANDS.gl}${gitlab}`) ||
428
+ (bitbucket && `${HOST_SHORTHANDS.bb}${bitbucket}`);
429
+ const gitRepository =
430
+ hosted || keywordValue(args, "GIT_REPOSITORY", positional);
431
+ const url = keywordValue(args, "URL", positional);
432
+ const derivedName =
433
+ name ||
434
+ (gitRepository || url)
435
+ ?.split("/")
436
+ .filter(Boolean)
437
+ .pop()
438
+ ?.replace(/\.git$/, "");
439
+ if (derivedName) {
440
+ dep = {
441
+ kind: "cpm",
442
+ name: derivedName,
443
+ gitRepository,
444
+ gitTag:
445
+ keywordValue(args, "GIT_TAG", positional) ||
446
+ (gitRepository && version ? `v${version}` : undefined),
447
+ url,
448
+ urlHash: keywordValue(args, "URL_HASH", positional),
449
+ version,
450
+ };
451
+ }
452
+ }
453
+ }
454
+ if (dep?.name) {
455
+ for (const key of Object.keys(dep)) {
456
+ if (dep[key] === undefined || dep[key] === "") {
457
+ delete dep[key];
458
+ }
459
+ }
460
+ dep.line = command.line;
461
+ dep.madeAvailable = madeAvailable.has(dep.name.toLowerCase());
462
+ deps.push(dep);
463
+ }
464
+ }
465
+ return deps;
466
+ }