@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
@@ -6,7 +6,14 @@ import {
6
6
  readFileSync,
7
7
  } from "node:fs";
8
8
  import { platform as _platform, userInfo as _userInfo, homedir } from "node:os";
9
- import { basename, join, resolve, win32 } from "node:path";
9
+ import {
10
+ basename,
11
+ isAbsolute,
12
+ join,
13
+ relative,
14
+ resolve,
15
+ win32,
16
+ } from "node:path";
10
17
  import process from "node:process";
11
18
  import stream from "node:stream/promises";
12
19
  import { URL } from "node:url";
@@ -293,6 +300,34 @@ let isWinLocalTLS = false;
293
300
  let isNerdctl;
294
301
  let isColima;
295
302
 
303
+ /**
304
+ * WSL Containers CLI (`wslc`), generally available with WSL 3.x. wslc accepts
305
+ * the exact docker CLI invocations cdxgen makes on Windows (`images
306
+ * --format=json`, `pull`, `inspect`, `save -o`), so it serves as a drop-in
307
+ * container CLI on Windows hosts that have WSL but no Docker installation.
308
+ *
309
+ * @see https://learn.microsoft.com/en-us/windows/wsl/wsl-container
310
+ */
311
+ const WSLC_CLI = "wslc";
312
+ let dockerCliInstalled;
313
+ let wslcInstalled;
314
+ // wslc reports container session startup failures (missing Virtual Machine
315
+ // Platform feature, no nested virtualization when Windows itself is a VM)
316
+ // with these markers on stderr.
317
+ const WSLC_SESSION_FAILURE_RE =
318
+ /HCS_E_HYPERV_NOT_INSTALLED|Virtual Machine Platform|WSL2 is unable to start/i;
319
+ const WSLC_SESSION_FAILURE_HINT =
320
+ "The wslc container session could not start. Ensure the 'Virtual Machine Platform' optional component is enabled by running 'wsl --install --no-distribution' with a subsequent reboot, and that virtualization is enabled in the firmware settings. Nested virtualization is required when Windows itself runs inside a VM.";
321
+ const WSLC_ALTERNATIVE_HINT =
322
+ "wslc (WSL Containers) is installed on this host. Set the environment variable DOCKER_CMD=wslc to pull and export Linux images through WSL, for example when Docker Desktop is not running or is switched to Windows containers.";
323
+ // Container spec files (compose, Kubernetes manifests) make cdxgen export one
324
+ // image after another in the same process, and every image fails the same way
325
+ // on a host with a broken container setup. Print each hint and log the CLI
326
+ // selection only once per process.
327
+ let wslcSessionFailureHintShown = false;
328
+ let wslcAlternativeHintShown = false;
329
+ let wslcSelectionRecorded = false;
330
+
296
331
  if (
297
332
  !readEnvironmentVariable("DOCKER_HOST") &&
298
333
  (readEnvironmentVariable("CONTAINERD_ADDRESS") ||
@@ -898,6 +933,154 @@ export const makeRequest = async (path, method, forRegistry) => {
898
933
  return await client.request(path, opts);
899
934
  };
900
935
 
936
+ const IMAGE_TAG_RE = /^[\w][\w.-]{0,127}$/;
937
+ const IMAGE_DIGEST_ALGO_RE = /^[a-z0-9]+(?:[.+_-][a-z0-9]+)*$/;
938
+ const IMAGE_DIGEST_HEX_RE = /^[a-f0-9]{32,}$/;
939
+ const IMAGE_COMPONENT_RE = /^[a-z0-9]+(?:[._-]+[a-z0-9]+)*$/;
940
+ const IMAGE_HOST_PORT_RE = /^\d{1,5}$/;
941
+ // A single label with a bounded quantifier and no alternation: checking hosts
942
+ // label by label keeps the cost linear on hostile inputs, which the equivalent
943
+ // whole-host pattern cannot promise (its nested quantifiers backtrack
944
+ // exponentially on strings such as "0.0" followed by many repetitions of "0").
945
+ const IMAGE_HOST_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
946
+
947
+ /**
948
+ * Validate the registry host of an image reference: a dotted name, `localhost`,
949
+ * or a bare name with a numeric port, each label 1-63 characters of letters,
950
+ * digits and inner hyphens.
951
+ *
952
+ * @param {string} host Host component without the trailing slash.
953
+ * @returns {boolean} true when the host is well-formed.
954
+ */
955
+ const isValidImageHost = (host) => {
956
+ let name = host;
957
+ let hasPort = false;
958
+ const colonIndex = name.lastIndexOf(":");
959
+ if (colonIndex !== -1) {
960
+ const port = name.slice(colonIndex + 1);
961
+ if (!IMAGE_HOST_PORT_RE.test(port)) {
962
+ return false;
963
+ }
964
+ name = name.slice(0, colonIndex);
965
+ hasPort = true;
966
+ }
967
+ if (name === "localhost") {
968
+ return true;
969
+ }
970
+ if (!name.length || name.length > 253) {
971
+ return false;
972
+ }
973
+ const labels = name.split(".");
974
+ if (labels.length < 2 && !hasPort) {
975
+ return false;
976
+ }
977
+ return labels.every((label) => IMAGE_HOST_LABEL_RE.test(label));
978
+ };
979
+
980
+ /**
981
+ * Decide whether a CLI path argument is a container image reference in the
982
+ * styles the container pipeline documents: name, name:tag, namespace/name,
983
+ * registry/namespace/name:tag, registry:port/..., and any of those with a
984
+ * @digest. Everything that smells like a filesystem path, a URL or a CLI
985
+ * option never matches, so strings such as "c:\Users", "c:/tmp" or
986
+ * "file:///etc" can never reach the container clients: backslashes and
987
+ * schemes are rejected outright, and a drive-letter prefix fails because a
988
+ * lone letter is neither a dotted host nor a host:port, and a tag may not
989
+ * begin with the "/" that follows the colon in "c:/tmp".
990
+ *
991
+ * The check mirrors how the distribution reference grammar detects a registry
992
+ * host: the first component counts as a host only when it carries a dot, a
993
+ * numeric port, or is exactly localhost. Each piece is validated by a linear
994
+ * regex or plain string splitting, and the input is length-capped, so hostile
995
+ * inputs cannot backtrack expensively.
996
+ *
997
+ * @param {string} fullImageName Candidate image reference
998
+ * @returns {boolean} true when the string is a plausible image reference
999
+ */
1000
+ export const isImageReference = (fullImageName) => {
1001
+ if (typeof fullImageName !== "string") {
1002
+ return false;
1003
+ }
1004
+ const ref = fullImageName.trim().toLowerCase();
1005
+ if (!ref.length || ref.length > 1024) {
1006
+ return false;
1007
+ }
1008
+ if (
1009
+ ref.includes("\\") ||
1010
+ ref.includes("://") ||
1011
+ ref.startsWith("/") ||
1012
+ ref.startsWith("./") ||
1013
+ ref.startsWith("../") ||
1014
+ ref.startsWith("-") ||
1015
+ ref.includes(" ") ||
1016
+ /^[a-z]:/.test(ref)
1017
+ ) {
1018
+ return false;
1019
+ }
1020
+ let remainder = ref;
1021
+ const atIndex = remainder.lastIndexOf("@");
1022
+ if (atIndex !== -1) {
1023
+ const digest = remainder.slice(atIndex + 1);
1024
+ remainder = remainder.slice(0, atIndex);
1025
+ if (remainder.includes("@") || !remainder.length) {
1026
+ return false;
1027
+ }
1028
+ const digestColonIndex = digest.indexOf(":");
1029
+ if (
1030
+ digestColonIndex <= 0 ||
1031
+ digestColonIndex === digest.length - 1 ||
1032
+ !IMAGE_DIGEST_ALGO_RE.test(digest.slice(0, digestColonIndex)) ||
1033
+ !IMAGE_DIGEST_HEX_RE.test(digest.slice(digestColonIndex + 1))
1034
+ ) {
1035
+ return false;
1036
+ }
1037
+ }
1038
+ const slashIndex = remainder.lastIndexOf("/");
1039
+ const colonIndex = remainder.lastIndexOf(":");
1040
+ if (colonIndex > slashIndex) {
1041
+ const tag = remainder.slice(colonIndex + 1);
1042
+ remainder = remainder.slice(0, colonIndex);
1043
+ if (!IMAGE_TAG_RE.test(tag)) {
1044
+ return false;
1045
+ }
1046
+ }
1047
+ if (!remainder.length) {
1048
+ return false;
1049
+ }
1050
+ const components = remainder.split("/");
1051
+ if (components.length > 1) {
1052
+ const host = components[0];
1053
+ const looksLikeHost =
1054
+ host.includes(".") || host.includes(":") || host === "localhost";
1055
+ if (looksLikeHost) {
1056
+ if (!isValidImageHost(host)) {
1057
+ return false;
1058
+ }
1059
+ components.shift();
1060
+ }
1061
+ }
1062
+ return components.every(
1063
+ (component) =>
1064
+ component.length > 0 &&
1065
+ component.length <= 255 &&
1066
+ IMAGE_COMPONENT_RE.test(component),
1067
+ );
1068
+ };
1069
+
1070
+ /**
1071
+ * Whether a string can be handed to the container CLI or the daemon API as an
1072
+ * image reference. The exact string is checked, not a trimmed copy, so what
1073
+ * reaches `pull`, `save` or `inspect` is a well-formed reference and never an
1074
+ * option (such as `-o file`), a path or a URL.
1075
+ *
1076
+ * @param {string} fullImageName Image reference as received
1077
+ * @returns {boolean} true when the string is safe to use as an image argument
1078
+ */
1079
+ const isSafeImageReference = (fullImageName) =>
1080
+ typeof fullImageName === "string" &&
1081
+ fullImageName === fullImageName.trim() &&
1082
+ isImageReference(fullImageName);
1083
+
901
1084
  /**
902
1085
  * Parse image name
903
1086
  *
@@ -983,19 +1166,129 @@ export const parseImageName = (fullImageName) => {
983
1166
  return nameObj;
984
1167
  };
985
1168
 
1169
+ /**
1170
+ * Detect whether the docker CLI binary is installed. `--version` runs without
1171
+ * a daemon, so the probe is safe on hosts where Docker is installed but not
1172
+ * running. The result is cached for the lifetime of the process.
1173
+ *
1174
+ * @returns {boolean} true when the docker CLI can be invoked
1175
+ */
1176
+ const isDockerCliInstalled = () => {
1177
+ if (dockerCliInstalled === undefined) {
1178
+ const result = safeSpawnSync("docker", ["--version"]);
1179
+ dockerCliInstalled = !result.error && result.status === 0;
1180
+ }
1181
+ return dockerCliInstalled;
1182
+ };
1183
+
1184
+ /**
1185
+ * Detect whether the WSL Containers CLI (`wslc`) is installed. `wslc version`
1186
+ * is answered by the CLI itself without starting a WSL container session, so
1187
+ * it succeeds even on hosts where nested virtualization is unavailable.
1188
+ * The result is cached for the lifetime of the process.
1189
+ *
1190
+ * @returns {boolean} true when the wslc CLI can be invoked
1191
+ */
1192
+ const isWslcInstalled = () => {
1193
+ if (wslcInstalled === undefined) {
1194
+ const result = safeSpawnSync(WSLC_CLI, ["version"]);
1195
+ wslcInstalled = !result.error && result.status === 0;
1196
+ }
1197
+ return wslcInstalled;
1198
+ };
1199
+
1200
+ /**
1201
+ * Recognise wslc failures caused by the WSL container session being unable to
1202
+ * start, as opposed to image-specific errors such as an unknown repository.
1203
+ *
1204
+ * @param {string|Buffer} [stderr] wslc command output to inspect
1205
+ * @returns {boolean} true when the output points to a session startup failure
1206
+ */
1207
+ const isWslcSessionFailure = (stderr) =>
1208
+ !!stderr && WSLC_SESSION_FAILURE_RE.test(Buffer.from(stderr).toString());
1209
+
1210
+ /**
1211
+ * Recognise the wslc CLI however it is named, since `DOCKER_CMD` may hold
1212
+ * `wslc`, `wslc.exe`, or a full path such as `C:\Program Files\WSL\wslc.exe`.
1213
+ *
1214
+ * @param {string} [cmd] Container CLI command
1215
+ * @returns {boolean} true when the command invokes wslc
1216
+ */
1217
+ const isWslcCmd = (cmd) => {
1218
+ const cmdName = win32.basename(`${cmd || ""}`).toLowerCase();
1219
+ return cmdName === WSLC_CLI || cmdName === `${WSLC_CLI}.exe`;
1220
+ };
1221
+
1222
+ /**
1223
+ * Print the wslc session-failure hint once per process. Every wslc invocation
1224
+ * fails the same way when the WSL container session cannot start, so repeating
1225
+ * the hint for each image would only add noise.
1226
+ */
1227
+ const printWslcSessionFailureHint = () => {
1228
+ if (!wslcSessionFailureHintShown) {
1229
+ wslcSessionFailureHintShown = true;
1230
+ console.log(WSLC_SESSION_FAILURE_HINT);
1231
+ }
1232
+ };
1233
+
1234
+ /**
1235
+ * Point at wslc when the auto-selected docker CLI could not provide an image on
1236
+ * a Windows host that also has wslc. cdxgen never switches to wslc on its own
1237
+ * here: wslc keeps its own image store, so a silent switch could scan a
1238
+ * registry image instead of a locally built image with the same name.
1239
+ *
1240
+ * @param {string} dockerCmd Container CLI that failed
1241
+ */
1242
+ const suggestWslcAlternative = (dockerCmd) => {
1243
+ if (
1244
+ wslcAlternativeHintShown ||
1245
+ !isWin ||
1246
+ dockerCmd !== "docker" ||
1247
+ readEnvironmentVariable("DOCKER_CMD")?.trim() ||
1248
+ !isWslcInstalled()
1249
+ ) {
1250
+ return;
1251
+ }
1252
+ wslcAlternativeHintShown = true;
1253
+ console.log(WSLC_ALTERNATIVE_HINT);
1254
+ };
1255
+
986
1256
  /**
987
1257
  * Prefer cli on windows, nerdctl on mac, or when using tcp/ssh based host.
1258
+ * On Windows, fall back to wslc (WSL Containers) when the docker CLI is not
1259
+ * installed, so OCI image scans work on hosts that only have WSL.
988
1260
  *
989
- * @returns boolean true if we should use the cli. false otherwise
1261
+ * @returns {string} Container CLI command to invoke
990
1262
  */
991
- const getContainerCliCmd = () => {
992
- if (readEnvironmentVariable("DOCKER_CMD")?.trim()) {
993
- return readEnvironmentVariable("DOCKER_CMD").trim();
1263
+ export const getContainerCliCmd = () => {
1264
+ const configuredCli = readEnvironmentVariable("DOCKER_CMD")?.trim();
1265
+ if (configuredCli) {
1266
+ return configuredCli;
994
1267
  }
995
1268
  detectRancherDesktop() || detectColima();
996
1269
  if (isNerdctl) {
997
1270
  return "nerdctl";
998
1271
  }
1272
+ if (isWin && !isDockerCliInstalled() && isWslcInstalled()) {
1273
+ if (!wslcSelectionRecorded) {
1274
+ wslcSelectionRecorded = true;
1275
+ recordDecisionActivity(`container-cli:${WSLC_CLI}`, {
1276
+ metadata: {
1277
+ decisionType: "container-cli-selection",
1278
+ evaluatedClis: ["docker:not-installed", `${WSLC_CLI}:installed`],
1279
+ selectedCli: WSLC_CLI,
1280
+ },
1281
+ reason:
1282
+ "Selected wslc (WSL Containers) for image operations because the docker CLI is not installed.",
1283
+ });
1284
+ if (DEBUG_MODE) {
1285
+ console.log(
1286
+ "Using wslc (WSL Containers) as the container CLI since docker was not found.",
1287
+ );
1288
+ }
1289
+ }
1290
+ return WSLC_CLI;
1291
+ }
999
1292
  return "docker";
1000
1293
  };
1001
1294
 
@@ -1019,6 +1312,10 @@ const needsCliFallback = () => {
1019
1312
  * Method to get image to the local registry by pulling from the remote if required
1020
1313
  */
1021
1314
  export const getImage = async (fullImageName) => {
1315
+ if (!isSafeImageReference(fullImageName)) {
1316
+ console.log(`${fullImageName} is not a valid container image reference.`);
1317
+ return undefined;
1318
+ }
1022
1319
  let localData;
1023
1320
  let pullData;
1024
1321
  const { registry, repo, tag, digest } = parseImageName(fullImageName);
@@ -1036,50 +1333,62 @@ export const getImage = async (fullImageName) => {
1036
1333
  );
1037
1334
  return undefined;
1038
1335
  }
1336
+ let dockerCmd;
1039
1337
  if (needsCliFallback()) {
1040
- const dockerCmd = getContainerCliCmd();
1041
- let needsPull = true;
1042
- // Let's check the local cache first
1043
- let result = safeSpawnSync(dockerCmd, ["images", "--format=json"]);
1044
- if (result.status === 0 && result.stdout) {
1045
- for (const imgLine of result.stdout.split("\n")) {
1046
- try {
1047
- const imgObj = JSON.parse(Buffer.from(imgLine).toString());
1048
- if (`${imgObj.Repository}:${imgObj.Tag}` === fullImageName) {
1049
- needsPull = false;
1050
- break;
1338
+ dockerCmd = getContainerCliCmd();
1339
+ // The local image is checked with the reference itself. `image inspect`
1340
+ // resolves the reference exactly as the pull below would, so a fully
1341
+ // qualified docker.io/library/... name and a digest reference both find
1342
+ // a local image. The `images` listing compared Repository:Tag strings
1343
+ // and missed both. docker, podman, nerdctl and wslc all implement
1344
+ // `image inspect`.
1345
+ let inspectResult = safeSpawnSync(dockerCmd, [
1346
+ "image",
1347
+ "inspect",
1348
+ fullImageName,
1349
+ ]);
1350
+ if (inspectResult.status !== 0 || inspectResult.error) {
1351
+ const pullResult = safeSpawnSync(dockerCmd, ["pull", fullImageName]);
1352
+ if (pullResult.status !== 0 || pullResult.error) {
1353
+ if (isWslcCmd(dockerCmd)) {
1354
+ // wslc runs Docker Engine inside its own VM, so its errors may
1355
+ // mention a docker daemon; the Docker Desktop and DOCKER_CMD hints
1356
+ // below do not apply. Show wslc's own error instead.
1357
+ if (isWslcSessionFailure(pullResult.stderr)) {
1358
+ printWslcSessionFailureHint();
1359
+ } else if (pullResult.stderr) {
1360
+ console.log(pullResult.stderr);
1051
1361
  }
1052
- } catch (_err) {
1053
- // continue regardless of error
1054
- }
1055
- }
1056
- }
1057
- if (needsPull) {
1058
- result = safeSpawnSync(dockerCmd, ["pull", fullImageName]);
1059
- if (result.status !== 0 || result.error) {
1060
- if (result.stderr?.includes("docker daemon is not running")) {
1362
+ } else if (
1363
+ pullResult.stderr?.includes("docker daemon is not running")
1364
+ ) {
1061
1365
  console.log(
1062
1366
  "Ensure Docker for Desktop is running as an administrator with 'Exposing daemon on TCP without TLS' setting turned on.",
1063
1367
  );
1064
- } else if (result.stderr?.includes("not found")) {
1368
+ } else if (pullResult.stderr?.includes("not found")) {
1065
1369
  console.log(
1066
1370
  "Set the environment variable DOCKER_CMD to use an alternative command such as nerdctl or podman.",
1067
1371
  );
1068
- } else if (result.stderr) {
1069
- console.log(result.stderr);
1372
+ } else if (pullResult.stderr) {
1373
+ console.log(pullResult.stderr);
1070
1374
  }
1071
1375
  }
1376
+ inspectResult = safeSpawnSync(dockerCmd, [
1377
+ "image",
1378
+ "inspect",
1379
+ fullImageName,
1380
+ ]);
1072
1381
  }
1073
- result = safeSpawnSync(dockerCmd, ["inspect", fullImageName]);
1074
- if (result.status !== 0 || result.error) {
1075
- if (result.stderr) {
1076
- console.log(result.stderr);
1382
+ if (inspectResult.status !== 0 || inspectResult.error) {
1383
+ if (inspectResult.stderr) {
1384
+ console.log(inspectResult.stderr);
1077
1385
  }
1386
+ suggestWslcAlternative(dockerCmd);
1078
1387
  // Continue with the daemon client when the CLI fallback is unavailable
1079
1388
  // or unable to inspect the image.
1080
1389
  } else {
1081
1390
  try {
1082
- const stdout = result.stdout;
1391
+ const stdout = inspectResult.stdout;
1083
1392
  if (stdout) {
1084
1393
  const inspectData = JSON.parse(Buffer.from(stdout).toString());
1085
1394
  if (inspectData && Array.isArray(inspectData)) {
@@ -1091,6 +1400,13 @@ export const getImage = async (fullImageName) => {
1091
1400
  // continue regardless of error
1092
1401
  }
1093
1402
  }
1403
+ // wslc has no daemon API, and exportImage saves the image through wslc
1404
+ // again. A Docker daemon could only offer an image that wslc cannot
1405
+ // export, and probing it warns about Docker Desktop on hosts without it.
1406
+ if (isWslcCmd(dockerCmd)) {
1407
+ reportImagePullFailure(fullImageName, dockerCmd);
1408
+ return undefined;
1409
+ }
1094
1410
  }
1095
1411
  try {
1096
1412
  localData = await makeRequest(
@@ -1197,16 +1513,26 @@ export const getImage = async (fullImageName) => {
1197
1513
  }
1198
1514
  }
1199
1515
  if (!localData) {
1200
- console.log(
1201
- `Unable to pull ${fullImageName}. Check if the name is valid. Perform any authentication prior to invoking cdxgen.`,
1202
- );
1203
- console.log(
1204
- `Try manually pulling this image using docker pull ${fullImageName}`,
1205
- );
1516
+ reportImagePullFailure(fullImageName, dockerCmd);
1206
1517
  }
1207
1518
  return localData;
1208
1519
  };
1209
1520
 
1521
+ /**
1522
+ * Explain that an image could not be pulled and how to pull it manually.
1523
+ *
1524
+ * @param {string} fullImageName Image that could not be pulled
1525
+ * @param {string} [dockerCmd] Container CLI used for the attempt, if any
1526
+ */
1527
+ const reportImagePullFailure = (fullImageName, dockerCmd) => {
1528
+ console.log(
1529
+ `Unable to pull ${fullImageName}. Check if the name is valid. Perform any authentication prior to invoking cdxgen.`,
1530
+ );
1531
+ console.log(
1532
+ `Try manually pulling this image using ${dockerCmd || (isPodman ? "podman" : "docker")} pull ${fullImageName}`,
1533
+ );
1534
+ };
1535
+
1210
1536
  /**
1211
1537
  * @typedef {{ path: string }} TarReadEntryLike
1212
1538
  */
@@ -1619,6 +1945,55 @@ export const exportArchive = async (fullImageName, options = {}) => {
1619
1945
  return undefined;
1620
1946
  };
1621
1947
 
1948
+ /**
1949
+ * Check that a path taken from an untrusted image archive (manifest layer or
1950
+ * config reference, or a config WorkingDir) stays inside the directory it is
1951
+ * resolved against. Values straight out of manifest.json otherwise traverse
1952
+ * out of the extraction temp dir and read host files into the SBOM.
1953
+ *
1954
+ * @param {string} baseDir Directory the value must resolve within
1955
+ * @param {string} candidatePath Raw value from the archive
1956
+ * @returns {boolean} true when the resolved path is inside (or equal to) baseDir
1957
+ */
1958
+ const isPathWithinDir = (baseDir, candidatePath) => {
1959
+ if (typeof candidatePath !== "string" || !candidatePath) {
1960
+ return false;
1961
+ }
1962
+ const relativePath = relative(
1963
+ resolve(baseDir),
1964
+ resolve(baseDir, candidatePath),
1965
+ ).replaceAll("\\", "/");
1966
+ return (
1967
+ relativePath === "" ||
1968
+ (!relativePath.startsWith("..") && !relativePath.split("/").includes(".."))
1969
+ );
1970
+ };
1971
+
1972
+ /**
1973
+ * Resolve an untrusted archive reference (manifest Layers/Config entry) against
1974
+ * the extraction directory, rejecting anything that escapes it.
1975
+ *
1976
+ * @param {string} tempDir Extraction directory holding the archive contents
1977
+ * @param {string} archiveRef Raw reference string from the archive manifest
1978
+ * @param {string} kind Label used in the skip message
1979
+ * @returns {string|undefined} Contained joined path, or undefined when the
1980
+ * reference escapes tempDir
1981
+ */
1982
+ const resolveContainedArchivePath = (tempDir, archiveRef, kind) => {
1983
+ if (
1984
+ typeof archiveRef !== "string" ||
1985
+ !archiveRef ||
1986
+ isAbsolute(archiveRef) ||
1987
+ !isPathWithinDir(tempDir, archiveRef)
1988
+ ) {
1989
+ console.warn(
1990
+ `Skipping ${kind} '${archiveRef}' from the image manifest: it resolves outside the extracted archive.`,
1991
+ );
1992
+ return undefined;
1993
+ }
1994
+ return join(tempDir, archiveRef);
1995
+ };
1996
+
1622
1997
  /**
1623
1998
  * Parse a Docker/containerd manifest file and extract all image layers into a
1624
1999
  * single merged directory. Resolves the last layer's config to determine the
@@ -1676,8 +2051,16 @@ export const extractFromManifest = async (
1676
2051
  layerPhase.progress(extractedLayers, layers.length);
1677
2052
  layerPhase.detail(basename(layer));
1678
2053
  extractedLayers += 1;
2054
+ const containedLayerPath = resolveContainedArchivePath(
2055
+ tempDir,
2056
+ layer,
2057
+ "layer",
2058
+ );
2059
+ if (!containedLayerPath) {
2060
+ continue;
2061
+ }
1679
2062
  try {
1680
- if (!lstatSync(join(tempDir, layer)).isFile()) {
2063
+ if (!lstatSync(containedLayerPath).isFile()) {
1681
2064
  console.log(
1682
2065
  `Skipping layer ${layer} since it is not a readable file.`,
1683
2066
  );
@@ -1691,7 +2074,7 @@ export const extractFromManifest = async (
1691
2074
  console.log(`Extracting layer ${layer} to ${allLayersExplodedDir}`);
1692
2075
  }
1693
2076
  try {
1694
- await extractTar(join(tempDir, layer), allLayersExplodedDir, options);
2077
+ await extractTar(containedLayerPath, allLayersExplodedDir, options);
1695
2078
  } catch (err) {
1696
2079
  if (err.code === "TAR_BAD_ARCHIVE") {
1697
2080
  if (DEBUG_MODE) {
@@ -1711,13 +2094,22 @@ export const extractFromManifest = async (
1711
2094
  `${layers.length} layer${layers.length === 1 ? "" : "s"}`,
1712
2095
  );
1713
2096
  if (selectedManifest?.Config) {
1714
- lastLayerConfigFile = join(tempDir, selectedManifest.Config);
2097
+ lastLayerConfigFile =
2098
+ resolveContainedArchivePath(
2099
+ tempDir,
2100
+ selectedManifest.Config,
2101
+ "config",
2102
+ ) || "";
1715
2103
  }
1716
2104
  if (!lastLayerConfigFile && lastLayer?.includes("layer.tar")) {
1717
- lastLayerConfigFile = join(
2105
+ const lastLayerPath = resolveContainedArchivePath(
1718
2106
  tempDir,
1719
- lastLayer.replace("layer.tar", "json"),
2107
+ lastLayer,
2108
+ "layer",
1720
2109
  );
2110
+ if (lastLayerPath) {
2111
+ lastLayerConfigFile = lastLayerPath.replace("layer.tar", "json");
2112
+ }
1721
2113
  }
1722
2114
  if (lastLayerConfigFile && safeExistsSync(lastLayerConfigFile)) {
1723
2115
  try {
@@ -1726,9 +2118,31 @@ export const extractFromManifest = async (
1726
2118
  encoding: "utf-8",
1727
2119
  }),
1728
2120
  );
1729
- lastWorkingDir = lastLayerConfig.config?.WorkingDir
1730
- ? join(allLayersExplodedDir, lastLayerConfig.config.WorkingDir)
2121
+ const workingDir = lastLayerConfig.config?.WorkingDir;
2122
+ // WorkingDir is an in-image path (usually absolute such as /app), so
2123
+ // join first to mirror the extraction layout, then keep it only when
2124
+ // the result stays inside the merged layers directory.
2125
+ const candidateWorkingDir = workingDir
2126
+ ? join(allLayersExplodedDir, workingDir)
1731
2127
  : "";
2128
+ const workingDirRelative = candidateWorkingDir
2129
+ ? relative(
2130
+ resolve(allLayersExplodedDir),
2131
+ resolve(candidateWorkingDir),
2132
+ ).replaceAll("\\", "/")
2133
+ : "..";
2134
+ lastWorkingDir =
2135
+ candidateWorkingDir &&
2136
+ (workingDirRelative === "" ||
2137
+ (!workingDirRelative.startsWith("..") &&
2138
+ !workingDirRelative.split("/").includes("..")))
2139
+ ? candidateWorkingDir
2140
+ : "";
2141
+ if (candidateWorkingDir && !lastWorkingDir) {
2142
+ console.warn(
2143
+ `Ignoring image WorkingDir '${workingDir}': it resolves outside the extracted layers.`,
2144
+ );
2145
+ }
1732
2146
  } catch (_err) {
1733
2147
  deferFailOnError(options, {
1734
2148
  ecosystem: "container",
@@ -1773,6 +2187,10 @@ export const exportImage = async (fullImageName, options) => {
1773
2187
  ) {
1774
2188
  return undefined;
1775
2189
  }
2190
+ if (!isSafeImageReference(fullImageName)) {
2191
+ console.log(`${fullImageName} is not a valid container image reference.`);
2192
+ return undefined;
2193
+ }
1776
2194
  if (isDryRun) {
1777
2195
  const imageDetails = parseImageName(fullImageName);
1778
2196
  const requestedRegistryRef = imageDetails.registry
@@ -1819,6 +2237,9 @@ export const exportImage = async (fullImageName, options) => {
1819
2237
  fullImageName,
1820
2238
  ]);
1821
2239
  if (result.status !== 0 || result.error) {
2240
+ if (isWslcCmd(dockerCmd) && isWslcSessionFailure(result.stderr)) {
2241
+ printWslcSessionFailureHint();
2242
+ }
1822
2243
  if (result.stdout || result.stderr) {
1823
2244
  console.log(result.stdout, result.stderr);
1824
2245
  }