diff --git a/.github/workflows/build-container.yml b/.github/workflows/build-container.yml index 67df008c220df..a9f5efbde475e 100644 --- a/.github/workflows/build-container.yml +++ b/.github/workflows/build-container.yml @@ -32,12 +32,15 @@ jobs: uses: actions/checkout@v4 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 diff --git a/.github/workflows/build-depends.yml b/.github/workflows/build-depends.yml index b2fefcba010cd..222ee4345dcdc 100644 --- a/.github/workflows/build-depends.yml +++ b/.github/workflows/build-depends.yml @@ -30,18 +30,19 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Initial setup 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}" DEP_HASH="$(echo -n "${BUILD_TARGET}" "${DEP_OPTS}" "${HOST}" | sha256sum | head -c 64)" echo "\"${BUILD_TARGET}\" has HOST=\"${HOST}\" and DEP_OPTS=\"${DEP_OPTS}\" with hash \"${DEP_HASH}\"" echo "DEP_HASH=${DEP_HASH}" >> "${GITHUB_OUTPUT}" - shell: bash - name: Cache depends sources @@ -76,12 +77,14 @@ jobs: depends-${{ hashFiles('contrib/containers/ci/ci.Dockerfile') }}-${{ inputs.build-target }}- - name: Build depends + env: + HOST: ${{ steps.setup.outputs.HOST }} + DEP_OPTS: ${{ steps.setup.outputs.DEP_OPTS }} run: | - export HOST="${{ steps.setup.outputs.HOST }}" if [ "${HOST}" = "x86_64-apple-darwin" ]; then ./contrib/containers/guix/scripts/setup-sdk fi - env ${{ steps.setup.outputs.DEP_OPTS }} make -j$(nproc) -C depends + env ${DEP_OPTS} make -j$(nproc) -C depends - name: Save depends cache uses: actions/cache/save@v4 diff --git a/.github/workflows/build-src.yml b/.github/workflows/build-src.yml index 6f9d3279b845b..20550778d00dc 100644 --- a/.github/workflows/build-src.yml +++ b/.github/workflows/build-src.yml @@ -35,16 +35,19 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 50 + persist-credentials: false - 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 @@ -75,12 +78,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="400M" 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 @@ -89,24 +93,27 @@ jobs: - name: Run linters if: inputs.build-target == 'linux64_multiprocess' + env: + BUILD_TARGET: ${{ inputs.build-target }} run: | - export BUILD_TARGET="${{ inputs.build-target }}" source ./ci/dash/matrix.sh ./ci/dash/lint-tidy.sh shell: bash - name: Run unit tests + env: + BUILD_TARGET: ${{ inputs.build-target }} run: | BASE_OUTDIR="/output" - 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)" ./ci/dash/bundle-artifacts.sh create echo "key=${BUNDLE_KEY}" >> "${GITHUB_OUTPUT}" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4e9b6c9efa8b1..cbd22fad01b20 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,11 +26,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/clang-diff-format.yml b/.github/workflows/clang-diff-format.yml index 5f43bb4f792ca..89276ca1ca069 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@v3 + 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 48ab032f40bf2..6f8c97a5f5af6 100644 --- a/.github/workflows/guix-build.yml +++ b/.github/workflows/guix-build.yml @@ -1,19 +1,20 @@ name: Guix Build -permissions: - packages: write - id-token: write - attestations: write - on: pull_request_target: pull_request: types: [labeled] push: +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') @@ -27,18 +28,21 @@ jobs: ref: ${{ github.event.pull_request.head.sha || github.sha }} path: dash fetch-depth: 0 + persist-credentials: false - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - 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 @@ -70,6 +74,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, arm-linux-gnueabihf, aarch64-linux-gnu, riscv64-linux-gnu, powerpc64-linux-gnu, x86_64-w64-mingw32, x86_64-apple-darwin, arm64-apple-darwin] @@ -86,6 +94,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha || github.sha }} path: dash fetch-depth: 0 + persist-credentials: false - name: Cache depends sources uses: actions/cache@v4 @@ -115,14 +124,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: | @@ -133,8 +147,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@v4 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 78b825aa8254e..7fbb42a65e09d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,6 +21,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 50 + persist-credentials: false - name: Initial setup run: | @@ -29,15 +30,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" @@ -47,8 +51,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 7fc2e22a87053..e797d65f9c4c1 100644 --- a/.github/workflows/merge-check.yml +++ b/.github/workflows/merge-check.yml @@ -17,6 +17,7 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 0 + persist-credentials: false - name: Set up Git run: | @@ -24,20 +25,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/predict-conflicts.yml b/.github/workflows/predict-conflicts.yml index 356ec7fcce68b..b0e53201f7cc1 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@v4 + 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 58f506860498d..ae76ec87c1c8c 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 f4e1775a78593..dde34f36a422f 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@v3 + with: + persist-credentials: false - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -28,17 +33,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@v6 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 +88,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 4e5c9e59de963..898e2ada9b51e 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 c0a9e5e58289d..7b5e161749f1c 100644 --- a/.github/workflows/test-src.yml +++ b/.github/workflows/test-src.yml @@ -33,6 +33,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 1 + persist-credentials: false - name: Download build artifacts uses: actions/download-artifact@v4 @@ -49,10 +50,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 @@ -62,8 +64,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}" diff --git a/contrib/containers/ci/ci-slim.Dockerfile b/contrib/containers/ci/ci-slim.Dockerfile index 4abab253573fa..476c893b29a6d 100644 --- a/contrib/containers/ci/ci-slim.Dockerfile +++ b/contrib/containers/ci/ci-slim.Dockerfile @@ -80,7 +80,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 diff --git a/test/lint/lint-github-actions.py b/test/lint/lint-github-actions.py new file mode 100755 index 0000000000000..09bda871ac037 --- /dev/null +++ b/test/lint/lint-github-actions.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# +# 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. + +""" +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(' disable: true') + + # Add ignored findings + for audit, locations in IGNORED.items(): + lines.append(f' {audit}:') + lines.append(' 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()