← 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