@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
@@ -4,6 +4,7 @@ import {
4
4
  sep as _sep,
5
5
  basename,
6
6
  dirname,
7
+ isAbsolute,
7
8
  join,
8
9
  relative,
9
10
  resolve,
@@ -12,11 +13,7 @@ import {
12
13
  import { build, Purl } from "@cdxgen/cdx-purl";
13
14
  import { parse as _load } from "yaml";
14
15
 
15
- import {
16
- cdxgenAgent,
17
- DEBUG_MODE,
18
- readEnvironmentVariable,
19
- } from "../core/activity.js";
16
+ import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
20
17
  import { recordDegradation } from "../core/buildLedger.js";
21
18
  import { deferFailOnError } from "../core/deferredExit.js";
22
19
  import { shouldFetchLicense } from "../core/env.js";
@@ -28,10 +25,12 @@ import {
28
25
  } from "../core/fs.js";
29
26
  import { thoughtLog } from "../core/logger.js";
30
27
  import { isWin } from "../core/paths.js";
28
+ import { registerRunReset } from "../core/runState.js";
31
29
  import { mesonWrapDB } from "../core/state.js";
32
30
  import { isGitHubUrl } from "../core/urls.js";
33
31
  import { parseWorkflowFile } from "../inventory/ciParsers/githubActions.js";
34
32
  import {
33
+ gatedGet,
35
34
  prefetchEnabled,
36
35
  prefetchedResponse,
37
36
  prefetchJson,
@@ -44,9 +43,37 @@ import {
44
43
  tryBuildPurl,
45
44
  } from "../inventory/purl.js";
46
45
  import { getLicenses, spdxLicenses } from "../inventory/spdx.js";
46
+ import { cmakeFetchDependencies } from "../parsers/cmakeLists.js";
47
+ import { submodulePurlCoordinates } from "../parsers/gitmodules.js";
47
48
  import { getDartMetadata } from "./ecosystems.js";
48
49
  import { parsePkgJson } from "./parsers-js.js";
49
50
 
51
+ /**
52
+ * Upper bound for a flake.nix `inputs = { ... }` block before the input-name
53
+ * patterns are applied. The patterns backtrack quadratically, so an unbounded
54
+ * attacker-crafted block is a CPU denial of service; genuine inputs blocks are
55
+ * a few kilobytes at most.
56
+ *
57
+ * @type {number}
58
+ */
59
+ const MAX_FLAKE_INPUTS_BLOCK_LENGTH = 16 * 1024;
60
+
61
+ /**
62
+ * Whether a package registry can hold a pubspec.lock entry.
63
+ *
64
+ * The entry's `source` names where the package comes from. `sdk` packages ship
65
+ * with the Dart or Flutter SDK, `path` packages live in the working tree, and
66
+ * `git` packages come from a repository. None is published on a registry, and
67
+ * asking about them leaks their names and imports a look-alike's metadata.
68
+ *
69
+ * @param {Object} packageData One pubspec.lock `packages` entry.
70
+ * @returns {boolean}
71
+ */
72
+ function canPubRegistryServe(packageData) {
73
+ const source = packageData?.source;
74
+ return source === undefined || source === "hosted";
75
+ }
76
+
50
77
  /**
51
78
  * Method to parse pubspec.lock files.
52
79
  *
@@ -59,10 +86,11 @@ export async function parsePubLockData(pubLockData, lockFile) {
59
86
  if (!pubLockData) {
60
87
  return [];
61
88
  }
62
- let pkgList = [];
89
+ const pkgList = [];
63
90
  const rootList = [];
64
91
  const data = _load(pubLockData);
65
92
  const packages = data.packages;
93
+ const lookupList = [];
66
94
  for (const [packageName, packageData] of Object.entries(packages)) {
67
95
  const pkg = {
68
96
  name: packageName,
@@ -119,9 +147,15 @@ export async function parsePubLockData(pubLockData, lockFile) {
119
147
  };
120
148
  }
121
149
  pkgList.push(pkg);
150
+ if (canPubRegistryServe(packageData)) {
151
+ lookupList.push(pkg);
152
+ }
122
153
  }
123
154
  if (shouldFetchLicense()) {
124
- pkgList = await getDartMetadata(pkgList);
155
+ // getDartMetadata enriches in place, so the sdk, path and git packages
156
+ // stay in pkgList with the lockfile's answers and are simply not handed
157
+ // to it.
158
+ await getDartMetadata(lookupList);
125
159
  }
126
160
  return { rootList, pkgList };
127
161
  }
@@ -1206,19 +1240,41 @@ export function parseFlakeNix(flakeNixFile) {
1206
1240
  let match;
1207
1241
  while ((match = inputsRegex.exec(flakeContent)) !== null) {
1208
1242
  const inputBlock = match[0];
1243
+ // The name-run patterns below backtrack quadratically over unbounded
1244
+ // blocks, so a crafted flake.nix could burn minutes of CPU. Real flake
1245
+ // inputs blocks are a few KB; anything larger is skipped.
1246
+ if (inputBlock.length > MAX_FLAKE_INPUTS_BLOCK_LENGTH) {
1247
+ if (DEBUG_MODE) {
1248
+ console.log(
1249
+ `Skipping a suspiciously large inputs block (${inputBlock.length} chars) in ${flakeNixFile}.`,
1250
+ );
1251
+ }
1252
+ continue;
1253
+ }
1209
1254
 
1210
- // Match different input patterns including nested inputs
1255
+ // Match different input patterns including nested inputs. The cheap
1256
+ // substring pre-checks keep URL-less blocks linear.
1211
1257
  const inputPatterns = [
1212
- /([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)\.url\s*=\s*"([^"]+)"/g,
1213
- /([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)\s*=\s*\{\s*url\s*=\s*"([^"]+)"[^}]*}/gs,
1258
+ {
1259
+ regex: /([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)\.url\s*=\s*"([^"]+)"/g,
1260
+ probe: ".url",
1261
+ },
1262
+ {
1263
+ regex:
1264
+ /([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)\s*=\s*\{\s*url\s*=\s*"([^"]+)"[^}]*}/gs,
1265
+ probe: "url",
1266
+ },
1214
1267
  ];
1215
1268
 
1216
1269
  const addedPackages = new Set();
1217
1270
 
1218
1271
  for (const pattern of inputPatterns) {
1272
+ if (!inputBlock.includes(pattern.probe)) {
1273
+ continue;
1274
+ }
1219
1275
  let subMatch;
1220
- pattern.lastIndex = 0;
1221
- while ((subMatch = pattern.exec(inputBlock)) !== null) {
1276
+ pattern.regex.lastIndex = 0;
1277
+ while ((subMatch = pattern.regex.exec(inputBlock)) !== null) {
1222
1278
  const name = subMatch[1];
1223
1279
  const url = subMatch[2] || subMatch[3];
1224
1280
 
@@ -1780,7 +1836,7 @@ export function parseSwiftJsonTreeObject(
1780
1836
  group: purl ? purl.namespace : "",
1781
1837
  version: purl ? purl.version : jsonObject.version,
1782
1838
  },
1783
- purl ? decodeURIComponent(purl.toString()) : null,
1839
+ purl ? purl.toString() : null,
1784
1840
  jsonObject.name,
1785
1841
  );
1786
1842
  const properties = [];
@@ -1886,15 +1942,23 @@ export function parseSwiftResolved(resolvedFile) {
1886
1942
  }
1887
1943
  for (const adep of resolvedList) {
1888
1944
  const locationOrUrl = adep.location || adep.repositoryURL;
1889
- const version = adep.state.version || adep.state.revision;
1945
+ const version = adep.state?.version || adep.state?.revision;
1946
+ // A pin with no remote URL (a local checkout) has no valid swift
1947
+ // namespace, so it becomes a component without a purl instead of
1948
+ // aborting the rest of the file.
1890
1949
  const purl = purlFromUrlString("swift", locationOrUrl, version);
1891
- const purlString = decodeURIComponent(purl.toString());
1950
+ const name =
1951
+ purl?.name ||
1952
+ adep.identity ||
1953
+ adep.package ||
1954
+ (locationOrUrl ? basename(locationOrUrl).replace(/\.git$/, "") : "");
1955
+ if (!name) {
1956
+ continue;
1957
+ }
1892
1958
  const rootPkg = {
1893
- name: purl.name,
1894
- group: purl.namespace,
1895
- version: purl.version,
1896
- purl: purlString,
1897
- "bom-ref": purlString,
1959
+ name,
1960
+ group: purl?.namespace || "",
1961
+ version: purl ? purl.version : version,
1898
1962
  properties: [
1899
1963
  {
1900
1964
  name: "internal:SrcFile",
@@ -1903,7 +1967,7 @@ export function parseSwiftResolved(resolvedFile) {
1903
1967
  ],
1904
1968
  evidence: {
1905
1969
  identity: {
1906
- field: "purl",
1970
+ field: purl ? "purl" : "name",
1907
1971
  confidence: 1,
1908
1972
  methods: [
1909
1973
  {
@@ -1915,6 +1979,7 @@ export function parseSwiftResolved(resolvedFile) {
1915
1979
  },
1916
1980
  },
1917
1981
  };
1982
+ applyPurl(rootPkg, purl ? purl.toString() : null);
1918
1983
  if (locationOrUrl) {
1919
1984
  rootPkg.repository = { url: locationOrUrl };
1920
1985
  }
@@ -1927,6 +1992,281 @@ export function parseSwiftResolved(resolvedFile) {
1927
1992
  return pkgList;
1928
1993
  }
1929
1994
 
1995
+ /**
1996
+ * Compute the bom-ref of a `swift package show-dependencies` node the same
1997
+ * way parseSwiftJsonTreeObject does.
1998
+ *
1999
+ * @param {Object} node show-dependencies node
2000
+ * @returns {undefined|string} bom-ref
2001
+ */
2002
+ function swiftTreeNodeRef(node) {
2003
+ if (node?.url?.startsWith("http")) {
2004
+ const purl = purlFromUrlString("swift", node.url, node.version);
2005
+ if (purl) {
2006
+ return decodeURIComponent(purl.toString());
2007
+ }
2008
+ }
2009
+ return node?.name;
2010
+ }
2011
+
2012
+ function swiftTreeNodeKeys(node) {
2013
+ const repoName = (node?.url || "")
2014
+ .split("?")[0]
2015
+ .replace(/\/+$/, "")
2016
+ .split("/")
2017
+ .pop()
2018
+ ?.replace(/\.git$/, "");
2019
+ return [node?.identity, node?.name, repoName]
2020
+ .filter(Boolean)
2021
+ .map((k) => String(k).toLowerCase());
2022
+ }
2023
+
2024
+ // Target types that are part of what the package builds and ships
2025
+ const SWIFT_PRODUCTION_TARGET_TYPES = new Set([
2026
+ "executable",
2027
+ "macro",
2028
+ "plugin",
2029
+ ]);
2030
+
2031
+ /**
2032
+ * Identify the targets of a Swift package that are part of what it ships.
2033
+ *
2034
+ * Production targets are the non-test targets that are part of a product,
2035
+ * executables, macros, plugins, targets nothing else depends on, and every
2036
+ * target they depend on. The remaining non-test targets only serve tests,
2037
+ * such as snapshot or fixture helper libraries.
2038
+ *
2039
+ * @param {Object} dumpJson `swift package dump-package` document
2040
+ * @returns {Set<string>} Lowercase names of the production targets
2041
+ */
2042
+ export function collectSwiftProductionTargets(dumpJson) {
2043
+ const targets = dumpJson?.targets || [];
2044
+ const targetsByName = new Map(
2045
+ targets.map((t) => [String(t?.name || "").toLowerCase(), t]),
2046
+ );
2047
+ const localTargetRefs = (target) => {
2048
+ const refs = [];
2049
+ for (const adep of target?.dependencies || []) {
2050
+ const name = adep?.target?.[0] ?? adep?.byName?.[0];
2051
+ if (name && targetsByName.has(String(name).toLowerCase())) {
2052
+ refs.push(String(name).toLowerCase());
2053
+ }
2054
+ }
2055
+ return refs;
2056
+ };
2057
+ const productTargets = new Set(
2058
+ (dumpJson?.products || []).flatMap((p) =>
2059
+ (p?.targets || []).map((t) => String(t).toLowerCase()),
2060
+ ),
2061
+ );
2062
+ const referencedTargets = new Set(targets.flatMap((t) => localTargetRefs(t)));
2063
+ const productionTargets = new Set();
2064
+ const stack = targets
2065
+ .filter(
2066
+ (t) =>
2067
+ !isSwiftTestTarget(t) &&
2068
+ (productTargets.has(String(t.name).toLowerCase()) ||
2069
+ SWIFT_PRODUCTION_TARGET_TYPES.has(t.type) ||
2070
+ !referencedTargets.has(String(t.name).toLowerCase())),
2071
+ )
2072
+ .map((t) => String(t.name).toLowerCase());
2073
+ while (stack.length) {
2074
+ const name = stack.pop();
2075
+ if (productionTargets.has(name)) {
2076
+ continue;
2077
+ }
2078
+ productionTargets.add(name);
2079
+ stack.push(...localTargetRefs(targetsByName.get(name)));
2080
+ }
2081
+ return productionTargets;
2082
+ }
2083
+
2084
+ /**
2085
+ * Classify the dependencies of a Swift package into the packages its shipped
2086
+ * code needs and the packages only its tests need.
2087
+ *
2088
+ * Production targets are the non-test targets that are part of a product,
2089
+ * executables, macros, plugins, targets nothing else depends on, and every
2090
+ * target they depend on. The remaining non-test targets only serve tests
2091
+ * (test-support libraries). A package is test-only when only test and
2092
+ * test-support targets reference its products or use its plugins. With the dependency graph from
2093
+ * `swift package show-dependencies`, the packages reachable only through
2094
+ * test-only packages are test-only as well; a package reachable from a
2095
+ * production dependency, or from a declared dependency no target references,
2096
+ * stays required.
2097
+ *
2098
+ * Target dependencies reference products either as
2099
+ * `{"product": [name, package]}` or `{"byName": [name]}`; SwiftPM resolves a
2100
+ * `byName` product through the package of the same name. When a production
2101
+ * `byName` reference names neither a local target nor a package, the
2102
+ * classification is abandoned rather than risking a required package being
2103
+ * scoped optional.
2104
+ *
2105
+ * @param {Object|String} dumpJson `swift package dump-package` document, parsed or as text
2106
+ * @param {Object|String} [treeJson] `swift package show-dependencies --format json` document, parsed or as text
2107
+ * @returns {Object} `{ optionalRefs, requiredRefs, optionalNames, requiredNames }`:
2108
+ * bom-refs from the dependency graph, and lowercase package identities,
2109
+ * names, and repository names (the only result without a graph)
2110
+ */
2111
+ export function classifySwiftDependencyScopes(dumpJson, treeJson) {
2112
+ // Command output may be handed over unparsed
2113
+ const parseJson = (value) => {
2114
+ if (typeof value !== "string") {
2115
+ return value;
2116
+ }
2117
+ try {
2118
+ return JSON.parse(value);
2119
+ } catch (_e) {
2120
+ return undefined;
2121
+ }
2122
+ };
2123
+ dumpJson = parseJson(dumpJson);
2124
+ treeJson = parseJson(treeJson);
2125
+ const empty = {
2126
+ optionalRefs: new Set(),
2127
+ requiredRefs: new Set(),
2128
+ optionalNames: new Set(),
2129
+ requiredNames: new Set(),
2130
+ };
2131
+ const targets = dumpJson?.targets || [];
2132
+ if (!targets.length) {
2133
+ return empty;
2134
+ }
2135
+ const targetsByName = new Map(
2136
+ targets.map((t) => [String(t?.name || "").toLowerCase(), t]),
2137
+ );
2138
+ const productionTargets = collectSwiftProductionTargets(dumpJson);
2139
+ const packageKeys = (target) => {
2140
+ const keys = [];
2141
+ const unresolved = [];
2142
+ for (const adep of target?.dependencies || []) {
2143
+ if (adep?.product) {
2144
+ keys.push(String(adep.product[1] ?? adep.product[0]).toLowerCase());
2145
+ } else if (adep?.byName?.[0]) {
2146
+ const name = String(adep.byName[0]).toLowerCase();
2147
+ if (!targetsByName.has(name)) {
2148
+ unresolved.push(name);
2149
+ }
2150
+ }
2151
+ }
2152
+ // Build tool plugins count for the target that uses them, so a plugin
2153
+ // used only by test targets is test-only
2154
+ for (const ausage of target?.pluginUsages || []) {
2155
+ if (ausage?.plugin?.[1]) {
2156
+ keys.push(String(ausage.plugin[1]).toLowerCase());
2157
+ }
2158
+ }
2159
+ return { keys, unresolved };
2160
+ };
2161
+ const directNodes = treeJson?.dependencies || [];
2162
+ const nodesByKey = new Map();
2163
+ for (const node of directNodes) {
2164
+ for (const key of swiftTreeNodeKeys(node)) {
2165
+ if (!nodesByKey.has(key)) {
2166
+ nodesByKey.set(key, node);
2167
+ }
2168
+ }
2169
+ }
2170
+ const prodKeys = new Set();
2171
+ const testKeys = new Set();
2172
+ for (const target of targets) {
2173
+ const isProduction =
2174
+ !isSwiftTestTarget(target) &&
2175
+ productionTargets.has(String(target.name).toLowerCase());
2176
+ const { keys, unresolved } = packageKeys(target);
2177
+ const bucket = isProduction ? prodKeys : testKeys;
2178
+ for (const key of keys) {
2179
+ bucket.add(key);
2180
+ }
2181
+ for (const name of unresolved) {
2182
+ // byName products resolve through the package of the same name
2183
+ if (!treeJson || nodesByKey.has(name)) {
2184
+ bucket.add(name);
2185
+ } else if (isProduction) {
2186
+ return empty;
2187
+ }
2188
+ }
2189
+ }
2190
+ if (!treeJson) {
2191
+ return {
2192
+ ...empty,
2193
+ optionalNames: new Set([...testKeys].filter((k) => !prodKeys.has(k))),
2194
+ requiredNames: prodKeys,
2195
+ };
2196
+ }
2197
+ // Package graph keyed by bom-ref
2198
+ const childrenByRef = new Map();
2199
+ const keysByRef = new Map();
2200
+ const collectGraph = (node) => {
2201
+ const ref = swiftTreeNodeRef(node);
2202
+ if (!ref || childrenByRef.has(ref)) {
2203
+ return ref;
2204
+ }
2205
+ childrenByRef.set(ref, new Set());
2206
+ keysByRef.set(ref, swiftTreeNodeKeys(node));
2207
+ for (const child of node?.dependencies || []) {
2208
+ const childRef = collectGraph(child);
2209
+ if (childRef) {
2210
+ childrenByRef.get(ref).add(childRef);
2211
+ }
2212
+ }
2213
+ return ref;
2214
+ };
2215
+ const prodRoots = [];
2216
+ const testRoots = [];
2217
+ for (const node of directNodes) {
2218
+ const ref = collectGraph(node);
2219
+ const keys = swiftTreeNodeKeys(node);
2220
+ const usedByProduction = keys.some((k) => prodKeys.has(k));
2221
+ const usedByTests = keys.some((k) => testKeys.has(k));
2222
+ if (usedByTests && !usedByProduction) {
2223
+ testRoots.push(ref);
2224
+ } else {
2225
+ // Production dependencies, and declared dependencies no target uses
2226
+ prodRoots.push(ref);
2227
+ }
2228
+ }
2229
+ const reach = (roots) => {
2230
+ const seen = new Set();
2231
+ const pending = [...roots];
2232
+ while (pending.length) {
2233
+ const ref = pending.pop();
2234
+ if (!ref || seen.has(ref)) {
2235
+ continue;
2236
+ }
2237
+ seen.add(ref);
2238
+ pending.push(...(childrenByRef.get(ref) || []));
2239
+ }
2240
+ return seen;
2241
+ };
2242
+ const requiredRefs = reach(prodRoots);
2243
+ const optionalRefs = new Set(
2244
+ [...reach(testRoots)].filter((ref) => !requiredRefs.has(ref)),
2245
+ );
2246
+ // Components from Package.resolved can carry a different version or URL
2247
+ // spelling than the graph, so the package names are reported as well
2248
+ const namesOf = (refs) =>
2249
+ new Set([...refs].flatMap((ref) => keysByRef.get(ref) || []));
2250
+ return {
2251
+ optionalRefs,
2252
+ requiredRefs,
2253
+ optionalNames: namesOf(optionalRefs),
2254
+ requiredNames: namesOf(requiredRefs),
2255
+ };
2256
+ }
2257
+
2258
+ /**
2259
+ * Decide whether a dump-package target is a test target. The `type` field is
2260
+ * usually the string `"test"`, but manifest APIs have also been observed
2261
+ * representing it as an object.
2262
+ *
2263
+ * @param {Object} atarget dump-package target
2264
+ * @returns {boolean} `true` for test targets
2265
+ */
2266
+ export function isSwiftTestTarget(atarget) {
2267
+ return atarget?.type === "test" || atarget?.type?.test === true;
2268
+ }
2269
+
1930
2270
  /**
1931
2271
  * Parse a CMake-generated dot/graphviz file and extract components and their dependency
1932
2272
  * relationships.
@@ -2047,12 +2387,256 @@ export function parseCmakeDotFile(dotFile, pkgType, options = {}) {
2047
2387
  };
2048
2388
  }
2049
2389
 
2390
+ /** CMake `URL_HASH` algorithm names, their CycloneDX spelling and hex length. */
2391
+ const CMAKE_HASH_ALGORITHMS = {
2392
+ MD5: ["MD5", 32],
2393
+ SHA1: ["SHA-1", 40],
2394
+ SHA256: ["SHA-256", 64],
2395
+ SHA384: ["SHA-384", 96],
2396
+ SHA512: ["SHA-512", 128],
2397
+ SHA3_256: ["SHA3-256", 64],
2398
+ SHA3_384: ["SHA3-384", 96],
2399
+ SHA3_512: ["SHA3-512", 128],
2400
+ };
2401
+
2402
+ /** Keywords of the CMake `find_*` commands; they are never package names. */
2403
+ const CMAKE_FIND_KEYWORDS = new Set([
2404
+ "BYPASS_PROVIDER",
2405
+ "CMAKE_FIND_ROOT_PATH_BOTH",
2406
+ "COMPONENTS",
2407
+ "CONFIG",
2408
+ "CONFIGS",
2409
+ "DOC",
2410
+ "ENV",
2411
+ "EXACT",
2412
+ "GLOBAL",
2413
+ "HINTS",
2414
+ "MODULE",
2415
+ "NAMES",
2416
+ "NAMES_PER_DIR",
2417
+ "NO_CACHE",
2418
+ "NO_CMAKE_BUILDS_PATH",
2419
+ "NO_CMAKE_ENVIRONMENT_PATH",
2420
+ "NO_CMAKE_FIND_ROOT_PATH",
2421
+ "NO_CMAKE_INSTALL_PREFIX",
2422
+ "NO_CMAKE_PACKAGE_REGISTRY",
2423
+ "NO_CMAKE_PATH",
2424
+ "NO_CMAKE_SYSTEM_PACKAGE_REGISTRY",
2425
+ "NO_CMAKE_SYSTEM_PATH",
2426
+ "NO_DEFAULT_PATH",
2427
+ "NO_MODULE",
2428
+ "NO_PACKAGE_ROOT_PATH",
2429
+ "NO_POLICY_SCOPE",
2430
+ "NO_SYSTEM_ENVIRONMENT_PATH",
2431
+ "ONLY_CMAKE_FIND_ROOT_PATH",
2432
+ "OPTIONAL",
2433
+ "OPTIONAL_COMPONENTS",
2434
+ "PATHS",
2435
+ "PATH_SUFFIXES",
2436
+ "QUIET",
2437
+ "REGISTRY_VIEW",
2438
+ "REQUIRED",
2439
+ "VALIDATOR",
2440
+ ]);
2441
+
2442
+ const CMAKE_FIND_COMMANDS = new Set([
2443
+ "find_package",
2444
+ "find_library",
2445
+ "find_dependency",
2446
+ "find_file",
2447
+ ]);
2448
+
2449
+ /**
2450
+ * A download URL that is safe to publish: http(s) or ftp only, with any user
2451
+ * information, query string and fragment removed.
2452
+ *
2453
+ * @param {string} url URL as written in the build file
2454
+ * @returns {string|undefined} The cleaned URL, or undefined when it is not one
2455
+ */
2456
+ function publishableDownloadUrl(url) {
2457
+ if (!url || url.includes("${")) {
2458
+ return undefined;
2459
+ }
2460
+ try {
2461
+ const parsed = new URL(url);
2462
+ if (!["http:", "https:", "ftp:"].includes(parsed.protocol)) {
2463
+ return undefined;
2464
+ }
2465
+ parsed.username = "";
2466
+ parsed.password = "";
2467
+ parsed.search = "";
2468
+ parsed.hash = "";
2469
+ return parsed.toString();
2470
+ } catch {
2471
+ return undefined;
2472
+ }
2473
+ }
2474
+
2475
+ /**
2476
+ * The repository and version of a GitHub source archive URL:
2477
+ * `https://github.com/<owner>/<repo>/archive/<ref>.zip` (also `refs/tags/...`
2478
+ * and `.tar.gz`) and
2479
+ * `https://github.com/<owner>/<repo>/releases/download/<tag>/<file>`.
2480
+ *
2481
+ * @param {string} url Download URL
2482
+ * @returns {{owner: string, repo: string, version: string}|undefined}
2483
+ */
2484
+ function githubArchiveCoordinates(url) {
2485
+ let parsed;
2486
+ try {
2487
+ parsed = new URL(url);
2488
+ } catch {
2489
+ return undefined;
2490
+ }
2491
+ if (parsed.hostname !== "github.com") {
2492
+ return undefined;
2493
+ }
2494
+ const segments = parsed.pathname.split("/").filter(Boolean);
2495
+ if (segments.length < 4) {
2496
+ return undefined;
2497
+ }
2498
+ const [owner, repo, section, ...rest] = segments;
2499
+ let version;
2500
+ if (section === "archive") {
2501
+ let ref = rest.join("/");
2502
+ for (const prefix of ["refs/tags/", "refs/heads/"]) {
2503
+ if (ref.startsWith(prefix)) {
2504
+ ref = ref.slice(prefix.length);
2505
+ }
2506
+ }
2507
+ for (const ext of [".tar.gz", ".tar.bz2", ".tar.xz", ".tgz", ".zip"]) {
2508
+ if (ref.endsWith(ext)) {
2509
+ ref = ref.slice(0, -ext.length);
2510
+ break;
2511
+ }
2512
+ }
2513
+ version = ref;
2514
+ } else if (section === "releases" && rest[0] === "download" && rest[1]) {
2515
+ version = rest[1];
2516
+ }
2517
+ if (!version) {
2518
+ return undefined;
2519
+ }
2520
+ return { owner, repo: repo.replace(/\.git$/, ""), version };
2521
+ }
2522
+
2523
+ /**
2524
+ * The source archive of the WrapDB release a Meson dependency name resolved to,
2525
+ * as a distribution reference carrying the archive's hash. The hash describes
2526
+ * that release's archive, which is not known to be the version the project
2527
+ * builds, so it belongs to the reference rather than to the component.
2528
+ *
2529
+ * @param {Object} awrap WrapDB entry
2530
+ * @returns {Object|undefined} External reference, or undefined without a URL
2531
+ */
2532
+ function wrapDbReference(awrap) {
2533
+ const url = publishableDownloadUrl(awrap?.analyzed_source_url);
2534
+ if (!url) {
2535
+ return undefined;
2536
+ }
2537
+ const reference = {
2538
+ type: "distribution",
2539
+ url,
2540
+ comment:
2541
+ `WrapDB release ${awrap.name_with_version || awrap.analyzed_version || ""}`.trim(),
2542
+ };
2543
+ if (/^[0-9a-fA-F]{64}$/.test(awrap.analyzed_source_hash || "")) {
2544
+ reference.hashes = [
2545
+ { alg: "SHA-256", content: awrap.analyzed_source_hash.toLowerCase() },
2546
+ ];
2547
+ }
2548
+ return reference;
2549
+ }
2550
+
2551
+ /**
2552
+ * The component for a dependency a CMake file downloads at configure time
2553
+ * (`FetchContent_Declare`, `ExternalProject_Add`, `CPMAddPackage`).
2554
+ *
2555
+ * A git repository becomes a `github` purl, or a `generic` one with a
2556
+ * `vcs_url`, at its tag or commit. A GitHub archive URL becomes a `github` purl
2557
+ * at the ref it names. Any other URL becomes a `generic` purl with a
2558
+ * `download_url`, and a `URL_HASH` becomes a hash. A version that still holds an
2559
+ * unresolved `${VAR}` is left out rather than published.
2560
+ *
2561
+ * @param {Object} dep Dependency facts from `cmakeFetchDependencies`
2562
+ * @param {string} srcFile The CMake file the dependency was read from
2563
+ * @returns {Object} The component
2564
+ */
2565
+ export function cmakeFetchDependencyComponent(dep, srcFile) {
2566
+ const resolved = (value) =>
2567
+ value && !value.includes("${") ? value : undefined;
2568
+ let version = resolved(dep.gitTag) || resolved(dep.version);
2569
+ let coords;
2570
+ const gitUrl = publishableDownloadUrl(dep.gitRepository);
2571
+ const downloadUrl = publishableDownloadUrl(dep.url);
2572
+ if (gitUrl) {
2573
+ coords = submodulePurlCoordinates(gitUrl, version);
2574
+ } else if (downloadUrl) {
2575
+ const archive = githubArchiveCoordinates(downloadUrl);
2576
+ if (archive) {
2577
+ version = version || archive.version;
2578
+ coords = {
2579
+ type: "github",
2580
+ namespace: archive.owner,
2581
+ name: archive.repo,
2582
+ version,
2583
+ };
2584
+ } else {
2585
+ coords = {
2586
+ type: "generic",
2587
+ name: dep.name,
2588
+ version: version || null,
2589
+ qualifiers: { download_url: downloadUrl },
2590
+ };
2591
+ }
2592
+ }
2593
+ const purl =
2594
+ (coords && tryBuildPurl(coords)) ||
2595
+ tryBuildPurl({ type: "generic", name: dep.name, version: version || null });
2596
+ const confidence = version ? 0.6 : 0.3;
2597
+ const component = {
2598
+ name: dep.name,
2599
+ version: version || "",
2600
+ type: "library",
2601
+ evidence: {
2602
+ identity: {
2603
+ field: "purl",
2604
+ confidence,
2605
+ methods: [
2606
+ {
2607
+ technique: "manifest-analysis",
2608
+ confidence,
2609
+ value: `Filename ${srcFile}`,
2610
+ },
2611
+ ],
2612
+ },
2613
+ },
2614
+ properties: [
2615
+ { name: "cdx:cmake:depKind", value: dep.kind },
2616
+ { name: "cdx:cmake:resolvedVia", value: "cmake-lists" },
2617
+ ],
2618
+ };
2619
+ applyPurl(component, purl, `library:${dep.name}:${version || ""}`);
2620
+ const [alg, content] = (dep.urlHash || "").split("=");
2621
+ const [cdxAlg, hexLength] = CMAKE_HASH_ALGORITHMS[alg?.toUpperCase()] || [];
2622
+ if (
2623
+ cdxAlg &&
2624
+ content?.length === hexLength &&
2625
+ /^[0-9a-fA-F]+$/.test(content)
2626
+ ) {
2627
+ component.hashes = [{ alg: cdxAlg, content: content.toLowerCase() }];
2628
+ }
2629
+ return component;
2630
+ }
2631
+
2050
2632
  /**
2051
2633
  * Parse a CMake-like build file (CMakeLists.txt, meson.build, etc.) and extract the
2052
2634
  * parent component and list of dependency packages.
2053
2635
  *
2054
2636
  * Handles `set`, `project`, `find_package`, `find_library`, `find_dependency`,
2055
- * `find_file`, `FetchContent_MakeAvailable`, and `dependency()` directives.
2637
+ * `find_file` and `dependency()` directives (CMake command names in any case),
2638
+ * and, for CMake files, the dependencies downloaded at configure time by
2639
+ * `FetchContent_Declare`, `ExternalProject_Add` and `CPMAddPackage`.
2056
2640
  * Uses the MesonWrapDB to improve name resolution confidence.
2057
2641
  *
2058
2642
  * @param {string} cmakeListFile Path to the CMake-like build file
@@ -2061,13 +2645,30 @@ export function parseCmakeDotFile(dotFile, pkgType, options = {}) {
2061
2645
  * @returns {{ parentComponent: Object, pkgList: Object[] }}
2062
2646
  */
2063
2647
  export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
2064
- let cmakeListData = readFileSync(cmakeListFile, { encoding: "utf-8" });
2648
+ const rawData = readFileSync(cmakeListFile, { encoding: "utf-8" });
2649
+ let cmakeListData = rawData;
2065
2650
  const pkgList = [];
2066
2651
  const pkgAddedMap = {};
2067
2652
  const versionSpecifiersMap = {};
2068
2653
  const versionsMap = {};
2069
2654
  let parentComponent = {};
2070
2655
  const templateValues = {};
2656
+ // A dependency fetched at configure time is the same package a find_package()
2657
+ // of the same name asks for: the fetched component, with its source and pin,
2658
+ // stands for both.
2659
+ const fetchedNames = new Set();
2660
+ if (!basename(cmakeListFile).startsWith("meson")) {
2661
+ const fetchedRefs = new Set();
2662
+ for (const dep of cmakeFetchDependencies(rawData)) {
2663
+ const component = cmakeFetchDependencyComponent(dep, cmakeListFile);
2664
+ if (fetchedRefs.has(component["bom-ref"])) {
2665
+ continue;
2666
+ }
2667
+ fetchedRefs.add(component["bom-ref"]);
2668
+ fetchedNames.add(dep.name.toLowerCase());
2669
+ pkgList.push(component);
2670
+ }
2671
+ }
2071
2672
  cmakeListData = cmakeListData
2072
2673
  .replace(/^ {2}/g, "")
2073
2674
  .replace(/\(\r\n/g, "(")
@@ -2082,138 +2683,129 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
2082
2683
  const group = "";
2083
2684
  const path = undefined;
2084
2685
  const name_list = [];
2085
- if (l.startsWith("set")) {
2086
- const tmpA = l.replace("set(", "").replace(")", "").trim().split(" ");
2686
+ // CMake command names are case-insensitive: `PROJECT(...)` and
2687
+ // `Find_Package(...)` are the same commands as their lower-case spellings.
2688
+ const openParen = l.indexOf("(");
2689
+ const command =
2690
+ openParen > 0 ? l.slice(0, openParen).trim().toLowerCase() : "";
2691
+ let commandArgs = openParen > 0 ? l.slice(openParen + 1) : "";
2692
+ if (command === "set") {
2693
+ const tmpA = commandArgs.replace(")", "").trim().split(" ");
2087
2694
  if (tmpA && tmpA.length === 2) {
2088
2695
  templateValues[tmpA[0]] = tmpA[1];
2089
2696
  }
2090
- } else if (
2091
- l.startsWith("project") &&
2092
- !Object.keys(parentComponent).length
2093
- ) {
2094
- if (l.includes("${")) {
2697
+ } else if (command === "project" && !Object.keys(parentComponent).length) {
2698
+ if (commandArgs.includes("${")) {
2095
2699
  for (const tmplKey of Object.keys(templateValues)) {
2096
- l = l.replace(`\${${tmplKey}}`, templateValues[tmplKey] || "");
2700
+ commandArgs = commandArgs.replace(
2701
+ `\${${tmplKey}}`,
2702
+ templateValues[tmplKey] || "",
2703
+ );
2097
2704
  }
2098
2705
  }
2099
- const tmpA = l.replace("project (", "project(").split("project(");
2100
- if (tmpA && tmpA.length > 1) {
2101
- const tmpB = (tmpA[1] || "")
2102
- .trim()
2103
- .replace(/["']/g, "")
2104
- .replace(/ /g, ",")
2105
- .split(")")[0]
2106
- .split(",")
2107
- .filter((v) => v.length > 1);
2108
- const parentName =
2109
- tmpB.length > 0 ? tmpB[0].replace(":", "").trim() : "";
2110
- let parentVersion;
2111
- // In case of meson.build we can find the version number after the word version
2112
- // thanks to our replaces and splits
2113
- const versionIndex = tmpB.findIndex(
2114
- (v) => v?.toLowerCase() === "version",
2706
+ const tmpB = commandArgs
2707
+ .trim()
2708
+ .replace(/["']/g, "")
2709
+ .replace(/ /g, ",")
2710
+ .split(")")[0]
2711
+ .split(",")
2712
+ .filter((v) => v.length > 1);
2713
+ const parentName = tmpB.length > 0 ? tmpB[0].replace(":", "").trim() : "";
2714
+ let parentVersion;
2715
+ // In case of meson.build we can find the version number after the word version
2716
+ // thanks to our replaces and splits
2717
+ const versionIndex = tmpB.findIndex(
2718
+ (v) => v?.toLowerCase() === "version",
2719
+ );
2720
+ if (versionIndex > -1 && tmpB.length > versionIndex) {
2721
+ parentVersion = tmpB[versionIndex + 1];
2722
+ }
2723
+ // A `${VAR}` that no `set()` in this file defines stays literal. It is
2724
+ // not a version, and percent-encoding it into a purl publishes the
2725
+ // variable name as the version, so the field is left empty instead.
2726
+ // CMakeCache.txt carries the evaluated value when a build tree exists.
2727
+ if (parentVersion?.includes("$")) {
2728
+ parentVersion = undefined;
2729
+ }
2730
+ if (parentName?.length && !parentName.includes("$")) {
2731
+ parentComponent = {
2732
+ group: options.projectGroup || "",
2733
+ name: parentName,
2734
+ version: parentVersion || options.projectVersion || "",
2735
+ type: "application",
2736
+ };
2737
+ const safePath = path ? path.replace(/^\/+/, "") : null;
2738
+ parentComponent["purl"] = build({
2739
+ type: pkgType,
2740
+ namespace: parentComponent.group || null,
2741
+ name: parentComponent.name,
2742
+ version: parentComponent.version || null,
2743
+ subpath: safePath,
2744
+ });
2745
+ parentComponent["bom-ref"] = decodeURIComponent(
2746
+ parentComponent["purl"],
2115
2747
  );
2116
- if (versionIndex > -1 && tmpB.length > versionIndex) {
2117
- parentVersion = tmpB[versionIndex + 1];
2118
- }
2119
- // A `${VAR}` that no `set()` in this file defines stays literal. It is
2120
- // not a version, and percent-encoding it into a purl publishes the
2121
- // variable name as the version, so the field is left empty instead.
2122
- // CMakeCache.txt carries the evaluated value when a build tree exists.
2123
- if (parentVersion?.includes("$")) {
2124
- parentVersion = undefined;
2125
- }
2126
- if (parentName?.length && !parentName.includes("$")) {
2127
- parentComponent = {
2128
- group: options.projectGroup || "",
2129
- name: parentName,
2130
- version: parentVersion || options.projectVersion || "",
2131
- type: "application",
2132
- };
2133
- const safePath = path ? path.replace(/^\/+/, "") : null;
2134
- parentComponent["purl"] = build({
2135
- type: pkgType,
2136
- namespace: parentComponent.group || null,
2137
- name: parentComponent.name,
2138
- version: parentComponent.version || null,
2139
- subpath: safePath,
2140
- });
2141
- parentComponent["bom-ref"] = decodeURIComponent(
2142
- parentComponent["purl"],
2143
- );
2144
- }
2145
2748
  }
2146
- } else if (l.startsWith("find_")) {
2147
- let tmpA = [];
2148
- for (const fm of [
2149
- "find_package(",
2150
- "find_library(",
2151
- "find_dependency(",
2152
- "find_file(",
2153
- "FetchContent_MakeAvailable(",
2154
- ]) {
2155
- if (l.startsWith(fm)) {
2156
- tmpA = l.split(fm);
2157
- break;
2749
+ } else if (CMAKE_FIND_COMMANDS.has(command)) {
2750
+ let tmpB = commandArgs.split(")")[0].split(" ");
2751
+ tmpB = tmpB.filter(
2752
+ (v) =>
2753
+ !CMAKE_FIND_KEYWORDS.has(v) &&
2754
+ !v.includes("$") &&
2755
+ !v.startsWith("CMAKE_") &&
2756
+ v.length,
2757
+ );
2758
+ // find_package(Catch2)
2759
+ // find_package(GTest REQUIRED)
2760
+ // find_package(Boost 1.79 COMPONENTS date_time)
2761
+ // find_library(PTHREADPOOL_LIB pthreadpool REQUIRED)
2762
+ // find_library and find_file name the cache variable that receives the
2763
+ // result in their first argument, so that argument is a destination
2764
+ // rather than a package. Every other command in this list opens with a
2765
+ // package name, including names such as `ZLIB` that read like a
2766
+ // variable.
2767
+ // Their names are the values after NAMES, or else the one argument after
2768
+ // the variable; the remaining arguments are search paths and options.
2769
+ if (command === "find_library" || command === "find_file") {
2770
+ const tokens = commandArgs
2771
+ .split(")")[0]
2772
+ .split(" ")
2773
+ .filter((v) => v.length);
2774
+ const namesAt = tokens.indexOf("NAMES");
2775
+ let names = tokens.slice(1, 2);
2776
+ if (namesAt > -1) {
2777
+ names = [];
2778
+ for (const token of tokens.slice(namesAt + 1)) {
2779
+ if (CMAKE_FIND_KEYWORDS.has(token)) {
2780
+ break;
2781
+ }
2782
+ names.push(token);
2783
+ }
2158
2784
  }
2785
+ tmpB = names.filter((v) => !v.includes("$") && !v.startsWith("CMAKE_"));
2159
2786
  }
2160
- if (tmpA && tmpA.length > 1) {
2161
- let tmpB = tmpA[1].split(")")[0].split(" ");
2162
- tmpB = tmpB.filter(
2163
- (v) =>
2164
- ![
2165
- "REQUIRED",
2166
- "COMPONENTS",
2167
- "QUIET",
2168
- "NAMES",
2169
- "PATHS",
2170
- "ENV",
2171
- "NO_MODULE",
2172
- "NO_DEFAULT_PATH",
2173
- ].includes(v) &&
2174
- !v.includes("$") &&
2175
- !v.startsWith("CMAKE_") &&
2176
- v.length,
2177
- );
2178
- // find_package(Catch2)
2179
- // find_package(GTest REQUIRED)
2180
- // find_package(Boost 1.79 COMPONENTS date_time)
2181
- // find_library(PTHREADPOOL_LIB pthreadpool REQUIRED)
2182
- // find_library and find_file name the cache variable that receives the
2183
- // result in their first argument, so that argument is a destination
2184
- // rather than a package. Every other command in this list opens with a
2185
- // package name, including names such as `ZLIB` that read like a
2186
- // variable.
2187
- if (l.startsWith("find_library") || l.startsWith("find_file")) {
2188
- tmpB = tmpB.slice(1);
2189
- }
2190
- if (tmpB.length) {
2191
- const working_name = tmpB[0];
2192
- name_list.push(working_name);
2193
- if (l.startsWith("find_package") && tmpB.length > 1) {
2194
- if (
2195
- /^\d+/.test(tmpB[1]) &&
2196
- !tmpB[1].includes("${") &&
2197
- !tmpB[1].startsWith("@")
2198
- ) {
2199
- versionsMap[working_name] = tmpB[1];
2787
+ if (tmpB.length) {
2788
+ const working_name = tmpB[0];
2789
+ name_list.push(working_name);
2790
+ if (command === "find_package" && tmpB.length > 1) {
2791
+ if (
2792
+ /^\d+/.test(tmpB[1]) &&
2793
+ !tmpB[1].includes("${") &&
2794
+ !tmpB[1].startsWith("@")
2795
+ ) {
2796
+ versionsMap[working_name] = tmpB[1];
2797
+ }
2798
+ } else {
2799
+ for (const n of tmpB) {
2800
+ if (n.match(/^\d/)) {
2801
+ continue;
2200
2802
  }
2201
- } else {
2202
- for (const n of tmpB) {
2203
- if (n.match(/^\d/)) {
2204
- continue;
2205
- }
2206
- if (n.includes(_sep)) {
2207
- if (
2208
- n.includes(".so") ||
2209
- n.includes(".a") ||
2210
- n.includes(".dll")
2211
- ) {
2212
- name_list.push(basename(n));
2213
- }
2214
- } else {
2215
- name_list.push(n);
2803
+ if (n.includes(_sep)) {
2804
+ if (n.includes(".so") || n.includes(".a") || n.includes(".dll")) {
2805
+ name_list.push(basename(n));
2216
2806
  }
2807
+ } else {
2808
+ name_list.push(n);
2217
2809
  }
2218
2810
  }
2219
2811
  }
@@ -2245,10 +2837,12 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
2245
2837
  for (let n of name_list) {
2246
2838
  const props = [];
2247
2839
  let confidence = 0;
2840
+ let wrapReference;
2248
2841
  if (
2249
2842
  n &&
2250
2843
  n.length > 1 &&
2251
2844
  !pkgAddedMap[n] &&
2845
+ !fetchedNames.has(n.toLowerCase()) &&
2252
2846
  !n.startsWith(_sep) &&
2253
2847
  !n.startsWith("@")
2254
2848
  ) {
@@ -2263,14 +2857,18 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
2263
2857
  ) {
2264
2858
  // Use the new name
2265
2859
  n = wrapkey;
2266
- for (const eprop of Object.keys(awrap)) {
2860
+ wrapReference = wrapDbReference(awrap);
2861
+ props.push({ name: "cdx:meson:wrapdb:wrap", value: wrapkey });
2862
+ if (awrap.analyzed_version) {
2267
2863
  props.push({
2268
- name: eprop,
2269
- value: Array.isArray(awrap[eprop])
2270
- ? awrap[eprop].join("|")
2271
- : awrap[eprop],
2864
+ name: "cdx:meson:wrapdb:latestVersion",
2865
+ value: awrap.analyzed_version,
2272
2866
  });
2273
2867
  }
2868
+ props.push({
2869
+ name: "internal:PkgProvides",
2870
+ value: awrap.PkgProvides.join("|"),
2871
+ });
2274
2872
  // Our confidence has improved from 0 since there is a matching wrap so we know the correct name
2275
2873
  // and url. We lack the version details.
2276
2874
  confidence = 0.5;
@@ -2321,6 +2919,9 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
2321
2919
  },
2322
2920
  properties: props,
2323
2921
  };
2922
+ if (wrapReference) {
2923
+ apkg.externalReferences = [wrapReference];
2924
+ }
2324
2925
  apkg["bom-ref"] = decodeURIComponent(apkg["purl"]);
2325
2926
  pkgList.push(apkg);
2326
2927
  pkgAddedMap[n] = true;
@@ -2350,41 +2951,48 @@ export function parseCUsageSlice(sliceData) {
2350
2951
  return undefined;
2351
2952
  }
2352
2953
  const usageData = {};
2353
- try {
2354
- const objectSlices = sliceData.objectSlices || [];
2355
- for (const slice of objectSlices) {
2954
+ const objectSlices = sliceData.objectSlices || [];
2955
+ for (const slice of objectSlices) {
2956
+ // One malformed slice must not cost the slices after it.
2957
+ try {
2958
+ const fileName = slice?.fileName || "";
2959
+ const isInclude = (slice?.code || "").startsWith("#include");
2356
2960
  if (
2357
- (!slice.fileName && !slice.code.startsWith("#include")) ||
2358
- slice.fileName.startsWith("<includes") ||
2359
- slice.fileName.startsWith("<global") ||
2360
- slice.fileName.includes("__")
2961
+ (!fileName && !isInclude) ||
2962
+ fileName.startsWith("<includes") ||
2963
+ fileName.startsWith("<global") ||
2964
+ fileName.includes("__")
2361
2965
  ) {
2362
2966
  continue;
2363
2967
  }
2364
- const slFileName = slice.fileName;
2365
- const allLines = usageData[slFileName] || new Set();
2968
+ const allLines = (fileName && usageData[fileName]) || new Set();
2366
2969
  if (slice.fullName && slice.fullName.length > 3) {
2367
- if (slice.code?.startsWith("#include")) {
2368
- usageData[slice.fullName] = new Set();
2970
+ if (isInclude) {
2971
+ // an included file keeps the symbols already recorded for it
2972
+ usageData[slice.fullName] = usageData[slice.fullName] || new Set();
2369
2973
  } else {
2370
2974
  allLines.add(slice.fullName);
2371
2975
  }
2372
2976
  }
2373
- for (const ausage of slice.usages) {
2374
- let calls = ausage?.invokedCalls || [];
2375
- calls = calls.concat(ausage?.argToCalls || []);
2977
+ for (const ausage of slice.usages || []) {
2978
+ const calls = (ausage?.invokedCalls || []).concat(
2979
+ ausage?.argToCalls || [],
2980
+ );
2376
2981
  for (const acall of calls) {
2377
- if (!acall.resolvedMethod.includes("->")) {
2378
- allLines.add(acall.resolvedMethod);
2982
+ const resolvedMethod = acall?.resolvedMethod;
2983
+ if (resolvedMethod && !resolvedMethod.includes("->")) {
2984
+ allLines.add(resolvedMethod);
2379
2985
  }
2380
2986
  }
2381
2987
  }
2382
- if (Array.from(allLines).length) {
2383
- usageData[slFileName] = allLines;
2988
+ if (fileName && allLines.size) {
2989
+ usageData[fileName] = allLines;
2384
2990
  }
2991
+ } catch (err) {
2992
+ thoughtLog("Skipping a C usage slice that could not be read.", {
2993
+ error: err?.message,
2994
+ });
2385
2995
  }
2386
- } catch (_err) {
2387
- // ignore
2388
2996
  }
2389
2997
  return usageData;
2390
2998
  }
@@ -2416,14 +3024,67 @@ export function parseMakeDFile(dfile) {
2416
3024
  return pkgFilesMap;
2417
3025
  }
2418
3026
 
3027
+ /**
3028
+ * Resolve a path a lockfile names against the lockfile's directory, refusing
3029
+ * absolute paths and any that leave the scanned directory. The value comes
3030
+ * from the scanned project, so it must not steer the scan at files outside
3031
+ * it. The bound is the scan root, not the lockfile's directory: a React
3032
+ * Native `ios/Podfile` names its pods as `../node_modules/...`.
3033
+ *
3034
+ * @param {String} projectPath Directory of the Podfile.lock
3035
+ * @param {String} value Path as the lockfile writes it
3036
+ * @param {String} [scanRoot] Directory the scan was asked for; the lockfile's
3037
+ * directory when not given
3038
+ * @returns {String|undefined} The resolved path, or undefined when refused
3039
+ */
3040
+ function resolveInsideScan(projectPath, value, scanRoot = projectPath) {
3041
+ if (typeof value !== "string" || !value.length) {
3042
+ return undefined;
3043
+ }
3044
+ if (
3045
+ isAbsolute(value) ||
3046
+ /^[a-zA-Z]:[\\/]/.test(value) ||
3047
+ value.startsWith("\\\\")
3048
+ ) {
3049
+ return undefined;
3050
+ }
3051
+ const resolved = resolve(projectPath, value);
3052
+ const back = relative(resolve(scanRoot), resolved);
3053
+ if (back.startsWith("..") || isAbsolute(back)) {
3054
+ return undefined;
3055
+ }
3056
+ return resolved;
3057
+ }
3058
+
3059
+ /**
3060
+ * Whether a pod name can be used as a file name. Pod names come from the
3061
+ * lockfile and are joined onto directories.
3062
+ *
3063
+ * @param {String} podName Root pod name
3064
+ * @returns {Boolean}
3065
+ */
3066
+ function isSafePodFileName(podName) {
3067
+ return (
3068
+ typeof podName === "string" &&
3069
+ podName.length > 0 &&
3070
+ !podName.includes("/") &&
3071
+ !podName.includes("\\") &&
3072
+ !podName.includes("\0") &&
3073
+ podName !== "." &&
3074
+ podName !== ".."
3075
+ );
3076
+ }
3077
+
2419
3078
  /**
2420
3079
  * Parse the contents of a 'Podfile.lock'
2421
3080
  *
2422
3081
  * @param {Object} podfileLock The content of the podfile.lock as an Object
2423
3082
  * @param {String} projectPath The path to the project root
3083
+ * @param {String} [scanRoot] Directory the scan was asked for. Local `:path`
3084
+ * and `:podspec` sources outside it are not read. Defaults to projectPath.
2424
3085
  * @returns {Map} Map of all dependencies with their direct dependencies
2425
3086
  */
2426
- export async function parsePodfileLock(podfileLock, projectPath) {
3087
+ export async function parsePodfileLock(podfileLock, projectPath, scanRoot) {
2427
3088
  const dependencies = new Map();
2428
3089
  for (const pod of podfileLock["PODS"]) {
2429
3090
  const dependency = {};
@@ -2445,58 +3106,103 @@ export async function parsePodfileLock(podfileLock, projectPath) {
2445
3106
  dependency.metadata.name.indexOf("/"),
2446
3107
  )
2447
3108
  : dependency.metadata.name;
2448
- if (podfileLock["EXTERNAL SOURCES"]?.[podName]) {
3109
+ if (
3110
+ podfileLock["EXTERNAL SOURCES"]?.[podName] &&
3111
+ isSafePodFileName(podName)
3112
+ ) {
2449
3113
  const externalPod = podfileLock["EXTERNAL SOURCES"][podName];
2450
3114
  if (externalPod[":git"]) {
2451
- let projectRepo = externalPod[":git"];
2452
- if (isGitHubUrl(projectRepo)) {
2453
- projectRepo = projectRepo.replace(
2454
- "github.com",
2455
- "raw.githubusercontent.com",
2456
- );
2457
- }
2458
- if (projectRepo.endsWith(".git")) {
2459
- projectRepo = projectRepo.substring(0, projectRepo.length - 4);
3115
+ // The podspec of a checked out pod, which `pod install` writes
3116
+ // alongside the sources it downloaded. Reading it needs no request,
3117
+ // so it answers before any raw repository URL.
3118
+ const localPodspec = join(
3119
+ projectPath,
3120
+ "Pods",
3121
+ "Local Podspecs",
3122
+ `${podName}.podspec.json`,
3123
+ );
3124
+ if (safeExistsSync(localPodspec)) {
3125
+ dependency.metadata.properties = [
3126
+ {
3127
+ name: "cdx:pods:podspecLocation",
3128
+ value: localPodspec,
3129
+ },
3130
+ ];
3131
+ } else {
3132
+ let projectRepo = externalPod[":git"];
3133
+ if (isGitHubUrl(projectRepo)) {
3134
+ projectRepo = projectRepo.replace(
3135
+ "github.com",
3136
+ "raw.githubusercontent.com",
3137
+ );
3138
+ }
3139
+ if (projectRepo.endsWith(".git")) {
3140
+ projectRepo = projectRepo.substring(0, projectRepo.length - 4);
3141
+ }
3142
+ // The commit the lockfile recorded in CHECKOUT OPTIONS names the
3143
+ // exact revision CocoaPods checked out, so its podspec is asked
3144
+ // for directly instead of probing the default branch names. It
3145
+ // wins over a branch, whose head may have moved since.
3146
+ const checkoutCommit =
3147
+ podfileLock["CHECKOUT OPTIONS"]?.[podName]?.[":commit"];
3148
+ const projectRepoBranchOrTag =
3149
+ typeof checkoutCommit === "string" &&
3150
+ /^[0-9a-f]{7,64}$/i.test(checkoutCommit)
3151
+ ? checkoutCommit
3152
+ : externalPod[":tag"]
3153
+ ? `refs/tags/${externalPod[":tag"]}`
3154
+ : externalPod[":branch"]
3155
+ ? `refs/heads/${externalPod[":branch"]}`
3156
+ : "refs/heads/<DEFAULT>";
3157
+ dependency.metadata.properties = [
3158
+ {
3159
+ name: "cdx:pods:podspecLocation",
3160
+ value: `${projectRepo}/${projectRepoBranchOrTag}/${podName}.podspec`,
3161
+ },
3162
+ ];
2460
3163
  }
2461
- const projectRepoBranchOrTag = externalPod[":tag"]
2462
- ? `tags/${externalPod[":tag"]}`
2463
- : `heads/${externalPod[":branch"] ? externalPod[":branch"] : "<DEFAULT>"}`;
2464
- dependency.metadata.properties = [
2465
- {
2466
- name: "cdx:pods:podspecLocation",
2467
- value: `${projectRepo}/refs/${projectRepoBranchOrTag}/${podName}.podspec`,
2468
- },
2469
- ];
2470
3164
  } else if (externalPod[":path"]) {
2471
- const projectLocation = resolve(projectPath, externalPod[":path"]);
2472
- dependency.metadata.properties = [
2473
- {
2474
- name: "cdx:pods:projectDir",
2475
- value: projectLocation,
2476
- },
2477
- ];
2478
- let podspec = join(projectLocation, `${podName}.podspec`);
2479
- if (!safeExistsSync(podspec)) {
2480
- podspec = `${podspec}.json`;
2481
- }
2482
- if (safeExistsSync(podspec)) {
2483
- dependency.metadata.properties.push({
2484
- name: "cdx:pods:podspecLocation",
2485
- value: podspec,
2486
- });
3165
+ const projectLocation = resolveInsideScan(
3166
+ projectPath,
3167
+ externalPod[":path"],
3168
+ scanRoot,
3169
+ );
3170
+ if (projectLocation) {
3171
+ dependency.metadata.properties = [
3172
+ {
3173
+ name: "cdx:pods:projectDir",
3174
+ value: projectLocation,
3175
+ },
3176
+ ];
3177
+ let podspec = join(projectLocation, `${podName}.podspec`);
3178
+ if (!safeExistsSync(podspec)) {
3179
+ podspec = `${podspec}.json`;
3180
+ }
3181
+ if (safeExistsSync(podspec)) {
3182
+ dependency.metadata.properties.push({
3183
+ name: "cdx:pods:podspecLocation",
3184
+ value: podspec,
3185
+ });
3186
+ }
2487
3187
  }
2488
3188
  } else if (externalPod[":podspec"]) {
2489
- const podspecLocation = resolve(projectPath, externalPod[":podspec"]);
2490
- dependency.metadata.properties = [
2491
- {
2492
- name: "cdx:pods:projectDir",
2493
- value: dirname(podspecLocation),
2494
- },
2495
- {
2496
- name: "cdx:pods:podspecLocation",
2497
- value: podspecLocation,
2498
- },
2499
- ];
3189
+ const podspecLocation = resolveInsideScan(
3190
+ projectPath,
3191
+ externalPod[":podspec"],
3192
+ scanRoot,
3193
+ );
3194
+ if (podspecLocation) {
3195
+ dependency.metadata.properties = [
3196
+ {
3197
+ name: "cdx:pods:projectDir",
3198
+ value: dirname(podspecLocation),
3199
+ },
3200
+ {
3201
+ name: "cdx:pods:podspecLocation",
3202
+ value: podspecLocation,
3203
+ },
3204
+ ];
3205
+ }
2500
3206
  }
2501
3207
  }
2502
3208
  dependencies.set(dependency.metadata.name, dependency);
@@ -2822,6 +3528,8 @@ export function resetCocoaPodspecPrefetch() {
2822
3528
  cocoaPodspecPrefetch = new Map();
2823
3529
  }
2824
3530
 
3531
+ registerRunReset("cocoapods:podspecs", resetCocoaPodspecPrefetch);
3532
+
2825
3533
  /**
2826
3534
  * Prefetch the remote podspecs a full CocoaPods scan is about to read.
2827
3535
  *
@@ -2924,7 +3632,7 @@ async function fullScanCocoaPod(dependency, component, options) {
2924
3632
  try {
2925
3633
  httpResult =
2926
3634
  prefetchedResponse(cocoaPodspecPrefetch, candidate) ||
2927
- (await cdxgenAgent.get(candidate));
3635
+ (await gatedGet(candidate, { responseType: "text" }));
2928
3636
  podspecLocation = candidate;
2929
3637
  } catch (_err) {
2930
3638
  continue;
@@ -2939,6 +3647,11 @@ async function fullScanCocoaPod(dependency, component, options) {
2939
3647
  } else {
2940
3648
  podspecContent = readFileSync(podspecLocation, "utf-8");
2941
3649
  }
3650
+ // A location none of the candidates served contributes nothing: the
3651
+ // component keeps what the lockfile already said about the pod.
3652
+ if (podspecContent === undefined) {
3653
+ return;
3654
+ }
2942
3655
  for (const replacement of replacements) {
2943
3656
  const replacementPair = replacement.split("=");
2944
3657
  let match = replacementPair[0].replaceAll("<NEWLINE>", "\n");