From 2fc8e3399ca86744035e10f7010d74fe06adfb46 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 24 Sep 2026 23:09:00 -0500 Subject: [PATCH 1/2] ci: move untrusted expressions into env and tighten workflow permissions Expand `${{ }}` expressions used inside `run:` scripts (and one github-script body) through step `env:` entries instead of splicing them into the script text, and use the runner-provided GITHUB_* variables where they already carry the same value. This removes every template-injection finding reported by zizmor without changing what the steps do. DEP_OPTS is still shell-parsed as before: it is split with `eval "DEP_OPTS_ARGS=(${DEP_OPTS})"` so quoted values such as CXX='clang++-19 -stdlib=libc++' (tsan) and the msan CFLAGS/CXXFLAGS keep producing the same arguments as the old inline substitution. Also add least-privilege `permissions:` blocks to workflows that ran with the default token permissions, move guix-build's write permissions from the workflow level to the jobs that need them, and set `persist-credentials: false` on the remaining checkouts that do not push. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/build-container.yml | 14 +++++++----- .github/workflows/build-depends.yml | 16 +++++++++---- .github/workflows/build-src.yml | 25 +++++++++++++++------ .github/workflows/build.yml | 9 ++++++-- .github/workflows/cache-depends-sources.yml | 3 +++ .github/workflows/clang-diff-format.yml | 6 +++++ .github/workflows/guix-build.yml | 24 ++++++++++++-------- .github/workflows/lint.yml | 8 +++---- .github/workflows/merge-check.yml | 14 +++++++----- .github/workflows/prevent-master-pr.yml | 2 ++ .github/workflows/release_docker_hub.yml | 16 ++++++++++--- .github/workflows/semantic-pull-request.yml | 4 ++++ .github/workflows/test-src.yml | 8 ++++--- 13 files changed, 107 insertions(+), 42 deletions(-) diff --git a/.github/workflows/build-container.yml b/.github/workflows/build-container.yml index 693bcf815d7f..1ea65c9e6d37 100644 --- a/.github/workflows/build-container.yml +++ b/.github/workflows/build-container.yml @@ -188,10 +188,14 @@ jobs: env: TRUSTED: ${{ github.event_name == 'push' }} CONTEXT_TAG: ${{ inputs.context-tag }} + REPO_NAME: ${{ needs.build-amd64.outputs.repo }} + IMAGE_NAME: ${{ inputs.name }} + TAG: ${{ needs.build-amd64.outputs.tag }} + HASH_TAG: ${{ hashFiles(inputs.file) }} + AMD64_DIGEST: ${{ needs.build-amd64.outputs.digest }} + ARM64_DIGEST: ${{ needs.build-arm64.outputs.digest }} run: | - REPO="ghcr.io/${{ needs.build-amd64.outputs.repo }}/${{ inputs.name }}" - TAG="${{ needs.build-amd64.outputs.tag }}" - HASH_TAG="${{ hashFiles(inputs.file) }}" + REPO="ghcr.io/${REPO_NAME}/${IMAGE_NAME}" if [[ "$TRUSTED" == "true" ]]; then TAGS=(-t "${REPO}:${HASH_TAG}" -t "${REPO}:${TAG}" -t "${REPO}:latest") @@ -205,8 +209,8 @@ jobs: # Use this run's digests: tags can be moved by concurrent runs docker buildx imagetools create --metadata-file "${RUNNER_TEMP}/manifest.json" "${TAGS[@]}" \ - "${REPO}@${{ needs.build-amd64.outputs.digest }}" \ - "${REPO}@${{ needs.build-arm64.outputs.digest }}" + "${REPO}@${AMD64_DIGEST}" \ + "${REPO}@${ARM64_DIGEST}" DIGEST=$(jq -er '."containerimage.descriptor".digest' "${RUNNER_TEMP}/manifest.json") echo "path=${REPO}@${DIGEST}" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/build-depends.yml b/.github/workflows/build-depends.yml index e79c5fa4248e..b22366c62253 100644 --- a/.github/workflows/build-depends.yml +++ b/.github/workflows/build-depends.yml @@ -64,8 +64,11 @@ jobs: - name: Compute cache key id: setup + env: + BUILD_TARGET: ${{ inputs.build-target }} + BASE_IMAGE_DIGEST: ${{ inputs.base-image-digest }} + RUNS_ON: ${{ inputs.runs-on }} run: | - BUILD_TARGET="${{ inputs.build-target }}" source ./ci/dash/matrix.sh echo "DEP_OPTS=${DEP_OPTS}" >> "${GITHUB_OUTPUT}" echo "HOST=${HOST}" >> "${GITHUB_OUTPUT}" @@ -74,7 +77,7 @@ jobs: echo "DEP_HASH=${DEP_HASH}" >> "${GITHUB_OUTPUT}" DOCKERFILE_HASH="${{ hashFiles('contrib/containers/ci/ci.Dockerfile', 'contrib/containers/ci/ci-slim.Dockerfile') }}" DEPENDS_INPUTS_HASH="${{ hashFiles('depends/Makefile', 'depends/funcs.mk', 'depends/gen_id', 'depends/config.guess', 'depends/config.sub', 'depends/config.site.in', 'depends/builders/**', 'depends/hosts/**', 'depends/packages/**', 'depends/patches/**') }}" - CACHE_KEY_PREFIX="depends-${DOCKERFILE_HASH}-${{ inputs.base-image-digest }}-${{ inputs.runs-on }}-${{ inputs.build-target }}" + CACHE_KEY_PREFIX="depends-${DOCKERFILE_HASH}-${BASE_IMAGE_DIGEST}-${RUNS_ON}-${BUILD_TARGET}" CACHE_KEY="${CACHE_KEY_PREFIX}-${DEP_HASH}-${DEPENDS_INPUTS_HASH}" echo "cache-key-prefix=${CACHE_KEY_PREFIX}" >> "${GITHUB_OUTPUT}" echo "cache-key=${CACHE_KEY}" >> "${GITHUB_OUTPUT}" @@ -176,9 +179,14 @@ jobs: ${{ needs.check-cache.outputs.cache-key-prefix }}- - name: Build depends + env: + HOST: ${{ needs.check-cache.outputs.host }} + DEP_OPTS: ${{ needs.check-cache.outputs.dep-opts }} run: | - export HOST="${{ needs.check-cache.outputs.host }}" - env ${{ needs.check-cache.outputs.dep-opts }} make -j$(nproc) -C depends + # DEP_OPTS may contain shell-quoted values, e.g. CXX='clang++-19 -stdlib=libc++' + eval "DEP_OPTS_ARGS=(${DEP_OPTS})" + env "${DEP_OPTS_ARGS[@]}" make -j$(nproc) -C depends + shell: bash - name: Save depends cache # Cache tokens on untrusted triggers are read-only (GitHub change, diff --git a/.github/workflows/build-src.yml b/.github/workflows/build-src.yml index 58c9a03b300d..990299c5fdd5 100644 --- a/.github/workflows/build-src.yml +++ b/.github/workflows/build-src.yml @@ -73,13 +73,15 @@ jobs: - name: Initial setup id: setup + env: + BUILD_TARGET: ${{ inputs.build-target }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha || '' }} run: | git config --global --add safe.directory "$PWD" git fetch -fu origin develop:develop - BUILD_TARGET="${{ inputs.build-target }}" source ./ci/dash/matrix.sh echo "HOST=${HOST}" >> $GITHUB_OUTPUT - echo "PR_BASE_SHA=${{ github.event.pull_request.base.sha || '' }}" >> $GITHUB_OUTPUT + echo "PR_BASE_SHA=${PR_BASE_SHA}" >> $GITHUB_OUTPUT shell: bash - name: Restore SDKs cache @@ -131,10 +133,15 @@ jobs: shell: bash - name: Rebuild depends prefix + env: + DEPENDS_HOST: ${{ inputs.depends-host }} + DEP_OPTS: ${{ inputs.depends-dep-opts }} run: | # Use the HOST and DEP_OPTS from the depends build, not this build-target # This ensures the build_id matches the cached packages - make -j$(nproc) -C depends HOST="${{ inputs.depends-host }}" ${{ inputs.depends-dep-opts }} + # DEP_OPTS may contain shell-quoted values, e.g. CXX='clang++-19 -stdlib=libc++' + eval "DEP_OPTS_ARGS=(${DEP_OPTS})" + make -j$(nproc) -C depends HOST="${DEPENDS_HOST}" "${DEP_OPTS_ARGS[@]}" shell: bash - name: Restore ccache cache @@ -185,6 +192,8 @@ jobs: shell: bash - name: Build source + env: + BUILD_TARGET: ${{ inputs.build-target }} run: | # Nothing after this step needs to write to the remote cache, and the # tests run code built from cached objects; drop the credential even @@ -194,7 +203,6 @@ jobs: CACHE_DIR="/cache" mkdir /output BASE_OUTDIR="/output" - BUILD_TARGET="${{ inputs.build-target }}" source ./ci/dash/matrix.sh ./ci/dash/build_src.sh { echo "### ccache: ${BUILD_TARGET}"; echo '```'; ccache --show-stats; echo '```'; } >> "${GITHUB_STEP_SUMMARY}" @@ -226,9 +234,10 @@ jobs: - name: Run linters if: inputs.build-target == 'linux64_multiprocess' + env: + BUILD_TARGET: ${{ inputs.build-target }} run: | CACHE_DIR="/cache" - export BUILD_TARGET="${{ inputs.build-target }}" source ./ci/dash/matrix.sh ./ci/dash/lint-tidy.sh shell: bash @@ -245,16 +254,18 @@ jobs: key: ctcache-${{ hashFiles('contrib/containers/ci/ci.Dockerfile', 'depends/packages/*') }}-${{ inputs.build-target }}-${{ github.sha }} - name: Run unit tests + env: + BUILD_TARGET: ${{ inputs.build-target }} run: | - BUILD_TARGET="${{ inputs.build-target }}" source ./ci/dash/matrix.sh ./ci/dash/test_unittests.sh shell: bash - name: Bundle artifacts id: bundle + env: + BUILD_TARGET: ${{ inputs.build-target }} run: | - export BUILD_TARGET="${{ inputs.build-target }}" export BUNDLE_KEY="build-${BUILD_TARGET}-$(git rev-parse --short=8 HEAD)" find "build-ci/dashcore-${BUILD_TARGET}/src/" -name "*.a" -type f -delete find "build-ci/dashcore-${BUILD_TARGET}/src/" -name "*.o" -type f -delete diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bf14e4dc9106..eab43a0624d5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,11 +53,14 @@ jobs: steps: - name: Check skip environment variables id: skip-check + env: + SKIP_ON_PUSH: ${{ vars.SKIP_ON_PUSH }} + SKIP_ON_PR: ${{ vars.SKIP_ON_PR }} run: | - if [[ "${{ github.event_name }}" == "push" && "${{ vars.SKIP_ON_PUSH }}" != "" ]]; then + if [[ "${GITHUB_EVENT_NAME}" == "push" && "${SKIP_ON_PUSH}" != "" ]]; then echo "Skipping build on push due to SKIP_ON_PUSH environment variable" echo "skip=true" >> $GITHUB_OUTPUT - elif [[ "${{ github.event_name }}" == "pull_request_target" && "${{ vars.SKIP_ON_PR }}" != "" ]]; then + elif [[ "${GITHUB_EVENT_NAME}" == "pull_request_target" && "${SKIP_ON_PR}" != "" ]]; then echo "Skipping build on pull request due to SKIP_ON_PR environment variable" echo "skip=true" >> $GITHUB_OUTPUT else @@ -67,6 +70,8 @@ jobs: - name: Checkout code if: ${{ steps.skip-check.outputs.skip == 'false' }} uses: actions/checkout@v6 + with: + persist-credentials: false - name: Select runners id: select-runner diff --git a/.github/workflows/cache-depends-sources.yml b/.github/workflows/cache-depends-sources.yml index d4eba66056df..d8fb54b67ee9 100644 --- a/.github/workflows/cache-depends-sources.yml +++ b/.github/workflows/cache-depends-sources.yml @@ -12,6 +12,9 @@ on: # Run daily at 6 AM UTC on the default branch to keep cache warm - cron: '0 6 * * *' +permissions: + contents: read + jobs: cache-sources: name: Cache depends sources diff --git a/.github/workflows/clang-diff-format.yml b/.github/workflows/clang-diff-format.yml index 25811ea647e8..cc8791a99f0f 100644 --- a/.github/workflows/clang-diff-format.yml +++ b/.github/workflows/clang-diff-format.yml @@ -4,12 +4,18 @@ on: pull_request: branches: - develop + +permissions: + contents: read + jobs: ClangFormat: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 + with: + persist-credentials: false - name: Fetch git run: git fetch --no-tags -fu origin develop:develop - name: Run Clang-Format-Diff.py diff --git a/.github/workflows/guix-build.yml b/.github/workflows/guix-build.yml index f79b85a6a8eb..5ffcb6f1e7b3 100644 --- a/.github/workflows/guix-build.yml +++ b/.github/workflows/guix-build.yml @@ -1,9 +1,6 @@ name: Guix Build -permissions: - packages: read - id-token: write - attestations: write +permissions: {} on: pull_request_target: @@ -83,6 +80,10 @@ jobs: needs: build-image # runs-on: [ "self-hosted", "linux", "x64", "ubuntu-core" ] runs-on: ubuntu-24.04-arm + permissions: + packages: read + id-token: write + attestations: write strategy: matrix: build_target: [x86_64-linux-gnu, aarch64-linux-gnu, riscv64-linux-gnu, powerpc64-linux-gnu, x86_64-w64-mingw32, x86_64-apple-darwin, arm64-apple-darwin] @@ -130,14 +131,17 @@ jobs: - name: Run Guix build timeout-minutes: 480 + env: + IMAGE: ${{ needs.build-image.outputs.image }} + BUILD_TARGET: ${{ matrix.build_target }} run: | docker run --privileged -d --rm -t \ --name guix-daemon \ - -v ${{ github.workspace }}/dash:/src/dash \ - -v ${{ github.workspace }}/.cache:/home/ubuntu/.cache \ + -v "${GITHUB_WORKSPACE}/dash:/src/dash" \ + -v "${GITHUB_WORKSPACE}/.cache:/home/ubuntu/.cache" \ -w /src/dash \ - ${{ needs.build-image.outputs.image }} && \ - docker exec guix-daemon bash -c 'HOSTS=${{ matrix.build_target }} /usr/local/bin/guix-start /src/dash' + "${IMAGE}" && \ + docker exec -e HOSTS="${BUILD_TARGET}" guix-daemon bash -c '/usr/local/bin/guix-start /src/dash' - name: Ensure build passes run: | @@ -148,8 +152,10 @@ jobs: - name: Compute SHA256 checksums continue-on-error: true # It will complain on depending on only some hosts + env: + HOSTS: ${{ matrix.build_target }} run: | - HOSTS=${{ matrix.build_target }} ./dash/contrib/containers/guix/scripts/guix-check ${{ github.workspace }}/dash + ./dash/contrib/containers/guix/scripts/guix-check "${GITHUB_WORKSPACE}/dash" - name: Upload build artifacts uses: actions/upload-artifact@v6 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2b859d86847f..9b5df785291e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -56,10 +56,10 @@ jobs: export CHECK_DOC=1 # Determine if this is a PR and set commit range accordingly - if [ "${{ github.event_name }}" = "pull_request_target" ]; then + if [ "${GITHUB_EVENT_NAME}" = "pull_request_target" ]; then export COMMIT_RANGE="$(git merge-base origin/develop HEAD)..HEAD" export PULL_REQUEST="true" - elif [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref }}" != "refs/heads/develop" ] && [ "${{ github.ref }}" != "refs/heads/master" ]; then + elif [ "${GITHUB_EVENT_NAME}" = "push" ] && [ "${GITHUB_REF}" != "refs/heads/develop" ] && [ "${GITHUB_REF}" != "refs/heads/master" ]; then # For push events on feature branches, check against develop export COMMIT_RANGE="$(git merge-base origin/develop HEAD)..HEAD" export PULL_REQUEST="true" @@ -69,8 +69,8 @@ jobs: export PULL_REQUEST="false" fi - echo "Event name: ${{ github.event_name }}" - echo "Ref: ${{ github.ref }}" + echo "Event name: ${GITHUB_EVENT_NAME}" + echo "Ref: ${GITHUB_REF}" echo "COMMIT_RANGE=${COMMIT_RANGE}" echo "PULL_REQUEST=${PULL_REQUEST}" echo "Running git log for commit range:" diff --git a/.github/workflows/merge-check.yml b/.github/workflows/merge-check.yml index c7fce44a329c..e2c855649487 100644 --- a/.github/workflows/merge-check.yml +++ b/.github/workflows/merge-check.yml @@ -20,6 +20,7 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 + persist-credentials: false - name: Set up Git run: | @@ -27,20 +28,23 @@ jobs: git config user.email "noreply@example.com" - name: Check merge --ff-only + env: + PR_BASE_REF: ${{ github.event.pull_request.base.ref }} + PR_NUMBER: ${{ github.event.pull_request.number }} run: | - if [[ "${{ github.ref_name }}" == "master" ]]; then + if [[ "${GITHUB_REF_NAME}" == "master" ]]; then echo "Already on master, no need to check --ff-only" else git fetch --no-tags origin master:master - if [[ "${{ github.event_name }}" == "pull_request"* ]]; then - git fetch --no-tags origin ${{ github.event.pull_request.base.ref }}:base_branch + if [[ "${GITHUB_EVENT_NAME}" == "pull_request"* ]]; then + git fetch --no-tags origin "${PR_BASE_REF}:base_branch" git checkout base_branch - git pull --rebase=false origin pull/${{ github.event.pull_request.number }}/head + git pull --rebase=false origin "pull/${PR_NUMBER}/head" git checkout master git merge --ff-only base_branch else git checkout master - git merge --ff-only ${{ github.sha }} + git merge --ff-only "${GITHUB_SHA}" fi fi diff --git a/.github/workflows/prevent-master-pr.yml b/.github/workflows/prevent-master-pr.yml index 58f506860498..ae76ec87c1c8 100644 --- a/.github/workflows/prevent-master-pr.yml +++ b/.github/workflows/prevent-master-pr.yml @@ -5,6 +5,8 @@ on: branches: - master +permissions: {} + jobs: fail: runs-on: ubuntu-latest diff --git a/.github/workflows/release_docker_hub.yml b/.github/workflows/release_docker_hub.yml index 707be942826a..05a07329f705 100644 --- a/.github/workflows/release_docker_hub.yml +++ b/.github/workflows/release_docker_hub.yml @@ -4,6 +4,9 @@ on: release: types: [published] +permissions: + contents: read + jobs: release: name: Release to Docker Hub @@ -11,6 +14,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + with: + persist-credentials: false - name: Set up QEMU uses: docker/setup-qemu-action@v4 @@ -28,17 +33,20 @@ jobs: - name: Set raw tag id: get_tag + env: + TAG: ${{ github.event.release.tag_name }} run: | - TAG=${{ github.event.release.tag_name }} echo "build_tag=${TAG#v}" >> $GITHUB_OUTPUT - name: Set suffix uses: actions/github-script@v8 id: suffix + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} with: result-encoding: string script: | - const fullTag = '${{ github.event.release.tag_name }}'; + const fullTag = process.env.RELEASE_TAG; if (fullTag.includes('-')) { const [, fullSuffix] = fullTag.split('-'); const [suffix] = fullSuffix.split('.'); @@ -79,4 +87,6 @@ jobs: platforms: linux/amd64,linux/arm64 - name: Image digest - run: echo ${{ steps.docker_build.outputs.digest }} + env: + DIGEST: ${{ steps.docker_build.outputs.digest }} + run: echo "${DIGEST}" diff --git a/.github/workflows/semantic-pull-request.yml b/.github/workflows/semantic-pull-request.yml index e9360c567f52..a3d1a22dcde8 100644 --- a/.github/workflows/semantic-pull-request.yml +++ b/.github/workflows/semantic-pull-request.yml @@ -7,6 +7,10 @@ on: - edited - synchronize +permissions: + contents: read + pull-requests: read + jobs: main: name: Validate PR title diff --git a/.github/workflows/test-src.yml b/.github/workflows/test-src.yml index 347d5e0aebc2..15d318544a95 100644 --- a/.github/workflows/test-src.yml +++ b/.github/workflows/test-src.yml @@ -66,10 +66,11 @@ jobs: - name: Run functional tests id: test + env: + BUILD_TARGET: ${{ inputs.build-target }} + BUNDLE_KEY: ${{ inputs.bundle-key }} run: | git config --global --add safe.directory "$PWD" - export BUILD_TARGET="${{ inputs.build-target }}" - export BUNDLE_KEY="${{ inputs.bundle-key }}" ./ci/dash/bundle-artifacts.sh extract ./ci/dash/slim-workspace.sh source ./ci/dash/matrix.sh @@ -79,8 +80,9 @@ jobs: - name: Bundle test logs id: bundle if: success() || (failure() && steps.test.outcome == 'failure') + env: + BUILD_TARGET: ${{ inputs.build-target }} run: | - export BUILD_TARGET="${{ inputs.build-target }}" echo "short-sha=$(git rev-parse --short=8 HEAD)" >> "${GITHUB_OUTPUT}" ( [ -d "testlogs" ] && echo "upload-logs=true" >> "${GITHUB_OUTPUT}" && ./ci/dash/bundle-logs.sh ) \ || echo "upload-logs=false" >> "${GITHUB_OUTPUT}" From 38511037d880cd944b5fa503ff3c33800c993a03 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 24 Sep 2026 23:18:36 -0500 Subject: [PATCH 2/2] lint: add zizmor GitHub Actions linter Add test/lint/lint-github-actions.py, which runs zizmor (offline) over .github/workflows/ and is picked up by all-lint.py like the other lint-* scripts. It skips gracefully when zizmor is not installed. zizmor 1.30.1 is installed in the CI slim image and in ci/lint/04_install.sh, pinned like the other Python lint tools. Configuration lives in .github/zizmor.yml, keyed by rule/file rather than line numbers: - unpinned-uses: actions stay pinned to release tags, so require an explicit ref (`ref-pin`) instead of zizmor's default SHA-only policy. - self-repository: ignored for build.yml, whose `./.github/workflows/...` references are local reusable workflows loaded from the caller's commit. The remaining accepted findings are suppressed inline with a reason: - dangerous-triggers on the six pull_request_target workflows, each of which needs a write token or secrets for fork PRs. - unpinned-images on the CI container jobs, whose image is built earlier in the same run by build-container.yml. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/build-depends.yml | 2 +- .github/workflows/build-src.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/guix-build.yml | 2 +- .github/workflows/label-merge-conflicts.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/merge-check.yml | 2 +- .github/workflows/predict-conflicts.yml | 2 +- .github/workflows/semantic-pull-request.yml | 2 +- .github/workflows/test-src.yml | 2 +- .github/zizmor.yml | 16 ++++++++ ci/lint/04_install.sh | 3 +- contrib/containers/ci/ci-slim.Dockerfile | 3 +- test/lint/lint-github-actions.py | 42 +++++++++++++++++++++ 14 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 .github/zizmor.yml create mode 100755 test/lint/lint-github-actions.py diff --git a/.github/workflows/build-depends.yml b/.github/workflows/build-depends.yml index b22366c62253..5f038022d102 100644 --- a/.github/workflows/build-depends.yml +++ b/.github/workflows/build-depends.yml @@ -130,7 +130,7 @@ jobs: outputs: built-artifact: depends-built-${{ inputs.build-target }} container: - image: ${{ inputs.container-path }} + image: ${{ inputs.container-path }} # zizmor: ignore[unpinned-images] built earlier in this run by build-container.yml options: --user root steps: - name: Checkout code diff --git a/.github/workflows/build-src.yml b/.github/workflows/build-src.yml index 990299c5fdd5..1c42cd236cf1 100644 --- a/.github/workflows/build-src.yml +++ b/.github/workflows/build-src.yml @@ -58,7 +58,7 @@ jobs: key: ${{ steps.bundle.outputs.key }} freshness: ${{ steps.freshness.outputs.freshness }} container: - image: ${{ inputs.container-path }} + image: ${{ inputs.container-path }} # zizmor: ignore[unpinned-images] built earlier in this run by build-container.yml # SYS_PTRACE is required by LeakSanitizer's stop-the-world # (https://github.com/google/sanitizers/issues/764) options: --user root --cap-add SYS_PTRACE diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eab43a0624d5..6d89460110fe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,7 @@ name: CI on: push: - pull_request_target: + pull_request_target: # zizmor: ignore[dangerous-triggers] CI for fork PRs; PR code runs with a read-only token except the container image builds permissions: actions: read diff --git a/.github/workflows/guix-build.yml b/.github/workflows/guix-build.yml index 5ffcb6f1e7b3..5bd5897eed09 100644 --- a/.github/workflows/guix-build.yml +++ b/.github/workflows/guix-build.yml @@ -3,7 +3,7 @@ name: Guix Build permissions: {} on: - pull_request_target: + pull_request_target: # zizmor: ignore[dangerous-triggers] runs only when a maintainer applies the 'guix-build' label types: [labeled] push: schedule: diff --git a/.github/workflows/label-merge-conflicts.yml b/.github/workflows/label-merge-conflicts.yml index b804b36c5138..6f4f56829c15 100644 --- a/.github/workflows/label-merge-conflicts.yml +++ b/.github/workflows/label-merge-conflicts.yml @@ -5,7 +5,7 @@ on: # So that the `dirtyLabel` is removed if conflicts are resolve # We recommend `pull_request_target` so that github secrets are available. # In `pull_request` we wouldn't be able to change labels of fork PRs - pull_request_target: + pull_request_target: # zizmor: ignore[dangerous-triggers] needs a write token to label fork PRs; does not check out PR code types: [synchronize] permissions: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9b5df785291e..a71e95459010 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,7 +23,7 @@ jobs: outputs: freshness: ${{ steps.freshness.outputs.freshness }} container: - image: ${{ inputs.container-path }} + image: ${{ inputs.container-path }} # zizmor: ignore[unpinned-images] built earlier in this run by build-container.yml options: --user root steps: - name: Checkout code diff --git a/.github/workflows/merge-check.yml b/.github/workflows/merge-check.yml index e2c855649487..7ce44648184e 100644 --- a/.github/workflows/merge-check.yml +++ b/.github/workflows/merge-check.yml @@ -8,7 +8,7 @@ permissions: on: push: - pull_request_target: + pull_request_target: # zizmor: ignore[dangerous-triggers] fetches the PR head only to test a fast-forward merge; runs no PR code pull_request_review: types: [submitted] diff --git a/.github/workflows/predict-conflicts.yml b/.github/workflows/predict-conflicts.yml index 189a563ed652..ebd312f87151 100644 --- a/.github/workflows/predict-conflicts.yml +++ b/.github/workflows/predict-conflicts.yml @@ -1,7 +1,7 @@ name: "Check Potential Conflicts" on: - pull_request_target: + pull_request_target: # zizmor: ignore[dangerous-triggers] needs a write token to comment on fork PRs; runs base-branch code only types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] pull_request_review: types: [submitted] diff --git a/.github/workflows/semantic-pull-request.yml b/.github/workflows/semantic-pull-request.yml index a3d1a22dcde8..b835ec860f24 100644 --- a/.github/workflows/semantic-pull-request.yml +++ b/.github/workflows/semantic-pull-request.yml @@ -1,7 +1,7 @@ name: "Check PR title and description" on: - pull_request_target: + pull_request_target: # zizmor: ignore[dangerous-triggers] validates PR metadata with base-branch code; does not check out PR code types: - opened - edited diff --git a/.github/workflows/test-src.yml b/.github/workflows/test-src.yml index 15d318544a95..4cbf06cea5a1 100644 --- a/.github/workflows/test-src.yml +++ b/.github/workflows/test-src.yml @@ -38,7 +38,7 @@ jobs: group: ${{ github.workflow }}-${{ inputs.cancel-stale && github.ref || github.run_id }}-test-src-${{ inputs.build-target }} cancel-in-progress: true container: - image: ${{ inputs.container-path }} + image: ${{ inputs.container-path }} # zizmor: ignore[unpinned-images] built earlier in this run by build-container.yml # SYS_PTRACE is required by LeakSanitizer's stop-the-world # (https://github.com/google/sanitizers/issues/764) options: --user root --cap-add SYS_PTRACE diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 000000000000..3adb3a382977 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,16 @@ +# Configuration for zizmor, the GitHub Actions static analyzer run by +# test/lint/lint-github-actions.py. Prefer fixing findings; otherwise ignore +# a single finding inline with `# zizmor: ignore[] `. +rules: + # Actions are pinned to release tags rather than commit SHAs so they keep + # receiving upstream fixes without manual bumps. Still require every + # `uses:` to name an explicit ref instead of zizmor's default SHA-only policy. + unpinned-uses: + config: + policies: + "*": ref-pin + self-repository: + ignore: + # build.yml only calls local reusable workflows (`./.github/workflows/...`), + # which GitHub loads from the caller's commit, not the runner filesystem. + - build.yml diff --git a/ci/lint/04_install.sh b/ci/lint/04_install.sh index 83010b9b0057..1104966a8540 100755 --- a/ci/lint/04_install.sh +++ b/ci/lint/04_install.sh @@ -38,7 +38,8 @@ ${CI_RETRY_EXE} pip3 install \ lief==0.13.2 \ mypy==0.981 \ pyzmq==24.0.1 \ - vulture==2.6 + vulture==2.6 \ + zizmor==1.30.1 SHELLCHECK_VERSION=v0.8.0 curl -sL "https://github.com/koalaman/shellcheck/releases/download/${SHELLCHECK_VERSION}/shellcheck-${SHELLCHECK_VERSION}.linux.x86_64.tar.xz" | \ diff --git a/contrib/containers/ci/ci-slim.Dockerfile b/contrib/containers/ci/ci-slim.Dockerfile index 73c864983939..74086f83cd52 100644 --- a/contrib/containers/ci/ci-slim.Dockerfile +++ b/contrib/containers/ci/ci-slim.Dockerfile @@ -83,7 +83,8 @@ RUN uv pip install --system --break-system-packages \ lief==0.13.2 \ mypy==0.981 \ pyzmq==24.0.1 \ - vulture==2.6 + vulture==2.6 \ + zizmor==1.30.1 # Install packages relied on by tests ARG DASH_HASH_VERSION=1.4.0 diff --git a/test/lint/lint-github-actions.py b/test/lint/lint-github-actions.py new file mode 100755 index 000000000000..b970bcfb1558 --- /dev/null +++ b/test/lint/lint-github-actions.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2026 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +""" +Check GitHub Actions workflows for security issues using zizmor. + +Audit configuration lives in .github/zizmor.yml; individual findings are +ignored inline in the workflow with `# zizmor: ignore[] `. +""" + +import shutil +import subprocess +import sys + +CONFIG = '.github/zizmor.yml' +WORKFLOWS_DIR = '.github/workflows/' + + +def main(): + if shutil.which('zizmor') is None: + print('Skipping GitHub Actions linting since zizmor is not installed.') + sys.exit(0) + + zizmor_cmd = [ + 'zizmor', + '--offline', + '--no-progress', + '-qq', # hide log lines (e.g. shell-type guesses); findings are still printed + f'--config={CONFIG}', + WORKFLOWS_DIR, + ] + if subprocess.run(zizmor_cmd).returncode != 0: + print('zizmor reported GitHub Actions issues (see above). Fix them, or add an inline') + print('`# zizmor: ignore[] ` comment if a finding is a false positive.') + sys.exit(1) + + +if __name__ == '__main__': + main()