mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Enforce workflow execution trust boundaries
This commit is contained in:
@@ -1,5 +1,17 @@
|
||||
# GitHub Actions Workflows
|
||||
|
||||
## Trust contract
|
||||
|
||||
`scripts/check_workflow_trust.py` validates every workflow during the script
|
||||
smoke suite. Remote actions and reusable workflows must use full commit SHAs,
|
||||
container actions must use SHA-256 digests, and GitHub-hosted runners must use
|
||||
dated image labels rather than moving `-latest` aliases.
|
||||
|
||||
Every `actions/checkout` step must also set `persist-credentials` explicitly.
|
||||
Use `false` unless a later command in the same job performs an authenticated
|
||||
Git write. The small number of write-path exceptions use `true` with the
|
||||
machine-checked `# required: authenticated git writes` rationale.
|
||||
|
||||
## Issue Triage Automation
|
||||
|
||||
**Files**:
|
||||
|
||||
@@ -24,6 +24,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
|
||||
@@ -34,6 +34,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install gitleaks
|
||||
@@ -72,6 +73,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Classify changed files
|
||||
@@ -110,6 +112,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
@@ -179,6 +183,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
@@ -228,6 +234,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
# test_gitleaks_ignore.py resolves historical fingerprints against
|
||||
# branch- and tag-reachable commits, so it needs full history.
|
||||
fetch-depth: 0
|
||||
@@ -258,6 +265,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
|
||||
@@ -255,6 +255,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
@@ -356,6 +358,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
@@ -644,6 +648,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate candidate identity
|
||||
|
||||
@@ -26,6 +26,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
path: repos/pulse
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ jobs:
|
||||
- name: Check out triage helper
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
.github/scripts/issue-version-triage.cjs
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
@@ -40,6 +40,7 @@ jobs:
|
||||
- name: Checkout exact release source
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
ref: ${{ inputs.source_sha }}
|
||||
|
||||
|
||||
@@ -139,6 +139,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Resolve required release branch
|
||||
@@ -303,6 +304,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
@@ -334,6 +337,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
@@ -369,6 +374,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
@@ -404,6 +411,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download verified frontend bundle
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
@@ -440,6 +449,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
@@ -603,6 +614,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
@@ -700,6 +713,7 @@ jobs:
|
||||
if: ${{ needs.prepare.outputs.visual_capture_count != '0' }}
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install browser capture runtime
|
||||
@@ -751,6 +765,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: true # required: authenticated git writes
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download immutable release candidate
|
||||
@@ -1182,6 +1197,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
@@ -1565,6 +1582,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout release integrity control
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Publish the fully staged release
|
||||
env:
|
||||
@@ -1889,6 +1908,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout release integrity control
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Enforce irreversible release commit outcome
|
||||
env:
|
||||
|
||||
@@ -36,6 +36,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
|
||||
@@ -26,6 +26,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0
|
||||
|
||||
@@ -151,7 +151,7 @@ jobs:
|
||||
ref: gh-pages
|
||||
path: gh-pages
|
||||
fetch-depth: 1
|
||||
persist-credentials: true
|
||||
persist-credentials: true # required: authenticated git writes
|
||||
|
||||
- name: Publish chart release and merge Pages index
|
||||
env:
|
||||
|
||||
@@ -101,6 +101,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository (for README key extraction)
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Resolve smoke inputs
|
||||
id: inputs
|
||||
|
||||
@@ -25,6 +25,7 @@ jobs:
|
||||
- name: Check out triage helper
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
.github/scripts/issue-version-triage.cjs
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
@@ -26,6 +26,7 @@ jobs:
|
||||
- name: Check out triage helper
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
.github/scripts/issue-version-triage.cjs
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
@@ -52,6 +52,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout pinned source
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
|
||||
@@ -43,6 +43,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
|
||||
@@ -65,6 +65,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
|
||||
@@ -27,10 +27,12 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate public documentation
|
||||
run: python3 scripts/check_public_docs.py
|
||||
|
||||
@@ -56,6 +56,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
@@ -239,6 +240,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout release verification control
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
|
||||
@@ -87,6 +87,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ jobs:
|
||||
- name: Checkout exact recovery implementation
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout release control
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Qualify the quarantined source run
|
||||
id: qualify
|
||||
|
||||
@@ -58,6 +58,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout release integrity control
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Wait for verified public activation marker
|
||||
id: marker
|
||||
@@ -211,7 +213,7 @@ jobs:
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
persist-credentials: true
|
||||
persist-credentials: true # required: authenticated git writes
|
||||
|
||||
- name: Acquire repository-ref lease
|
||||
id: acquire
|
||||
@@ -465,7 +467,7 @@ jobs:
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
persist-credentials: true
|
||||
persist-credentials: true # required: authenticated git writes
|
||||
|
||||
- name: Release owned repository-ref lease
|
||||
env:
|
||||
|
||||
@@ -121,6 +121,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Resolve required release branch
|
||||
|
||||
@@ -13,6 +13,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install boundary audit dependencies
|
||||
run: |
|
||||
|
||||
@@ -26,6 +26,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
@@ -57,6 +59,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
|
||||
@@ -27,6 +27,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
|
||||
@@ -42,6 +42,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
@@ -81,6 +83,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
@@ -232,6 +236,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
|
||||
@@ -74,6 +74,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
@@ -178,6 +180,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
|
||||
@@ -130,6 +130,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
@@ -272,6 +273,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
|
||||
@@ -72,6 +72,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Determine release context
|
||||
id: context
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fail closed when GitHub Actions trust inputs become mutable or implicit."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ACTION_SHA_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
CONTAINER_DIGEST_RE = re.compile(r"^docker://.+@sha256:[0-9a-f]{64}$")
|
||||
HOSTED_LATEST_RE = re.compile(r"\b(?:ubuntu|windows|macos)-latest\b")
|
||||
USES_RE = re.compile(r"^\s*(?:-\s*)?uses:\s*([^\s#]+)")
|
||||
CHECKOUT_PREFIX = "actions/checkout@"
|
||||
WRITE_CREDENTIAL_RATIONALE = "# required: authenticated git writes"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Finding:
|
||||
path: Path
|
||||
line: int
|
||||
message: str
|
||||
|
||||
def render(self) -> str:
|
||||
return f"{self.path}:{self.line}: {self.message}"
|
||||
|
||||
|
||||
def _indent(line: str) -> int:
|
||||
return len(line) - len(line.lstrip())
|
||||
|
||||
|
||||
def _checkout_block(lines: list[str], uses_index: int) -> list[tuple[int, str]]:
|
||||
"""Return lines belonging to the checkout step after its uses declaration."""
|
||||
uses_indent = _indent(lines[uses_index])
|
||||
block: list[tuple[int, str]] = []
|
||||
for index in range(uses_index + 1, len(lines)):
|
||||
line = lines[index]
|
||||
stripped = line.strip()
|
||||
if stripped and (
|
||||
_indent(line) < uses_indent
|
||||
or (_indent(line) == uses_indent and stripped.startswith("- "))
|
||||
):
|
||||
break
|
||||
block.append((index, line))
|
||||
return block
|
||||
|
||||
|
||||
def audit_workflow(path: Path) -> list[Finding]:
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
findings: list[Finding] = []
|
||||
|
||||
for index, line in enumerate(lines):
|
||||
line_number = index + 1
|
||||
code = line.split("#", 1)[0]
|
||||
if HOSTED_LATEST_RE.search(code):
|
||||
findings.append(
|
||||
Finding(
|
||||
path,
|
||||
line_number,
|
||||
"mutable hosted runner label; use an explicit dated image",
|
||||
)
|
||||
)
|
||||
|
||||
match = USES_RE.search(code)
|
||||
if not match:
|
||||
continue
|
||||
dependency = match.group(1).strip("'\"")
|
||||
if dependency.startswith("./"):
|
||||
continue
|
||||
if dependency.startswith("docker://"):
|
||||
if not CONTAINER_DIGEST_RE.fullmatch(dependency):
|
||||
findings.append(
|
||||
Finding(
|
||||
path,
|
||||
line_number,
|
||||
"container action is not pinned to a sha256 digest",
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
owner_and_action, separator, ref = dependency.rpartition("@")
|
||||
if not separator or "/" not in owner_and_action or not ACTION_SHA_RE.fullmatch(ref):
|
||||
findings.append(
|
||||
Finding(
|
||||
path,
|
||||
line_number,
|
||||
"remote action or reusable workflow is not pinned to a full commit SHA",
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
# GitHub repository names are case-insensitive, so normalize before
|
||||
# applying checkout-specific credential controls.
|
||||
if not dependency.lower().startswith(CHECKOUT_PREFIX):
|
||||
continue
|
||||
credential_settings = [
|
||||
(block_index, block_line)
|
||||
for block_index, block_line in _checkout_block(lines, index)
|
||||
if re.match(r"^\s*persist-credentials\s*:", block_line)
|
||||
]
|
||||
if len(credential_settings) != 1:
|
||||
findings.append(
|
||||
Finding(
|
||||
path,
|
||||
line_number,
|
||||
"checkout must set persist-credentials explicitly exactly once",
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
setting_index, setting = credential_settings[0]
|
||||
value_match = re.match(
|
||||
r"^\s*persist-credentials\s*:\s*(true|false)\b", setting
|
||||
)
|
||||
if not value_match:
|
||||
findings.append(
|
||||
Finding(
|
||||
path,
|
||||
setting_index + 1,
|
||||
"persist-credentials must be the literal true or false",
|
||||
)
|
||||
)
|
||||
elif value_match.group(1) == "true" and WRITE_CREDENTIAL_RATIONALE not in setting:
|
||||
findings.append(
|
||||
Finding(
|
||||
path,
|
||||
setting_index + 1,
|
||||
f"persisted checkout credentials require {WRITE_CREDENTIAL_RATIONALE}",
|
||||
)
|
||||
)
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def audit_directory(workflow_directory: Path) -> list[Finding]:
|
||||
findings: list[Finding] = []
|
||||
paths = sorted(workflow_directory.glob("*.yml")) + sorted(
|
||||
workflow_directory.glob("*.yaml")
|
||||
)
|
||||
for path in paths:
|
||||
findings.extend(audit_workflow(path))
|
||||
return findings
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"workflow_directory",
|
||||
nargs="?",
|
||||
type=Path,
|
||||
default=Path(__file__).resolve().parents[1] / ".github" / "workflows",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
if not args.workflow_directory.is_dir():
|
||||
print(f"workflow directory not found: {args.workflow_directory}", file=sys.stderr)
|
||||
return 2
|
||||
findings = audit_directory(args.workflow_directory)
|
||||
if findings:
|
||||
for finding in findings:
|
||||
print(finding.render(), file=sys.stderr)
|
||||
print(f"GitHub Actions trust validation failed ({len(findings)} finding(s)).", file=sys.stderr)
|
||||
return 1
|
||||
print("GitHub Actions trust validation passed.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for the GitHub Actions trust contract."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
MODULE_PATH = REPO_ROOT / "scripts" / "check_workflow_trust.py"
|
||||
SPEC = importlib.util.spec_from_file_location("check_workflow_trust", MODULE_PATH)
|
||||
assert SPEC and SPEC.loader
|
||||
workflow_trust = importlib.util.module_from_spec(SPEC)
|
||||
sys.modules[SPEC.name] = workflow_trust
|
||||
SPEC.loader.exec_module(workflow_trust)
|
||||
|
||||
PIN = "a" * 40
|
||||
DIGEST = "b" * 64
|
||||
|
||||
|
||||
class WorkflowTrustTest(unittest.TestCase):
|
||||
def audit(self, content: str) -> list[str]:
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
path = Path(temporary_directory) / "test.yml"
|
||||
path.write_text(content, encoding="utf-8")
|
||||
return [finding.message for finding in workflow_trust.audit_workflow(path)]
|
||||
|
||||
def test_accepts_immutable_dependencies_and_explicit_checkout_credentials(self) -> None:
|
||||
findings = self.audit(
|
||||
f"""jobs:
|
||||
test:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@{PIN}
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: owner/action/path@{PIN} # v1
|
||||
- uses: docker://example/image@sha256:{DIGEST}
|
||||
- uses: ./.github/workflows/local.yml
|
||||
"""
|
||||
)
|
||||
self.assertEqual(findings, [])
|
||||
|
||||
def test_rejects_mutable_action_runner_and_container_references(self) -> None:
|
||||
findings = self.audit(
|
||||
"""jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: owner/action@v2
|
||||
- uses: docker://alpine:3.22
|
||||
"""
|
||||
)
|
||||
self.assertTrue(any("mutable hosted runner" in finding for finding in findings))
|
||||
self.assertTrue(any("full commit SHA" in finding for finding in findings))
|
||||
self.assertTrue(any("sha256 digest" in finding for finding in findings))
|
||||
|
||||
def test_rejects_implicit_or_unjustified_checkout_credentials(self) -> None:
|
||||
omitted = self.audit(
|
||||
f"""steps:
|
||||
- uses: actions/checkout@{PIN}
|
||||
"""
|
||||
)
|
||||
self.assertTrue(any("must set persist-credentials" in finding for finding in omitted))
|
||||
|
||||
unjustified = self.audit(
|
||||
f"""steps:
|
||||
- uses: actions/checkout@{PIN}
|
||||
with:
|
||||
persist-credentials: true
|
||||
"""
|
||||
)
|
||||
self.assertTrue(any("require # required" in finding for finding in unjustified))
|
||||
|
||||
mixed_case = self.audit(
|
||||
f"""steps:
|
||||
- uses: Actions/Checkout@{PIN}
|
||||
"""
|
||||
)
|
||||
self.assertTrue(
|
||||
any("must set persist-credentials" in finding for finding in mixed_case)
|
||||
)
|
||||
|
||||
def test_accepts_documented_authenticated_git_write(self) -> None:
|
||||
findings = self.audit(
|
||||
f"""steps:
|
||||
- uses: actions/checkout@{PIN}
|
||||
with:
|
||||
persist-credentials: true # required: authenticated git writes
|
||||
"""
|
||||
)
|
||||
self.assertEqual(findings, [])
|
||||
|
||||
def test_repository_workflows_satisfy_contract(self) -> None:
|
||||
findings = workflow_trust.audit_directory(REPO_ROOT / ".github" / "workflows")
|
||||
self.assertEqual([finding.render() for finding in findings], [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user