← back to Cli Printing Press

.github/workflows/verify-supply-chain.yml

86 lines

name: Verify supply chain

# Mirror of the supply-chain hardening gate from
# mvanhorn/printing-press-library/.github/workflows/verify-supply-chain.yml
# (2026-05-17). Catches the workflow-trust and Go-module-env attack shapes
# in PRs against this generator repo.
#
# Scope adapted: this repo doesn't have a library/**/go.mod surface, an
# npm wrapper, or published-CLI module paths, so signals for those are
# omitted. See .github/scripts/verify-supply-chain/signals.py for the
# rationale.
#
# IMPORTANT — scan integrity: this workflow checks out the BASE branch
# (not the PR head) so the scan.py / signals.py that execute are the
# trusted base versions, not whatever the PR author shipped. The PR head
# is fetched separately and passed via --head-ref. Prevents a PR from
# weakening the scanner in the same change that introduces an attack
# payload — relevant once promoted to a required gate.
#
# Informational on landing — promote to a required check via branch
# protection only after a one-week green window.

on:
  pull_request:
    paths:
      - '.github/workflows/**'
      - '.github/scripts/verify-supply-chain/**'
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: verify-supply-chain-${{ github.event.pull_request.number || github.run_id }}
  cancel-in-progress: true

jobs:
  scan:
    name: Scan
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      # Checkout the BASE branch from the base repo — never the PR head.
      - uses: actions/checkout@v6
        with:
          repository: ${{ github.repository }}
          ref: ${{ github.base_ref || github.ref }}
          fetch-depth: 0
          persist-credentials: false

      - name: Run supply-chain scan
        if: github.event_name == 'pull_request'
        env:
          HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
          HEAD_SHA: ${{ github.event.pull_request.head.sha }}
        run: |
          set -euo pipefail

          git remote add pr "https://github.com/${HEAD_REPO}.git" 2>/dev/null || git remote set-url pr "https://github.com/${HEAD_REPO}.git"
          git fetch --no-tags --depth=200 pr "${HEAD_SHA}"

          # Deletion guard: refuse to proceed if the PR deletes scanner
          # files (closes the staged two-step attack: delete scanner →
          # re-introduce a patched one that whitelists payload).
          deleted_scanner=$(git diff --name-status --diff-filter=D HEAD "${HEAD_SHA}" -- .github/scripts/verify-supply-chain/ || true)
          if [ -n "${deleted_scanner}" ]; then
            echo "::error::PR deletes supply-chain scanner files. Removing or relocating the scanner requires explicit out-of-band review — automated gate refuses to proceed. Files: ${deleted_scanner}"
            exit 1
          fi

          # Bootstrap case: base has no scanner yet (this PR introduces it).
          # Restore scanner from PR head; no-op after merge. The deletion
          # guard above ensures only the genuine first-introduction case
          # reaches this branch.
          if [ ! -f .github/scripts/verify-supply-chain/scan.py ]; then
            echo "::notice::Base branch has no scanner — bootstrap mode, restoring scanner from PR head ${HEAD_SHA}."
            mkdir -p .github/scripts/verify-supply-chain
            git restore --source="${HEAD_SHA}" --worktree -- .github/scripts/verify-supply-chain/
          fi

          python3 .github/scripts/verify-supply-chain/scan.py --base-ref HEAD --head-ref "${HEAD_SHA}"

      - name: Run supply-chain scan (manual dispatch)
        if: github.event_name == 'workflow_dispatch'
        run: python3 .github/scripts/verify-supply-chain/scan.py --base-ref origin/main