From 47730b76fe81fcdb20d14bb29737bd6f98a890eb Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 12 Sep 2026 15:41:44 +0000 Subject: [PATCH] Harden CI: point the security audit's zizmor scan at composite actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Actions-security scan in supply-chain.yml audited `.github/workflows` and nothing else. A composite action's steps run inline in the calling job, with that job's token and secrets, so it carries the same rule families a workflow does — but `.github/actions/setup-openclaw/action.yml` was handed to no scanner at all. The step's own comment recorded this as a known gap and deferred it. clawmetry-cloud closed the same gap on its mirror of this scan (cloud #2299), which left this repo the only one of the three whose actions nothing audited. Widen the input set the way cloud does: workflows, plus each `action.yml` / `action.yaml` under `.github/actions` named individually. Naming the files rather than the directory is deliberate — pointing zizmor at a directory hands it every YAML inside, and a non-action YAML landing there later would abort the audit (which this job records as a scanner outage) rather than be skipped. Coverage goes 40 files -> 41, and `zizmor-inputs.txt` records the set as before. Turning the scan on surfaces two `github-env` findings in the action, both Low confidence and both already safe. Declared accepted inline, next to the code, with the reasoning: - the `$GITHUB_PATH` write takes `github.action_path` — the runner's own checkout path for the action, not a caller input and not event data — and exposing its own `node_modules/.bin` is the entire point of the step. - the `$GITHUB_ENV` write is the case the audit warns about, and the step already rejects a multi-line value loudly before writing it. That guard predates this change; the declaration just records that it is the mitigation. Both values reach their scripts through `env:` rather than being expanded into them. Verified: every workflow and the action still parse as YAML; the widened input set reports the same 34 findings as `main` does today, none of them in `.github/actions`; and with the two declarations removed the same run reports 36, which is how I know the action is genuinely being audited rather than silently skipped. No-PRD: CI-only change under .github/, no product code touched. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01McFruqfSz3dEKbDKEVKCDm --- .github/actions/setup-openclaw/action.yml | 20 +++++++++++++++-- .github/workflows/supply-chain.yml | 27 +++++++++++++++-------- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/.github/actions/setup-openclaw/action.yml b/.github/actions/setup-openclaw/action.yml index fb3cc1a478..d4a8367db7 100644 --- a/.github/actions/setup-openclaw/action.yml +++ b/.github/actions/setup-openclaw/action.yml @@ -81,7 +81,15 @@ runs: shell: bash env: ACTION_DIR: ${{ github.action_path }} - run: | + # zizmor github-env, analysed and accepted: the audit fires on any + # $GITHUB_PATH write, because a write whose value an attacker controls + # puts a directory of their choosing ahead of the real tools. The value + # here is `github.action_path` — the runner's own checkout path for this + # action, not a caller input, not event data — bound through env rather + # than expanded into the script. Exposing its own node_modules/.bin is + # the whole point of the step, and there is no other way to put a + # composite action's binaries on PATH for the steps that follow. + run: | # zizmor: ignore[github-env] set -e npm ci --no-audit --no-fund --prefix "$ACTION_DIR" echo "$ACTION_DIR/node_modules/.bin" >> "$GITHUB_PATH" @@ -103,7 +111,15 @@ runs: shell: bash env: OPENCLAW_GATEWAY_TOKEN_INPUT: ${{ inputs.gateway-token }} - run: | + # zizmor github-env, analysed and accepted: the audit fires on any + # $GITHUB_ENV write built from a value the step did not define itself, + # because $GITHUB_ENV is line-oriented and a newline in the value + # defines extra variables for every later step in the job. That is + # exactly the case the `case` guard below rejects, loudly and before + # the write — which is the mitigation the audit asks for, and predates + # this declaration. The value also reaches the script through env + # rather than being expanded into it, so it is never parsed as shell. + run: | # zizmor: ignore[github-env] set -e # Downstream workflow steps read this to authenticate dashboard # requests against the running gateway. Empty token = dashboard diff --git a/.github/workflows/supply-chain.yml b/.github/workflows/supply-chain.yml index a4e0ee6fa7..a91f5759f3 100644 --- a/.github/workflows/supply-chain.yml +++ b/.github/workflows/supply-chain.yml @@ -249,17 +249,26 @@ jobs: GH_TOKEN: ${{ github.token }} run: | mkdir -p audit - # Workflow definitions only, which is the scope the SecurityAuditScanner - # component describes. + # Workflow definitions AND the composite actions under .github/actions. # - # The composite action under .github/actions/ is a real gap and is NOT - # covered here: its steps run inline in the calling job, with that job's - # token and secrets, so it carries the same rule families a workflow - # does, and nothing scans it today. Widening this input set to include - # it is a one-line change, but it is a scope the blueprint does not - # describe, so it needs the product record first rather than arriving - # as a side effect of turning the scanner on. Tracked as follow-up. + # A composite action's steps run inline in the calling job, with that + # job's token and secrets, so it carries the same rule families a + # workflow does — and until now nothing scanned it. This was recorded + # here as a known gap and deferred; clawmetry-cloud closed the same + # gap on its own mirror of this scan (cloud #2299), which left this + # repo the only one of the three whose actions nothing audited. + # + # Composite actions are named individually rather than by directory: + # pointing zizmor at a directory hands it every YAML file inside, and + # a non-action YAML landing there later would abort the audit + # (recorded as a scanner outage below) rather than be skipped. inputs=".github/workflows" + if [ -d .github/actions ]; then + while IFS= read -r a; do + inputs="$inputs $a" + done < <(find .github/actions -type f \ + \( -name 'action.yml' -o -name 'action.yaml' \) | sort) + fi # Record the file set the scanner was handed. A finding list is # identical whether it audited thirty-seven files or none, so # coverage is the one thing the JSON cannot tell you afterwards.