@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.
- package/README.md +33 -14
- package/bin/cdxgen.js +310 -138
- package/bin/evinse.js +22 -0
- package/bin/repl.js +86 -0
- package/bin/sign.js +119 -15
- package/bin/tracebom.js +69 -5
- package/bin/validate.js +107 -19
- package/bin/verify.js +172 -63
- package/data/helpers/init.gradle +46 -21
- package/data/remediations.json +85 -0
- package/data/rules/dependency-sources.yaml +67 -0
- package/data/rules/scala-evidence.yaml +115 -0
- package/lib/audit/index.js +38 -6
- package/lib/cli/bomAssembly.js +105 -15
- package/lib/cli/cliOptions.js +93 -1
- package/lib/cli/index.js +156 -58
- package/lib/cli/jsBom.js +99 -53
- package/lib/cli/jvmBom.js +708 -147
- package/lib/cli/managedBom.js +415 -224
- package/lib/cli/nativeBom.js +264 -44
- package/lib/core/activity.js +12 -0
- package/lib/core/env.js +22 -15
- package/lib/core/fs.js +176 -7
- package/lib/core/httpClient.js +159 -24
- package/lib/core/metadataProperties.js +97 -0
- package/lib/core/pluginRun.js +184 -0
- package/lib/core/processTree.js +100 -0
- package/lib/core/projectConfig.js +31 -4
- package/lib/core/runState.js +39 -0
- package/lib/core/toml.js +72 -0
- package/lib/ecosystems/asarutils.js +4 -2
- package/lib/ecosystems/bunutils.js +274 -108
- package/lib/ecosystems/cargoCache.js +231 -8
- package/lib/ecosystems/caxa.js +434 -2
- package/lib/ecosystems/cmakeResolver.js +76 -21
- package/lib/ecosystems/cppBuildContext.js +631 -0
- package/lib/ecosystems/cppEvidence.js +399 -51
- package/lib/ecosystems/cppIncludeResolver.js +294 -0
- package/lib/ecosystems/denoutils.js +93 -66
- package/lib/ecosystems/ecosystems.js +1983 -793
- package/lib/ecosystems/golem.js +28 -12
- package/lib/ecosystems/gradleutils.js +356 -37
- package/lib/ecosystems/jsEvidence.js +104 -44
- package/lib/ecosystems/kosi.js +31 -7
- package/lib/ecosystems/npmutils.js +174 -9
- package/lib/ecosystems/parsers-dotnet.js +616 -159
- package/lib/ecosystems/parsers-elm.js +46 -28
- package/lib/ecosystems/parsers-gleam.js +3 -4
- package/lib/ecosystems/parsers-go.js +371 -70
- package/lib/ecosystems/parsers-js.js +263 -76
- package/lib/ecosystems/parsers-julia.js +2 -3
- package/lib/ecosystems/parsers-jvm.js +204 -12
- package/lib/ecosystems/parsers-misc.js +929 -216
- package/lib/ecosystems/parsers-python.js +167 -24
- package/lib/ecosystems/parsers-rust.js +28 -22
- package/lib/ecosystems/parsers-terraform.js +1573 -19
- package/lib/ecosystems/pythonutils.js +102 -6
- package/lib/ecosystems/remote/huggingface.js +3 -0
- package/lib/ecosystems/rubyutils.js +1492 -182
- package/lib/ecosystems/rusi.js +54 -10
- package/lib/ecosystems/sbtutils.js +243 -187
- package/lib/ecosystems/scalacliutils.js +342 -0
- package/lib/ecosystems/scalasem.js +2618 -0
- package/lib/ecosystems/terraformRegistry.js +288 -0
- package/lib/ecosystems/utils.js +2 -0
- package/lib/ecosystems/vendoredCode.js +156 -0
- package/lib/evinser/evinser.js +1008 -191
- package/lib/evinser/swiftsem.js +1914 -302
- package/lib/helpers/agenticToolutils.js +2 -2
- package/lib/helpers/bomSigner.js +1101 -222
- package/lib/inventory/aiCollector.js +4 -1
- package/lib/inventory/analyzer.js +110 -127
- package/lib/inventory/atomUtils.js +703 -43
- package/lib/inventory/auditCategories.js +7 -0
- package/lib/inventory/buildToolRateLimit.js +144 -0
- package/lib/inventory/cacheCommands.js +2 -1
- package/lib/inventory/cdxrs.js +103 -12
- package/lib/inventory/cmakeBuildDirs.js +254 -0
- package/lib/inventory/deps.js +501 -124
- package/lib/inventory/depsUtils.js +183 -11
- package/lib/inventory/display.js +4 -5
- package/lib/inventory/dosai.js +798 -57
- package/lib/inventory/dosaiParsers.js +310 -15
- package/lib/inventory/dynamic.js +90 -6
- package/lib/inventory/evidenceUtils.js +166 -113
- package/lib/inventory/fetchBatch.js +690 -55
- package/lib/inventory/fetchDiskCache.js +597 -0
- package/lib/inventory/forgeEnricher.js +14 -5
- package/lib/inventory/jvmLocalRepos.js +917 -0
- package/lib/inventory/mcpConfigParser.js +2 -3
- package/lib/inventory/parentSelection.js +146 -0
- package/lib/inventory/protobom.js +8 -1
- package/lib/inventory/purl.js +152 -3
- package/lib/inventory/rustFormulationParser.js +3 -4
- package/lib/inventory/sbomDocument.js +2 -70
- package/lib/inventory/scalaCoords.js +153 -0
- package/lib/inventory/source.js +45 -27
- package/lib/inventory/spdx.js +101 -0
- package/lib/inventory/traceRunner.js +295 -25
- package/lib/inventory/tracedCrypto.js +294 -0
- package/lib/managers/binary.js +115 -24
- package/lib/managers/docker.js +467 -46
- package/lib/managers/oci.js +63 -3
- package/lib/parsers/cmakeCache.js +69 -0
- package/lib/parsers/cmakeLists.js +466 -0
- package/lib/parsers/cmakePresets.js +400 -0
- package/lib/parsers/compileCommands.js +557 -0
- package/lib/parsers/largeJson.js +454 -0
- package/lib/parsers/npmrc.js +19 -1
- package/lib/parsers/strictJson.js +322 -0
- package/lib/parsers/terraformConfig.js +1073 -0
- package/lib/parsers/terraformSource.js +652 -0
- package/lib/server/server.js +23 -0
- package/lib/stages/postgen/introspection/markers.js +1 -1
- package/lib/stages/postgen/postgen.js +83 -16
- package/lib/stages/postgen/specVersionCompat.js +32 -0
- package/lib/validator/bomValidator.js +6 -16
- package/lib/validator/index.js +64 -11
- package/lib/validator/reporters/console.js +3 -0
- package/lib/validator/reporters/json.js +2 -0
- package/lib/validator/reporters/sarif.js +7 -0
- package/package.json +23 -23
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts +2 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +33 -1
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts +24 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts +7 -4
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts +3 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/activity.d.ts.map +1 -1
- package/types/lib/core/env.d.ts +9 -0
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +55 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/httpClient.d.ts +13 -8
- package/types/lib/core/httpClient.d.ts.map +1 -1
- package/types/lib/core/metadataProperties.d.ts +39 -0
- package/types/lib/core/metadataProperties.d.ts.map +1 -0
- package/types/lib/core/pluginRun.d.ts +97 -0
- package/types/lib/core/pluginRun.d.ts.map +1 -0
- package/types/lib/core/processTree.d.ts +36 -0
- package/types/lib/core/processTree.d.ts.map +1 -0
- package/types/lib/core/projectConfig.d.ts +6 -2
- package/types/lib/core/projectConfig.d.ts.map +1 -1
- package/types/lib/core/runState.d.ts +29 -0
- package/types/lib/core/runState.d.ts.map +1 -0
- package/types/lib/core/toml.d.ts +20 -0
- package/types/lib/core/toml.d.ts.map +1 -0
- package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
- package/types/lib/ecosystems/bunutils.d.ts +12 -5
- package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
- package/types/lib/ecosystems/cargoCache.d.ts +12 -0
- package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
- package/types/lib/ecosystems/caxa.d.ts +14 -0
- package/types/lib/ecosystems/caxa.d.ts.map +1 -1
- package/types/lib/ecosystems/cmakeResolver.d.ts +28 -3
- package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
- package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
- package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
- package/types/lib/ecosystems/cppEvidence.d.ts +17 -1
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
- package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
- package/types/lib/ecosystems/denoutils.d.ts +9 -3
- package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts +62 -13
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +70 -4
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
- package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts +13 -0
- package/types/lib/ecosystems/kosi.d.ts.map +1 -1
- package/types/lib/ecosystems/npmutils.d.ts +57 -0
- package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-dotnet.d.ts +118 -4
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-go.d.ts +54 -2
- package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts +16 -5
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts +46 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +70 -4
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts +53 -3
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
- package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +27 -2
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts +164 -3
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts +13 -0
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/ecosystems/sbtutils.d.ts +88 -2
- package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
- package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
- package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
- package/types/lib/ecosystems/scalasem.d.ts +311 -0
- package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
- package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
- package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
- package/types/lib/ecosystems/utils.d.ts +2 -2
- package/types/lib/ecosystems/utils.d.ts.map +1 -1
- package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
- package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
- package/types/lib/evinser/evinser.d.ts +132 -15
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/swiftsem.d.ts +280 -15
- package/types/lib/evinser/swiftsem.d.ts.map +1 -1
- package/types/lib/helpers/bomSigner.d.ts +182 -13
- package/types/lib/helpers/bomSigner.d.ts.map +1 -1
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/analyzer.d.ts.map +1 -1
- package/types/lib/inventory/atomUtils.d.ts +219 -5
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +3 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
- package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
- package/types/lib/inventory/cacheCommands.d.ts +2 -1
- package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
- package/types/lib/inventory/cdxrs.d.ts +8 -3
- package/types/lib/inventory/cdxrs.d.ts.map +1 -1
- package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
- package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
- package/types/lib/inventory/deps.d.ts +59 -1
- package/types/lib/inventory/deps.d.ts.map +1 -1
- package/types/lib/inventory/depsUtils.d.ts +15 -0
- package/types/lib/inventory/depsUtils.d.ts.map +1 -1
- package/types/lib/inventory/display.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +121 -9
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dosaiParsers.d.ts +53 -7
- package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
- package/types/lib/inventory/dynamic.d.ts +10 -0
- package/types/lib/inventory/dynamic.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts +19 -4
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +83 -14
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
- package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
- package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
- package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
- package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/parentSelection.d.ts +34 -0
- package/types/lib/inventory/parentSelection.d.ts.map +1 -0
- package/types/lib/inventory/plugins.d.ts.map +1 -1
- package/types/lib/inventory/protobom.d.ts.map +1 -1
- package/types/lib/inventory/purl.d.ts +71 -2
- package/types/lib/inventory/purl.d.ts.map +1 -1
- package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
- package/types/lib/inventory/scalaCoords.d.ts +80 -0
- package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/spdx.d.ts +9 -0
- package/types/lib/inventory/spdx.d.ts.map +1 -1
- package/types/lib/inventory/traceRunner.d.ts +59 -1
- package/types/lib/inventory/traceRunner.d.ts.map +1 -1
- package/types/lib/inventory/tracedCrypto.d.ts +42 -0
- package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
- package/types/lib/managers/binary.d.ts +30 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +29 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts +17 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/parsers/cmakeCache.d.ts +16 -0
- package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
- package/types/lib/parsers/cmakeLists.d.ts +77 -0
- package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
- package/types/lib/parsers/cmakePresets.d.ts +78 -0
- package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
- package/types/lib/parsers/compileCommands.d.ts +115 -0
- package/types/lib/parsers/compileCommands.d.ts.map +1 -0
- package/types/lib/parsers/largeJson.d.ts +52 -0
- package/types/lib/parsers/largeJson.d.ts.map +1 -0
- package/types/lib/parsers/npmrc.d.ts +5 -0
- package/types/lib/parsers/npmrc.d.ts.map +1 -1
- package/types/lib/parsers/strictJson.d.ts +46 -0
- package/types/lib/parsers/strictJson.d.ts.map +1 -0
- package/types/lib/parsers/terraformConfig.d.ts +70 -0
- package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
- package/types/lib/parsers/terraformSource.d.ts +66 -0
- package/types/lib/parsers/terraformSource.d.ts.map +1 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/index.d.ts +18 -1
- package/types/lib/validator/index.d.ts.map +1 -1
- package/types/lib/validator/reporters/console.d.ts.map +1 -1
- package/types/lib/validator/reporters/json.d.ts.map +1 -1
- package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
- package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
package/lib/managers/docker.js
CHANGED
|
@@ -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 {
|
|
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
|
|
1261
|
+
* @returns {string} Container CLI command to invoke
|
|
990
1262
|
*/
|
|
991
|
-
const getContainerCliCmd = () => {
|
|
992
|
-
|
|
993
|
-
|
|
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
|
-
|
|
1041
|
-
|
|
1042
|
-
//
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
-
}
|
|
1053
|
-
|
|
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 (
|
|
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 (
|
|
1069
|
-
console.log(
|
|
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
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
2097
|
+
lastLayerConfigFile =
|
|
2098
|
+
resolveContainedArchivePath(
|
|
2099
|
+
tempDir,
|
|
2100
|
+
selectedManifest.Config,
|
|
2101
|
+
"config",
|
|
2102
|
+
) || "";
|
|
1715
2103
|
}
|
|
1716
2104
|
if (!lastLayerConfigFile && lastLayer?.includes("layer.tar")) {
|
|
1717
|
-
|
|
2105
|
+
const lastLayerPath = resolveContainedArchivePath(
|
|
1718
2106
|
tempDir,
|
|
1719
|
-
lastLayer
|
|
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
|
-
|
|
1730
|
-
|
|
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
|
}
|