fix(GHA002): establish need before calling a write grant excessive - #73
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
ActionPermissionRequirementsholds 24 actions and the scopes they need. Conditional entries carry their condition —dependency-review-actionneedspull-requests: writeonly whencomment-summary-in-practually asks it to comment, which the parsed step inputs already expose.write-allA 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.
contents,packages,actions,attestations: code and artefacts an attacker can make others runsecurity-events,checks,statuses: signal an attacker can suppress but not act throughNot 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:
github/codeql-action-mirrorcannot borrowgithub/codeql-action's exemptioncontents: writebeside CodeQL is still reportedwrite-allis never excusedEvery exemption has a paired negative test.
Exemptions deleted
RepositoryWorkflowsTestsfails when an accepted finding stops appearing, and two of ours now do.codeql.ymlanddependency-review.ymlare recognised by the rule itself, so their entries are gone.prune-runs.ymlkeeps its entry:actions: writeis required by arun: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