Skip to content

fix(GHA002): establish need before calling a write grant excessive - #73

Merged
bgard68 merged 1 commit into
mainfrom
claude/gha002-required-permissions
Aug 25, 2026
Merged

fix(GHA002): establish need before calling a write grant excessive#73
bgard68 merged 1 commit into
mainfrom
claude/gha002-required-permissions

Conversation

@bgard68

@bgard68 bgard68 commented Aug 25, 2026

Copy link
Copy Markdown
Owner

GHA002 reported every write permission, which made a correct configuration indistinguishable from a dangerous one.

A CodeQL job holding the one scope it cannot upload results without scored the same High as a job able to push to the default branch. Worse, the rule's own remediation — "grant only the specific write permission required by the job" — was already satisfied by the config it was flagging, so following the advice meant breaking code scanning.

The evidence was already in this repo: three GHA002 findings on our own workflows carried hand-written exemptions, each explaining the grant was the documented minimum. A rule that needs three exceptions for correct configurations is describing something the rule should know.

What changed

What an action requires is public, static and documented, so it is a lookup rather than an inference and the rule stays deterministic. ActionPermissionRequirements holds 24 actions and the scopes they need. Conditional entries carry their condition — dependency-review-action needs pull-requests: write only when comment-summary-in-pr actually asks it to comment, which the parsed step inputs already expose.

Situation Before After
Job-scoped write an action in that job requires High not reported
Workflow-scoped write some job requires High Low — "move it to that job"
Write nothing requires High reported, severity by scope
write-all High High, unchanged

A workflow-scoped grant is still reported even when a job needs it, because it reaches every job including ones added later — but the remediation changes from remove to move, which is the action that actually applies.

Severity now follows blast radius

It was constant across every scope, which flattens a real difference and teaches readers to skim the band that matters.

  • Highcontents, packages, actions, attestations: code and artefacts an attacker can make others run
  • Medium — everything else, including scopes GitHub adds after this table was written
  • Lowsecurity-events, checks, statuses: signal an attacker can suppress but not act through

Not a blanket exemption

An entry that is absent costs a false positive; an entry that is wrong silently suppresses a real finding. The table is built for the second risk:

  • prefix matching stops at the path separator, so github/codeql-action-mirror cannot borrow github/codeql-action's exemption
  • a required scope excuses only itself — contents: write beside CodeQL is still reported
  • an unrecognised scope stays Medium rather than falling through unreported
  • a reusable-workflow call justifies nothing, since its steps are not visible here
  • write-all is never excused

Every exemption has a paired negative test.

Exemptions deleted

RepositoryWorkflowsTests fails when an accepted finding stops appearing, and two of ours now do. codeql.yml and dependency-review.yml are recognised by the rule itself, so their entries are gone. prune-runs.yml keeps its entry: actions: write is required by a run: step no action declares, which is exactly the judgement a table cannot encode.

Verification

298 tests pass across the solution, 18 of them new. Run against a real repository's nine workflows — the one that prompted this — GHA002 goes from 1 unfixable finding to clean, with no other rule's output changed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01N7HjzX5e8Wv7PKENDo2Uj7


Generated by Claude Code

The rule reported every write permission, which made a correct configuration
indistinguishable from a dangerous one. A CodeQL job holding the single scope
it cannot upload results without scored the same High as a job able to push to
the default branch, and its remediation — "grant only the specific write
permission required by the job" — was already satisfied by the config it was
flagging. Following the advice would have broken code scanning.

The evidence was in our own test suite: three GHA002 findings on this
repository's workflows carried hand-written exemptions, each explaining that
the grant was the documented minimum.

What an action requires is public, static and documented, so it is a lookup,
not an inference, and the rule stays deterministic. ActionPermissionRequirements
holds 24 actions and the scopes they need, with conditional entries carrying
their condition — dependency-review needs pull-requests: write only when
comment-summary-in-pr asks it to comment.

Now:
  - job-scoped write an action in that job requires -> not reported
  - workflow-scoped write some job requires -> Low, "move it to that job",
    since a workflow grant reaches jobs that have no use for it
  - write nothing requires -> reported, as before
  - write-all -> reported whatever the job runs

Severity follows what the scope can do once a token is stolen, instead of being
constant: contents/packages/actions High, security-events/checks/statuses Low,
the rest Medium. An unrecognised scope stays Medium so a scope GitHub adds
later is still reported, and prefix matching stops at the path separator so
codeql-action-mirror cannot borrow codeql-action's exemption.

Two exemptions in RepositoryWorkflowsTests are deleted: codeql.yml and
dependency-review.yml are now recognised by the rule itself. prune-runs.yml
keeps its entry — actions: write is required by a run: step no action declares,
which is exactly the judgement a table cannot encode.

18 new tests pin the boundary in both directions; 298 pass across the solution.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7HjzX5e8Wv7PKENDo2Uj7
@bgard68
bgard68 merged commit 202dd16 into main Aug 25, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants