From 54d17ac29f8c6afefa6e59b98a6cd4dd8c141def Mon Sep 17 00:00:00 2001 From: Aaron Stainback Date: Tue, 28 Apr 2026 14:46:28 -0400 Subject: [PATCH] fix(scorecard): scope budget-cadence permissions job-level (TokenPermissionsID) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scorecard TokenPermissionsID alert (#26, high severity) flagged top-level 'contents: write' on budget-snapshot-cadence.yml. This is a Scorecard best-practice violation — top-level should be read-only, with write scoped narrowly to jobs that need it. Refactor: - Top-level permissions block: 'contents: read' only - Job-level (jobs.snapshot.permissions): contents:write + pull-requests:write + actions:read (the original set, just moved) Functional behavior: identical. The snapshot job still gets all 3 write/read scopes it needs. Security posture: tightened. If any step in this workflow runs untrusted input, the blast radius is bounded to the snapshot job rather than the whole workflow. EVIDENCE-BASED: - VERIFIED: Scorecard alert message specifies "topLevel 'contents' permission set to 'write'" — matches the diagnosis. - VERIFIED: GitHub Actions job-level permissions override top-level per docs (https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token). Side-effect: should close TokenPermissionsID alert on next CodeQL/ Scorecard scan, which unblocks PR #661 (B-0073 CodeQL unblock — gated by 'code_quality:severity=all' ruleset that requires zero open alerts). Co-Authored-By: Claude Opus 4.7 --- .github/workflows/budget-snapshot-cadence.yml | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/.github/workflows/budget-snapshot-cadence.yml b/.github/workflows/budget-snapshot-cadence.yml index c468141d3d..c8172ff790 100644 --- a/.github/workflows/budget-snapshot-cadence.yml +++ b/.github/workflows/budget-snapshot-cadence.yml @@ -75,18 +75,10 @@ on: default: "" permissions: - # Need contents:write to push the snapshot branch; pull-requests:write - # to open the snapshot PR; actions:read so snapshot-burn.sh can call - # the Actions REST endpoints (/repos/.../actions/runs and - # /actions/runs/{id}/timing) to populate burn metrics. Without - # actions:read, those API calls 403 silently and snapshot-burn.sh - # falls back to empty/zeroed timing data while still writing a - # snapshot — producing misleading evidence rather than a hard - # failure. With explicit workflow permissions, omitted scopes are - # `none`, so actions:read MUST be listed explicitly here. - contents: write - pull-requests: write - actions: read + # Top-level: read-only by default (per Scorecard TokenPermissions + # best-practice — minimize blast radius if any step is compromised). + # The snapshot job below scopes write permissions narrowly. + contents: read concurrency: # Only one cadence run at a time. Retriggers queue (rather than @@ -101,6 +93,20 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 5 + permissions: + # Need contents:write to push the snapshot branch; pull-requests:write + # to open the snapshot PR; actions:read so snapshot-burn.sh can call + # the Actions REST endpoints (/repos/.../actions/runs and + # /actions/runs/{id}/timing) to populate burn metrics. Without + # actions:read, those API calls 403 silently and snapshot-burn.sh + # falls back to empty/zeroed timing data while still writing a + # snapshot — producing misleading evidence rather than a hard + # failure. With explicit job permissions, omitted scopes are + # `none`, so actions:read MUST be listed explicitly here. + contents: write + pull-requests: write + actions: read + steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2