From f166d3e57c972e6ca2c05739029343a41ebd2788 Mon Sep 17 00:00:00 2001 From: Valera Satsura Date: Tue, 14 Jul 2026 08:34:23 +0300 Subject: [PATCH] Stop checkov from writing github_conf into the scanned repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The github_configuration framework dumps branch_protection_rules.json into the working directory. The container runs as root over a mounted workspace, so the file outlives the scan owned by root and the next actions/checkout — running as the runner's user — cannot delete it and fails every workflow on that runner. Skip the framework, and hand the workspace back to its owner after the scan as a guard. --- action.yml | 12 ++++++++++++ src/cerberus.test.ts | 8 ++++++++ src/scanners.ts | 8 ++++++++ 3 files changed, 28 insertions(+) diff --git a/action.yml b/action.yml index bf1f010..0af0c7e 100644 --- a/action.yml +++ b/action.yml @@ -89,3 +89,15 @@ runs: -e GITHUB_ACTIONS -e GITHUB_REPOSITORY -e GITHUB_REF_NAME -e GITHUB_HEAD_REF \ -e GITHUB_SHA -e GITHUB_ACTOR -e GITHUB_DEFAULT_BRANCH="${{ github.event.repository.default_branch }}" \ "${{ inputs.image }}" scan --mode "${{ inputs.mode }}" + + # The container runs as root over the mounted workspace, so anything a + # scanner leaves behind is owned by root. The next `actions/checkout` runs + # as the runner's user, cannot delete those files, and fails the job — + # every job, not only this one. Hand the workspace back before leaving. + - name: Restore workspace ownership + if: always() + shell: bash + run: | + owner=$(stat -c '%u:%g' "${{ github.workspace }}") + docker run --rm -v "${{ github.workspace }}:/src" \ + --entrypoint chown "${{ inputs.image }}" -R "$owner" /src || true diff --git a/src/cerberus.test.ts b/src/cerberus.test.ts index 1c1bbe2..bde8014 100644 --- a/src/cerberus.test.ts +++ b/src/cerberus.test.ts @@ -193,6 +193,14 @@ describe("buildInvocations", () => { expect(inv.command).toContain("--output-file-path"); }); + it("keeps checkov out of the scanned directory", () => { + // github_configuration writes `github_conf/` into the workspace as root, + // which breaks the next checkout on a self-hosted runner. + const inv = buildInvocations(parseConfig({}), "/tmp/out").find((i) => i.name === "checkov")!; + const argv = inv.command as string[]; + expect(argv[argv.indexOf("--skip-framework") + 1]).toBe("github_configuration"); + }); + it("hadolint writes an empty report when the repo has no Dockerfile", () => { const inv = buildInvocations(parseConfig({}), "/tmp/out").find((i) => i.name === "hadolint")!; expect(inv.command).toContain('"version":"2.1.0"'); // the no-Dockerfile fallback diff --git a/src/scanners.ts b/src/scanners.ts index e1308ef..31aea7d 100644 --- a/src/scanners.ts +++ b/src/scanners.ts @@ -57,11 +57,19 @@ export function buildInvocations(config: CerberusConfig, outDir: string): Invoca // --output-file-path takes a directory; checkov names the file itself. // It also exits non-zero when it finds anything, which is not a failure — // the runner prefers the report over the exit code. + // + // github_configuration is skipped on purpose: with a GITHUB_TOKEN in the + // environment it calls the API and dumps `github_conf/` into the scanned + // directory. We run as root over a mounted workspace, so that directory + // outlives the container owned by root — and the next `actions/checkout`, + // running as the runner's user, cannot delete it and fails the whole + // pipeline. It audits branch protection, which we do not act on anyway. inv.push({ name: "checkov", output: join(outDir, "results_sarif.sarif"), command: [ "checkov", "-d", ".", "-o", "sarif", "--output-file-path", outDir, + "--skip-framework", "github_configuration", "--compact", "--quiet", ...s.checkov.args, ], });