diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7f1e43ed..e9aa0239 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -3,6 +3,7 @@ - [ ] Followed the steps in the contributor's guide: https://crashoverride.com/docs/other/contributing#filing-the-pull-request - [ ] PR title uses [semantic commit messages](https://nitayneeman.com/posts/understanding-semantic-commit-messages-using-git-and-angular/#fix) - [ ] Filled out the template to a useful degree +- [ ] Updated `CHANGELOG.md` if necessary ## Issue diff --git a/CHANGELOG.md b/CHANGELOG.md index 216b2259..4e094637 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ `AWS_DEFAULT_REGION` environment variables were not set [#246](https://github.com/crashappsec/chalk/pull/246) +### Fixes + +- The Docker codec is now bypassed when `docker` is not + installed. Previously, any chalk sub-scan such as + during `chalk exec` had misleading error logs. + [#248](https://github.com/crashappsec/chalk/pull/248) + ## 0.3.4 **Mar 18, 2024** diff --git a/src/chalk_common.nim b/src/chalk_common.nim index 3ce977ca..2fb38d1a 100644 --- a/src/chalk_common.nim +++ b/src/chalk_common.nim @@ -84,6 +84,7 @@ type c: Option[string]) {.cdecl.} Plugin* = ref object name*: string + enabled*: bool configInfo*: PluginSpec getChalkTimeHostInfo*: ChalkTimeHostCb getChalkTimeArtifactInfo*: ChalkTimeArtifactCb @@ -412,7 +413,6 @@ var chalkConfig*: ChalkConfig con4mRuntime*: ConfigStack commandName*: string - dockerExeLocation*: string = "" gitExeLocation*: string = "" sshKeyscanExeLocation*: string = "" diff --git a/src/commands/cmd_docker.nim b/src/commands/cmd_docker.nim index 7f03c2dd..b53b5d9f 100644 --- a/src/commands/cmd_docker.nim +++ b/src/commands/cmd_docker.nim @@ -44,8 +44,9 @@ proc runMungedDockerInvocation(ctx: DockerInvocation): int = var newStdin = "" # Indicated passthrough. args = ctx.newCmdLine + exe = getDockerExeLocation() - trace("Running docker: " & dockerExeLocation & " " & args.join(" ")) + trace("Running docker: " & exe & " " & args.join(" ")) if ctx.dfPassOnStdin: if not ctx.inDockerFile.endswith("\n"): @@ -53,7 +54,7 @@ proc runMungedDockerInvocation(ctx: DockerInvocation): int = newStdin = ctx.addInstructions(ctx.inDockerFile) trace("Passing on stdin: \n" & newStdin) - result = runCmdNoOutputCapture(dockerExeLocation, args, newStdin) + result = runCmdNoOutputCapture(exe, args, newStdin) proc doReporting*(topic: string){.importc.} @@ -508,6 +509,7 @@ proc runBuild(ctx: DockerInvocation): int = chalk.marked = true proc runPush(ctx: DockerInvocation): int = + let exe = getDockerExeLocation() if ctx.cmdBuild: var tags = ctx.foundTags if len(tags) == 0: @@ -516,7 +518,7 @@ proc runPush(ctx: DockerInvocation): int = for tag in tags: trace("docker pushing: " & tag) ctx.newCmdLine = @["push", tag] - result = runCmdNoOutputCapture(dockerExeLocation, ctx.newCmdLine) + result = runCmdNoOutputCapture(exe, ctx.newCmdLine) if result != 0: break @@ -533,9 +535,10 @@ proc runPush(ctx: DockerInvocation): int = # Here, if we fail, there's no re-run. # We ran their original command line so there is nothing to fall back on. - return runCmdNoOutputCapture(dockerExeLocation, ctx.newCmdLine) + return runCmdNoOutputCapture(exe, ctx.newCmdLine) proc createAndPushManifest(ctx: DockerInvocation, platforms: seq[string]): int = + let exe = getDockerExeLocation() # not a multi-platform build so manifest should not be used if len(platforms) < 2: return 0 @@ -546,7 +549,7 @@ proc createAndPushManifest(ctx: DockerInvocation, platforms: seq[string]): int = platformTags.add(ctx.getTagForPlatform(tag, platform)) let exitCode = runCmdNoOutputCapture( - dockerExeLocation, + exe, @["buildx", "imagetools", "create", "-t"] & platformTags, ) if exitCode != 0: @@ -561,9 +564,10 @@ proc createAndPushManifest(ctx: DockerInvocation, platforms: seq[string]): int = # TODO: Any other noteworthy commands to wrap (run, etc) template passThroughLogic() = + let exe = getDockerExeLocation() try: # Silently pass through other docker commands right now. - exitCode = runCmdNoOutputCapture(dockerExeLocation, args) + exitCode = runCmdNoOutputCapture(exe, args) if chalkConfig.dockerConfig.getReportUnwrappedCommands(): reporting.doReporting("report") except: @@ -707,14 +711,16 @@ template postDockerActivity() = exitCode = 0 proc runCmdDocker*(args: seq[string]) = - setDockerExeLocation() - var exitCode = 0 ctx = args.processDockerCmdLine() ctx.originalArgs = args + if getDockerExeLocation() == "": + error("docker command is missing. chalk requires docker binary installed to wrap docker commands.") + ctx.dockerFailSafe() + if ctx.cmdBuild: # Build with --push is still a build operation. setCommandName("build") diff --git a/src/commands/cmd_extract.nim b/src/commands/cmd_extract.nim index 06d5e82e..2f2e5285 100644 --- a/src/commands/cmd_extract.nim +++ b/src/commands/cmd_extract.nim @@ -7,8 +7,7 @@ ## The `chalk extract` command. -import ".."/[config, collect, reporting, plugins/codecDocker, plugin_api, - docker_base] +import ".."/[config, collect, reporting, plugins/codecDocker, plugin_api] template processDockerChalkList(chalkList: seq[ChalkObj]) = for item in chalkList: @@ -57,26 +56,22 @@ template coreExtractContainers() = proc runCmdExtract*(path: seq[string]) {.exportc,cdecl.} = - setDockerExeLocation() setContextDirectories(path) initCollection() coreExtractFiles(path) doReporting() proc runCmdExtractImages*() = - setDockerExeLocation() initCollection() coreExtractImages() doReporting() proc runCmdExtractContainers*() = - setDockerExeLocation() initCollection() coreExtractContainers() doReporting() proc runCmdExtractAll*(path: seq[string]) = - setDockerExeLocation() setContextDirectories(path) initCollection() coreExtractFiles(path) diff --git a/src/docker_base.nim b/src/docker_base.nim index 5bc7caad..14ac6b7f 100644 --- a/src/docker_base.nim +++ b/src/docker_base.nim @@ -12,14 +12,13 @@ import std/[httpclient, uri] import "."/[config, dockerfile, util, reporting, semver, www_authenticate] var - buildXVersion: Version = parseVersion("0") - dockerVersion: Version = parseVersion("0") + buildXVersion = parseVersion("0") + dockerVersion = parseVersion("0") + dockerExeLocation = "" const hashHeader* = "sha256:" -var dockerPathOpt: Option[string] = none(string) - template extractDockerHash*(value: string): string = if not value.startsWith(hashHeader): value @@ -29,24 +28,25 @@ template extractDockerHash*(value: string): string = template extractBoxedDockerHash*(value: Box): Box = pack(extractDockerHash(unpack[string](value))) -proc setDockerExeLocation*() = +proc getDockerExeLocation*(): string = once: - trace("Searching PATH for 'docker'") let dockerConfigPath = chalkConfig.getDockerExe() dockerExeOpt = findExePath("docker", - configPath = dockerConfigPath, + configPath = dockerConfigPath, ignoreChalkExes = true) dockerExeLocation = dockerExeOpt.get("") if dockerExeLocation == "": warn("No docker command found in PATH. `chalk docker` not available.") + return dockerExeLocation proc runDockerGetEverything*(args: seq[string], stdin = "", silent = true): ExecOutput = + let exe = getDockerExeLocation() if not silent: - trace("Running docker: " & dockerExeLocation & " " & args.join(" ")) + trace("Running docker: " & exe & " " & args.join(" ")) if stdin != "": trace("Passing on stdin: \n" & stdin) - result = runCmdGetEverything(dockerExeLocation, args, stdin) + result = runCmdGetEverything(exe, args, stdin) if not silent and result.exitCode > 0: trace(strutils.strip(result.stderr & result.stdout)) return result @@ -121,9 +121,14 @@ proc dockerFailsafe*(info: DockerInvocation) {.cdecl, exportc.} = if info.dockerFileLoc == ":stdin:": newStdin = info.inDockerFile - let exitCode = runCmdNoOutputCapture(dockerExeLocation, - info.originalArgs, - newStdin) + let + exe = getDockerExeLocation() + # even if docker is not found call subprocess with valid command name + # so that we can bubble up error from subprocess + docker = if exe != "": exe else: "docker" + exitCode = runCmdNoOutputCapture(docker, + info.originalArgs, + newStdin) doReporting("fail") quitChalk(exitCode) diff --git a/src/plugin_api.nim b/src/plugin_api.nim index 6208a8ea..f90b5e7a 100644 --- a/src/plugin_api.nim +++ b/src/plugin_api.nim @@ -20,6 +20,9 @@ import "."/[config, chalkjson, util] # should all be pre-checked. proc callGetChalkTimeHostInfo*(plugin: Plugin): ChalkDict = + if not plugin.enabled: + return ChalkDict() + let cb = plugin.getChalkTimeHostInfo # explicit callback check - otherwise it results in segfault @@ -31,6 +34,9 @@ proc callGetChalkTimeHostInfo*(plugin: Plugin): ChalkDict = proc callGetChalkTimeArtifactInfo*(plugin: Plugin, obj: ChalkObj): ChalkDict = + if not plugin.enabled: + return ChalkDict() + let cb = plugin.getChalkTimeArtifactInfo # explicit callback check - otherwise it results in segfault @@ -42,6 +48,9 @@ proc callGetChalkTimeArtifactInfo*(plugin: Plugin, obj: ChalkObj): proc callGetRunTimeArtifactInfo*(plugin: Plugin, obj: ChalkObj, b: bool): ChalkDict = + if not plugin.enabled: + return ChalkDict() + let cb = plugin.getRunTimeArtifactInfo # explicit callback check - otherwise it results in segfault @@ -53,6 +62,9 @@ proc callGetRunTimeArtifactInfo*(plugin: Plugin, obj: ChalkObj, b: bool): proc callGetRunTimeHostInfo*(plugin: Plugin, objs: seq[ChalkObj]): ChalkDict = + if not plugin.enabled: + return ChalkDict() + let cb = plugin.getRunTimeHostInfo # explicit callback check - otherwise it results in segfault @@ -594,7 +606,8 @@ proc newPlugin*( getChalkTimeArtifactInfo: ctArtCallback, getRunTimeArtifactInfo: rtArtCallback, getRunTimeHostInfo: rtHostCallback, - internalState: cache) + internalState: cache, + enabled: true) if not result.checkPlugin(codec = false): result = Plugin(nil) @@ -612,7 +625,8 @@ proc newCodec*( handleWrite: HandleWriteCb = HandleWritecb(defaultCodecWrite), nativeObjPlatforms: seq[string] = @[], cache: RootRef = RootRef(nil), - commentStart: string = "#"): + commentStart: string = "#", + enabled: bool = true): Plugin {.discardable, cdecl.} = result = Plugin(name: name, @@ -627,7 +641,8 @@ proc newCodec*( handleWrite: handleWrite, nativeObjPlatforms: nativeObjPlatforms, internalState: cache, - commentStart: commentStart) + commentStart: commentStart, + enabled: enabled) if not result.checkPlugin(codec = true): result = Plugin(nil) diff --git a/src/plugins/codecDocker.nim b/src/plugins/codecDocker.nim index 0e4a6771..017dc1c7 100644 --- a/src/plugins/codecDocker.nim +++ b/src/plugins/codecDocker.nim @@ -6,9 +6,7 @@ ## import ".."/[config, docker_base, chalkjson, attestation_api, plugin_api, util] -const - markFile = "chalk.json" - markLocation = "/chalk.json" +const markLocation = "/chalk.json" proc dockerGetChalkId*(self: Plugin, chalk: ChalkObj): string {.cdecl.} = if chalk.extract != nil and "CHALK_ID" in chalk.extract: @@ -530,7 +528,7 @@ proc inspectContainer(chalk: ChalkObj) = cmdOut = runDockerGetEverything(@["container", "inspect", id]) if cmdout.getExit() != 0: - warn(chalk.userRef & ": Container inspection failed (no longer running?)") + warn(chalk.userRef & ": Could not perform container inspection (no longer running?)") return let @@ -601,6 +599,12 @@ proc dockerExtractChalkMark*(chalk: ChalkObj): ChalkDict {.exportc, cdecl.} = addUnmarked(chalk.name) proc loadCodecDocker*() = + # cant use getDockerExePath as that uses codecs to ignore chalk + # wrappings hence we just check if anything docker is on PATH here + let enabled = nimutils.findExePath("docker") != "" + if not enabled: + warn("Disabling docker codec as docker command is not available") newCodec("docker", rtArtCallback = RunTimeArtifactCb(dockerGetRunTimeArtifactInfo), - getChalkId = ChalkIdCb(dockerGetChalkId)) + getChalkId = ChalkIdCb(dockerGetChalkId), + enabled = enabled) diff --git a/src/util.nim b/src/util.nim index 044b23f6..9c7ea1fd 100644 --- a/src/util.nim +++ b/src/util.nim @@ -363,7 +363,7 @@ proc findExePath*(cmdName: string, # takes precedence over rest of dirs in PATH paths = @[configPath.get()] & paths - trace("Searching path for " & cmdName) + trace("Searching PATH for " & cmdName) var foundExes = findAllExePaths(cmdName, paths, usePath) if ignoreChalkExes: @@ -386,10 +386,10 @@ proc findExePath*(cmdName: string, foundExes = newExes if foundExes.len() == 0: - trace("Could not find '" & cmdName & "' in path.") + trace("Could not find '" & cmdName & "' in PATH.") return none(string) - trace("Found '" & cmdName & "' in path: " & foundExes[0]) + trace("Found '" & cmdName & "' in PATH: " & foundExes[0]) return some(foundExes[0]) proc handleExec*(prioritizedExes: seq[string], args: seq[string]) {.noreturn.} = diff --git a/tests/Dockerfile b/tests/Dockerfile index ab91141e..696ef4f1 100644 --- a/tests/Dockerfile +++ b/tests/Dockerfile @@ -51,7 +51,7 @@ RUN mkdir -p /.cache/pypoetry \ COPY pyproject.toml poetry.lock $WORKDIR/ RUN poetry install --no-plugins -COPY --from=gcr.io/projectsigstore/cosign /ko-app/cosign /usr/local/bin/cosign +COPY --from=gcr.io/projectsigstore/cosign:v2.2.3 /ko-app/cosign /usr/local/bin/cosign COPY --from=docker:24 /usr/local/bin/docker /usr/local/bin/docker COPY --from=docker/buildx-bin:0.11.2 /buildx /usr/lib/docker/cli-plugins/docker-buildx