From e3347e502a1f44ad927678ffd3cf653d85488a2a Mon Sep 17 00:00:00 2001 From: pasta Date: Fri, 28 Nov 2025 09:32:19 -0600 Subject: [PATCH 1/8] ci: harden workflows against script injection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move all ${{ }} expressions from run: script bodies to env: blocks. This prevents potential script injection attacks by ensuring that expression values are passed as environment variables rather than being interpolated directly into shell scripts. While most of these expressions come from trusted sources (workflow_call inputs with hardcoded values, step outputs, safe github context fields), this change provides structural safety as a defense-in-depth measure. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/build-container.yml | 4 +++- .github/workflows/build-depends.yml | 12 +++++++++--- .github/workflows/build-src.yml | 20 +++++++++++++------- .github/workflows/build.yml | 8 ++++++-- .github/workflows/guix-build.yml | 22 ++++++++++++++++------ .github/workflows/lint.yml | 11 +++++++---- .github/workflows/merge-check.yml | 16 +++++++++++----- .github/workflows/release_docker_hub.yml | 12 +++++++++--- .github/workflows/test-src.yml | 8 +++++--- 9 files changed, 79 insertions(+), 34 deletions(-) diff --git a/.github/workflows/build-container.yml b/.github/workflows/build-container.yml index 2da2ae152999..a571cac8d230 100644 --- a/.github/workflows/build-container.yml +++ b/.github/workflows/build-container.yml @@ -44,9 +44,11 @@ jobs: - name: Prepare variables id: prepare + env: + GITHUB_REPOSITORY_LC: ${{ github.repository }} run: | BRANCH_NAME=$(echo "${GITHUB_REF##*/}" | tr '[:upper:]' '[:lower:]') - REPO_NAME=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') + REPO_NAME=$(echo "${GITHUB_REPOSITORY_LC}" | tr '[:upper:]' '[:lower:]') echo "tag=${BRANCH_NAME}" >> "$GITHUB_OUTPUT" echo "repo=${REPO_NAME}" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/build-depends.yml b/.github/workflows/build-depends.yml index d6679e47c79d..ebf964d480c2 100644 --- a/.github/workflows/build-depends.yml +++ b/.github/workflows/build-depends.yml @@ -52,8 +52,9 @@ jobs: - name: Compute cache key id: setup + env: + BUILD_TARGET: ${{ inputs.build-target }} run: | - BUILD_TARGET="${{ inputs.build-target }}" source ./ci/dash/matrix.sh echo "DEP_OPTS=${DEP_OPTS}" >> "${GITHUB_OUTPUT}" echo "HOST=${HOST}" >> "${GITHUB_OUTPUT}" @@ -127,9 +128,14 @@ jobs: depends-${{ hashFiles('contrib/containers/ci/ci.Dockerfile', 'contrib/containers/ci/ci-slim.Dockerfile') }}-${{ inputs.build-target }}- - name: Build depends + env: + HOST: ${{ steps.setup.outputs.HOST }} + DEP_OPTS: ${{ steps.setup.outputs.DEP_OPTS }} run: | - export HOST="${{ needs.check-cache.outputs.host }}" - env ${{ needs.check-cache.outputs.dep-opts }} make -j$(nproc) -C depends + if [ "${HOST}" = "x86_64-apple-darwin" ]; then + ./contrib/containers/guix/scripts/setup-sdk + fi + env ${DEP_OPTS} make -j$(nproc) -C depends - name: Save depends cache uses: actions/cache/save@v5 diff --git a/.github/workflows/build-src.yml b/.github/workflows/build-src.yml index de19917fefa3..533ebeb5278d 100644 --- a/.github/workflows/build-src.yml +++ b/.github/workflows/build-src.yml @@ -51,13 +51,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 @@ -93,12 +95,13 @@ jobs: ccache-${{ hashFiles('contrib/containers/ci/ci.Dockerfile', 'depends/packages/*') }}-${{ inputs.build-target }}- - name: Build source + env: + BUILD_TARGET: ${{ inputs.build-target }} run: | CCACHE_MAXSIZE="600M" CACHE_DIR="/cache" mkdir /output BASE_OUTDIR="/output" - BUILD_TARGET="${{ inputs.build-target }}" source ./ci/dash/matrix.sh ./ci/dash/build_src.sh ccache -X 9 @@ -129,9 +132,9 @@ 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 @@ -148,16 +151,19 @@ 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 }}" + BASE_OUTDIR="/output" 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 67b2b595fc3f..522fbf3a902d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,11 +32,15 @@ jobs: steps: - name: Check skip environment variables id: skip-check + env: + EVENT_NAME: ${{ github.event_name }} + 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 [[ "${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 [[ "${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 diff --git a/.github/workflows/guix-build.yml b/.github/workflows/guix-build.yml index 5c3bc61d39c9..9c4ab8c2ab22 100644 --- a/.github/workflows/guix-build.yml +++ b/.github/workflows/guix-build.yml @@ -36,12 +36,14 @@ jobs: - name: Commit variables id: prepare + env: + GITHUB_REPOSITORY_LC: ${{ github.repository }} run: | echo "hash=$(sha256sum ./dash/contrib/containers/guix/Dockerfile | cut -d ' ' -f1)" >> $GITHUB_OUTPUT echo "host_user_id=$(id -u)" >> $GITHUB_OUTPUT echo "host_group_id=$(id -g)" >> $GITHUB_OUTPUT BRANCH_NAME=$(echo "${GITHUB_REF##*/}" | tr '[:upper:]' '[:lower:]') - REPO_NAME=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') + REPO_NAME=$(echo "${GITHUB_REPOSITORY_LC}" | tr '[:upper:]' '[:lower:]') echo "image-tag=${BRANCH_NAME}" >> $GITHUB_OUTPUT echo "repo-name=${REPO_NAME}" >> $GITHUB_OUTPUT @@ -118,14 +120,19 @@ jobs: - name: Run Guix build timeout-minutes: 480 + env: + WORKSPACE: ${{ github.workspace }} + REPO_NAME: ${{ needs.build-image.outputs.repo-name }} + IMAGE_TAG: ${{ needs.build-image.outputs.image-tag }} + 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 "${WORKSPACE}/dash:/src/dash" \ + -v "${WORKSPACE}/.cache:/home/ubuntu/.cache" \ -w /src/dash \ - ghcr.io/${{ needs.build-image.outputs.repo-name }}/dashcore-guix-builder:${{ needs.build-image.outputs.image-tag }} && \ - docker exec guix-daemon bash -c 'HOSTS=${{ matrix.build_target }} /usr/local/bin/guix-start /src/dash' + "ghcr.io/${REPO_NAME}/dashcore-guix-builder:${IMAGE_TAG}" && \ + docker exec guix-daemon bash -c "HOSTS=${BUILD_TARGET} /usr/local/bin/guix-start /src/dash" - name: Ensure build passes run: | @@ -136,8 +143,11 @@ jobs: - name: Compute SHA256 checksums continue-on-error: true # It will complain on depending on only some hosts + env: + BUILD_TARGET: ${{ matrix.build_target }} + WORKSPACE: ${{ github.workspace }} run: | - HOSTS=${{ matrix.build_target }} ./dash/contrib/containers/guix/scripts/guix-check ${{ github.workspace }}/dash + HOSTS="${BUILD_TARGET}" ./dash/contrib/containers/guix/scripts/guix-check "${WORKSPACE}/dash" - name: Upload build artifacts uses: actions/upload-artifact@v6 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a411130eb42c..57709d9f5380 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -33,15 +33,18 @@ jobs: shell: bash - name: Run linters + env: + EVENT_NAME: ${{ github.event_name }} + REF: ${{ github.ref }} run: | export BUILD_TARGET="linux64" export CHECK_DOC=1 # Determine if this is a PR and set commit range accordingly - if [ "${{ github.event_name }}" = "pull_request_target" ]; then + if [ "${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 [ "${EVENT_NAME}" = "push" ] && [ "${REF}" != "refs/heads/develop" ] && [ "${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" @@ -51,8 +54,8 @@ jobs: export PULL_REQUEST="false" fi - echo "Event name: ${{ github.event_name }}" - echo "Ref: ${{ github.ref }}" + echo "Event name: ${EVENT_NAME}" + echo "Ref: ${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 59daf7faf791..d7bb6b7b0cfb 100644 --- a/.github/workflows/merge-check.yml +++ b/.github/workflows/merge-check.yml @@ -24,20 +24,26 @@ jobs: git config user.email "noreply@example.com" - name: Check merge --ff-only + env: + REF_NAME: ${{ github.ref_name }} + EVENT_NAME: ${{ github.event_name }} + PR_BASE_REF: ${{ github.event.pull_request.base.ref }} + PR_NUMBER: ${{ github.event.pull_request.number }} + COMMIT_SHA: ${{ github.sha }} run: | - if [[ "${{ github.ref_name }}" == "master" ]]; then + if [[ "${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 [[ "${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 "${COMMIT_SHA}" fi fi diff --git a/.github/workflows/release_docker_hub.yml b/.github/workflows/release_docker_hub.yml index e0a759928ab9..578cc5bf4cca 100644 --- a/.github/workflows/release_docker_hub.yml +++ b/.github/workflows/release_docker_hub.yml @@ -28,17 +28,21 @@ jobs: - name: Set raw tag id: get_tag + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} run: | - TAG=${{ github.event.release.tag_name }} + TAG="${RELEASE_TAG}" echo "build_tag=${TAG#v}" >> $GITHUB_OUTPUT - name: Set suffix uses: actions/github-script@v7 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 +83,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/test-src.yml b/.github/workflows/test-src.yml index 0f6e22ec0b0d..f66209994efe 100644 --- a/.github/workflows/test-src.yml +++ b/.github/workflows/test-src.yml @@ -53,10 +53,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 @@ -66,8 +67,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 8a34a7325e59257ffa0aa4d349f1740244814c10 Mon Sep 17 00:00:00 2001 From: pasta Date: Fri, 28 Nov 2025 14:23:12 -0600 Subject: [PATCH 2/8] ci: add persist-credentials and explicit permissions to workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add persist-credentials: false to all checkout actions to prevent credential persistence in .git/config (artipacked vulnerability) - Add explicit minimal permissions blocks to workflows that were using defaults (clang-diff-format, prevent-master-pr, release_docker_hub, semantic-pull-request) - Scope permissions to job level in guix-build.yml where possible (build-image needs packages:write, build needs id-token/attestations) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/build-container.yml | 1 + .github/workflows/build-depends.yml | 1 + .github/workflows/build-src.yml | 1 + .github/workflows/clang-diff-format.yml | 6 ++++++ .github/workflows/guix-build.yml | 17 ++++++++++++----- .github/workflows/lint.yml | 1 + .github/workflows/merge-check.yml | 1 + .github/workflows/predict-conflicts.yml | 2 ++ .github/workflows/prevent-master-pr.yml | 2 ++ .github/workflows/release_docker_hub.yml | 5 +++++ .github/workflows/semantic-pull-request.yml | 3 +++ .github/workflows/test-src.yml | 1 + 12 files changed, 36 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-container.yml b/.github/workflows/build-container.yml index a571cac8d230..349384eb817f 100644 --- a/.github/workflows/build-container.yml +++ b/.github/workflows/build-container.yml @@ -41,6 +41,7 @@ jobs: uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Prepare variables id: prepare diff --git a/.github/workflows/build-depends.yml b/.github/workflows/build-depends.yml index ebf964d480c2..f041b1e70b2a 100644 --- a/.github/workflows/build-depends.yml +++ b/.github/workflows/build-depends.yml @@ -40,6 +40,7 @@ jobs: uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false sparse-checkout: | ci/dash ci/test diff --git a/.github/workflows/build-src.yml b/.github/workflows/build-src.yml index 533ebeb5278d..1a0aa5df6c58 100644 --- a/.github/workflows/build-src.yml +++ b/.github/workflows/build-src.yml @@ -48,6 +48,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 50 + persist-credentials: false - name: Initial setup id: setup diff --git a/.github/workflows/clang-diff-format.yml b/.github/workflows/clang-diff-format.yml index 0c9087c3d898..797285dca4ee 100644 --- a/.github/workflows/clang-diff-format.yml +++ b/.github/workflows/clang-diff-format.yml @@ -4,6 +4,10 @@ on: pull_request: branches: - develop + +permissions: + contents: read + jobs: ClangFormat: runs-on: ubuntu-latest @@ -12,6 +16,8 @@ jobs: 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 9c4ab8c2ab22..031330a9abb6 100644 --- a/.github/workflows/guix-build.yml +++ b/.github/workflows/guix-build.yml @@ -1,10 +1,5 @@ name: Guix Build -permissions: - packages: write - id-token: write - attestations: write - on: pull_request_target: types: [labeled] @@ -13,9 +8,15 @@ on: # Run weekly at 3 AM UTC on Sunday on the default branch - cron: '0 3 * * 0' +permissions: + contents: read + jobs: build-image: runs-on: ubuntu-24.04-arm + permissions: + contents: read + packages: write if: | (github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/') || vars.RUN_GUIX_ON_ALL_PUSH == 'true')) || contains(github.event.pull_request.labels.*.name, 'guix-build') || @@ -30,6 +31,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} path: dash fetch-depth: 0 + persist-credentials: false - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 @@ -75,6 +77,10 @@ jobs: needs: build-image # runs-on: [ "self-hosted", "linux", "x64", "ubuntu-core" ] runs-on: ubuntu-24.04-arm + permissions: + contents: 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] @@ -91,6 +97,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} path: dash fetch-depth: 0 + persist-credentials: false - name: Cache depends sources uses: actions/cache@v5 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 57709d9f5380..856c50d76826 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -25,6 +25,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 50 + persist-credentials: false - name: Initial setup run: | diff --git a/.github/workflows/merge-check.yml b/.github/workflows/merge-check.yml index d7bb6b7b0cfb..070f2b7ad626 100644 --- a/.github/workflows/merge-check.yml +++ b/.github/workflows/merge-check.yml @@ -17,6 +17,7 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 + persist-credentials: false - name: Set up Git run: | diff --git a/.github/workflows/predict-conflicts.yml b/.github/workflows/predict-conflicts.yml index 80a06ed793bc..a99381bb3189 100644 --- a/.github/workflows/predict-conflicts.yml +++ b/.github/workflows/predict-conflicts.yml @@ -29,6 +29,8 @@ jobs: ghToken: "${{ secrets.GITHUB_TOKEN }}" - name: Checkout uses: actions/checkout@v6 + with: + persist-credentials: false - name: validate potential conflicts id: validate_conflicts run: pip3 install hjson && .github/workflows/handle_potential_conflicts.py "$conflicts" 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 578cc5bf4cca..0355d78b385a 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 diff --git a/.github/workflows/semantic-pull-request.yml b/.github/workflows/semantic-pull-request.yml index 3ad1d1a4d93d..744a41ad08b1 100644 --- a/.github/workflows/semantic-pull-request.yml +++ b/.github/workflows/semantic-pull-request.yml @@ -7,6 +7,9 @@ on: - edited - synchronize +permissions: + pull-requests: read + jobs: main: name: Validate PR title diff --git a/.github/workflows/test-src.yml b/.github/workflows/test-src.yml index f66209994efe..d2e8d177e6c5 100644 --- a/.github/workflows/test-src.yml +++ b/.github/workflows/test-src.yml @@ -37,6 +37,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 1 + persist-credentials: false - name: Download build artifacts uses: actions/download-artifact@v8 From 0e9d7b494535959465c1eb03fb818861f17d62be Mon Sep 17 00:00:00 2001 From: pasta Date: Fri, 28 Nov 2025 14:24:09 -0600 Subject: [PATCH 3/8] ci: add zizmor linter for GitHub Actions security MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add lint-github-actions.py to check workflows for security issues using zizmor. The linter: - Checks for template injection, dangerous triggers, excessive permissions, credential persistence, and other security issues - Skips gracefully if zizmor is not installed - Disables unpinned-uses and unpinned-images audits (not a priority) - Ignores specific findings that are intentional or false positives: - dangerous-triggers: pull_request_target used with proper safeguards - template-injection: workflow_call inputs from hardcoded callers - excessive-permissions: required for reusable workflow architecture Install zizmor with: pip install zizmor 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- test/lint/lint-github-actions.py | 114 +++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100755 test/lint/lint-github-actions.py diff --git a/test/lint/lint-github-actions.py b/test/lint/lint-github-actions.py new file mode 100755 index 000000000000..b3f690bf928d --- /dev/null +++ b/test/lint/lint-github-actions.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 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 for security issues in GitHub Actions workflow files using zizmor. +""" + +import subprocess +import sys +import tempfile +import os + +# Disabled audits: +# These are intentionally disabled and don't indicate security issues in our context. +DISABLED = [ + 'unpinned-uses', # We use version tags rather than SHA pinning + 'unpinned-images', # Container images use dynamic tags +] + +# Ignored findings for specific files/locations: +# Format: 'audit-name': ['filename.yml:line', ...] +# Note: Use base filename only, not full path +IGNORED = { + # pull_request_target is used intentionally in these workflows with proper + # safeguards (explicit checkout of PR head SHA, limited permissions) + 'dangerous-triggers': [ + 'build.yml:3', + 'guix-build.yml:3', + 'label-merge-conflicts.yml:2', + 'merge-check.yml:6', + 'predict-conflicts.yml:3', + 'semantic-pull-request.yml:3', + ], + # inputs.context is passed to docker/build-push-action but only from internal + # workflow_call callers with hardcoded paths - not user-controllable + 'template-injection': [ + 'build-container.yml:60', + ], + # packages:write at workflow level is required because reusable workflows + # (build-container.yml) inherit caller permissions and need it to push to ghcr.io + 'excessive-permissions': [ + 'build.yml:9', + ], +} + + +def check_zizmor_install(): + try: + subprocess.run( + ['zizmor', '--version'], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=True + ) + except FileNotFoundError: + print('Skipping GitHub Actions linting since zizmor is not installed.') + print('Install with: pip install zizmor') + sys.exit(0) + + +def generate_config(): + """Generate zizmor configuration with disabled and ignored rules.""" + lines = ['rules:'] + + # Add disabled audits + for audit in DISABLED: + lines.append(f' {audit}:') + lines.append(f' disable: true') + + # Add ignored findings + for audit, locations in IGNORED.items(): + lines.append(f' {audit}:') + lines.append(f' ignore:') + for loc in locations: + lines.append(f' - {loc}') + + return '\n'.join(lines) + '\n' + + +def main(): + check_zizmor_install() + + # Create a temporary config file + config_content = generate_config() + + with tempfile.NamedTemporaryFile(mode='w', suffix='.yml', delete=False) as f: + f.write(config_content) + config_path = f.name + + try: + # Build the zizmor command + zizmor_cmd = [ + 'zizmor', + '--config', config_path, + '.github/workflows/', + ] + + # Run zizmor + result = subprocess.run(zizmor_cmd) + + # zizmor returns non-zero if it finds issues + if result.returncode != 0: + print('GitHub Actions security issues found. Please fix the above issues.') + sys.exit(1) + finally: + # Clean up temp file + os.unlink(config_path) + + +if __name__ == '__main__': + main() From 25aa4813a64902701f62615312e03cb1713e6664 Mon Sep 17 00:00:00 2001 From: pasta Date: Fri, 28 Nov 2025 14:36:56 -0600 Subject: [PATCH 4/8] ci: add zizmor to CI container image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Install zizmor==1.17.0 in the CI container so lint-github-actions.py can run as part of the lint suite. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- contrib/containers/ci/ci-slim.Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contrib/containers/ci/ci-slim.Dockerfile b/contrib/containers/ci/ci-slim.Dockerfile index 5332f8504a57..a0d848e84109 100644 --- a/contrib/containers/ci/ci-slim.Dockerfile +++ b/contrib/containers/ci/ci-slim.Dockerfile @@ -84,7 +84,8 @@ RUN uv pip install --system --break-system-packages \ multiprocess \ mypy==0.981 \ pyzmq==24.0.1 \ - vulture==2.6 + vulture==2.6 \ + zizmor==1.17.0 # Install packages relied on by tests ARG DASH_HASH_VERSION=1.4.0 From 299b9a9ffafc39e8c4b760e9d9a45c4688ac15e5 Mon Sep 17 00:00:00 2001 From: PastaPastaPasta <6443210+PastaPastaPasta@users.noreply.github.com> Date: Mon, 1 Dec 2025 11:12:28 -0600 Subject: [PATCH 5/8] chore: bump copyright to 2025 Co-authored-by: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> --- test/lint/lint-github-actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lint/lint-github-actions.py b/test/lint/lint-github-actions.py index b3f690bf928d..c6b155340513 100755 --- a/test/lint/lint-github-actions.py +++ b/test/lint/lint-github-actions.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (c) 2024 The Dash Core developers +# Copyright (c) 2025 The Dash Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. From 7e0c6ef40d3e84c5b96824abf1736ed4d5ffba97 Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 1 Dec 2025 12:05:20 -0600 Subject: [PATCH 6/8] fix: coderabbit suggestions --- test/lint/lint-github-actions.py | 4 ++-- test/util/data/non-backported.txt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/lint/lint-github-actions.py b/test/lint/lint-github-actions.py index c6b155340513..09bda871ac03 100755 --- a/test/lint/lint-github-actions.py +++ b/test/lint/lint-github-actions.py @@ -68,12 +68,12 @@ def generate_config(): # Add disabled audits for audit in DISABLED: lines.append(f' {audit}:') - lines.append(f' disable: true') + lines.append(' disable: true') # Add ignored findings for audit, locations in IGNORED.items(): lines.append(f' {audit}:') - lines.append(f' ignore:') + lines.append(' ignore:') for loc in locations: lines.append(f' - {loc}') diff --git a/test/util/data/non-backported.txt b/test/util/data/non-backported.txt index a570da3bc6fa..7bbb49afbcb7 100644 --- a/test/util/data/non-backported.txt +++ b/test/util/data/non-backported.txt @@ -72,3 +72,4 @@ src/wallet/bip39* src/wallet/coinjoin.* src/wallet/hdchain.* src/hash_x11.h +test/lint/lint-github-actions.py From 4374c76032cc7d63ab571e088fa76c7a7e5b53e3 Mon Sep 17 00:00:00 2001 From: PastaPastaPasta <6443210+PastaPastaPasta@users.noreply.github.com> Date: Mon, 1 Dec 2025 12:11:55 -0600 Subject: [PATCH 7/8] Update test/util/data/non-backported.txt --- test/util/data/non-backported.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/test/util/data/non-backported.txt b/test/util/data/non-backported.txt index 7bbb49afbcb7..a570da3bc6fa 100644 --- a/test/util/data/non-backported.txt +++ b/test/util/data/non-backported.txt @@ -72,4 +72,3 @@ src/wallet/bip39* src/wallet/coinjoin.* src/wallet/hdchain.* src/hash_x11.h -test/lint/lint-github-actions.py From ee4a64e91f8e6af13b5a36b6899bb16dc771746d Mon Sep 17 00:00:00 2001 From: pasta Date: Sun, 5 Apr 2026 13:01:17 -0500 Subject: [PATCH 8/8] fix: address zizmor linter findings from rebase - Add persist-credentials: false to all checkout steps - Move template-injection-flagged inputs to env vars - Fix cross-job step reference in build-depends.yml - Remove stale BASE_OUTDIR in build-src.yml unit tests - Add permissions block to cache-depends-sources.yml - Update zizmor ignore list for remaining false positives --- .github/workflows/build-container.yml | 14 +++++++++++--- .github/workflows/build-depends.yml | 8 +++++--- .github/workflows/build-src.yml | 6 ++++-- .github/workflows/build.yml | 2 ++ .github/workflows/cache-depends-sources.yml | 4 ++++ test/lint/lint-github-actions.py | 9 ++++++--- 6 files changed, 32 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build-container.yml b/.github/workflows/build-container.yml index 349384eb817f..62fe5beaf84b 100644 --- a/.github/workflows/build-container.yml +++ b/.github/workflows/build-container.yml @@ -88,12 +88,15 @@ jobs: uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Prepare variables id: prepare + env: + GITHUB_REPOSITORY_LC: ${{ github.repository }} run: | BRANCH_NAME=$(echo "${GITHUB_REF##*/}" | tr '[:upper:]' '[:lower:]') - REPO_NAME=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') + REPO_NAME=$(echo "${GITHUB_REPOSITORY_LC}" | tr '[:upper:]' '[:lower:]') echo "tag=${BRANCH_NAME}" >> "$GITHUB_OUTPUT" echo "repo=${REPO_NAME}" >> "$GITHUB_OUTPUT" @@ -131,6 +134,7 @@ jobs: uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 @@ -143,9 +147,13 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Create and push multi-arch manifest + env: + REPO_NAME: ${{ needs.build-amd64.outputs.repo }} + CONTAINER_NAME: ${{ inputs.name }} + BRANCH_TAG: ${{ needs.build-amd64.outputs.tag }} run: | - REPO="ghcr.io/${{ needs.build-amd64.outputs.repo }}/${{ inputs.name }}" - TAG="${{ needs.build-amd64.outputs.tag }}" + REPO="ghcr.io/${REPO_NAME}/${CONTAINER_NAME}" + TAG="${BRANCH_TAG}" HASH_TAG="${{ hashFiles(inputs.file) }}" # Create manifest from arch-specific images diff --git a/.github/workflows/build-depends.yml b/.github/workflows/build-depends.yml index f041b1e70b2a..1f6023e445d3 100644 --- a/.github/workflows/build-depends.yml +++ b/.github/workflows/build-depends.yml @@ -55,6 +55,7 @@ jobs: id: setup env: BUILD_TARGET: ${{ inputs.build-target }} + RUNS_ON: ${{ inputs.runs-on }} run: | source ./ci/dash/matrix.sh echo "DEP_OPTS=${DEP_OPTS}" >> "${GITHUB_OUTPUT}" @@ -64,7 +65,7 @@ jobs: echo "DEP_HASH=${DEP_HASH}" >> "${GITHUB_OUTPUT}" DOCKERFILE_HASH="${{ hashFiles('contrib/containers/ci/ci.Dockerfile', 'contrib/containers/ci/ci-slim.Dockerfile') }}" PACKAGES_HASH="${{ hashFiles('depends/packages/*', 'depends/Makefile') }}" - CACHE_KEY="depends-${DOCKERFILE_HASH}-${{ inputs.runs-on }}-${{ inputs.build-target }}-${DEP_HASH}-${PACKAGES_HASH}" + CACHE_KEY="depends-${DOCKERFILE_HASH}-${RUNS_ON}-${BUILD_TARGET}-${DEP_HASH}-${PACKAGES_HASH}" echo "cache-key=${CACHE_KEY}" >> "${GITHUB_OUTPUT}" echo "Cache key: ${CACHE_KEY}" shell: bash @@ -103,6 +104,7 @@ jobs: uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Restore depends sources uses: actions/cache/restore@v5 @@ -130,8 +132,8 @@ jobs: - name: Build depends env: - HOST: ${{ steps.setup.outputs.HOST }} - DEP_OPTS: ${{ steps.setup.outputs.DEP_OPTS }} + HOST: ${{ needs.check-cache.outputs.host }} + DEP_OPTS: ${{ needs.check-cache.outputs.dep-opts }} run: | if [ "${HOST}" = "x86_64-apple-darwin" ]; then ./contrib/containers/guix/scripts/setup-sdk diff --git a/.github/workflows/build-src.yml b/.github/workflows/build-src.yml index 1a0aa5df6c58..cbe510c1b66b 100644 --- a/.github/workflows/build-src.yml +++ b/.github/workflows/build-src.yml @@ -80,10 +80,13 @@ jobs: fail-on-cache-miss: true - 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 }} + env ${DEP_OPTS} make -j$(nproc) -C depends HOST="${DEPENDS_HOST}" shell: bash - name: Restore ccache cache @@ -155,7 +158,6 @@ jobs: env: BUILD_TARGET: ${{ inputs.build-target }} run: | - BASE_OUTDIR="/output" source ./ci/dash/matrix.sh ./ci/dash/test_unittests.sh shell: bash diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 522fbf3a902d..23b1b90cb412 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,6 +50,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 60dfa4457ab1..557ff95abc11 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 @@ -23,6 +26,7 @@ jobs: uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Check for cached sources id: cache-check diff --git a/test/lint/lint-github-actions.py b/test/lint/lint-github-actions.py index 09bda871ac03..1a37d4a01484 100755 --- a/test/lint/lint-github-actions.py +++ b/test/lint/lint-github-actions.py @@ -34,10 +34,13 @@ 'predict-conflicts.yml:3', 'semantic-pull-request.yml:3', ], - # inputs.context is passed to docker/build-push-action but only from internal - # workflow_call callers with hardcoded paths - not user-controllable + # inputs.context/file/name are passed to docker/build-push-action but only from + # internal workflow_call callers with hardcoded paths - not user-controllable. + # hashFiles(inputs.file) in the manifest run block produces a hash string. 'template-injection': [ - 'build-container.yml:60', + 'build-container.yml:70', + 'build-container.yml:114', + 'build-container.yml:157', ], # packages:write at workflow level is required because reusable workflows # (build-container.yml) inherit caller permissions and need it to push to ghcr.io