Skip to content

Minimally scope permissions in GitHub Actions workflows#549

Draft
desrosj wants to merge 2 commits into
mainfrom
add/scoped-workflow-permissions
Draft

Minimally scope permissions in GitHub Actions workflows#549
desrosj wants to merge 2 commits into
mainfrom
add/scoped-workflow-permissions

Conversation

@desrosj

@desrosj desrosj commented May 14, 2026

Copy link
Copy Markdown
Member

This updates the GitHub Actions workflow files to:

  • Grant minimally-scoped permissions to each job to adhere to the principle of least privilege
  • Specify a timeout on each job to prevent runaway processes consuming too many minutes (the default is 360)

Once this PR is merged, the Settings -> Actions -> Workflow permissions setting can be changed by a repo admin to "Read repository contents and packages permissions".

For more information, see PRESS11-470.

References

Use of AI

Cursor was used with (Claude Opus 4.7 and Composer 2.0 at varying points) to analyze the repository and make the initial changes.

When this PR is marked "ready for review" it means that I have manually reviewed all permissions and timeouts that were changed and made any necessary adjustments.

As a part of the analysis, the following summary was created:

Status

Field Value
Workflows scanned 8 (auto-translate.yml, brand-plugin-test-playwright.yml, codecoverage-main.yml, i18n-crowdin-download.yml, i18n-crowdin-upload.yml, lint.yml, newfold-prep-release.yml, satis-update.yml)
Verification glob **/wp-module-patterns/.github/workflows/*.yml — 8 YAML files matched; zero *.yaml files in .github/workflows
Branch add/scoped-workflow-permissions not created — no remediation applied (audit-only; no edits)
Permissions commit none
Timeouts commit none

Top-level permissions: {}

Category Entry
Workflows that were missing the top-level permissions: {} directive (added in this run): Not applicable — no changes committed. The following workflows are non-compliant with the required # Disable permissions … comment pair immediately followed by permissions: {} immediately before jobs: (some have no equivalent block; others place non-empty workflow permissions: or omit the mandated comments):
Workflows that were missing the top-level permissions: {} directive (added in this run): satis-update.yml
Workflows that were missing the top-level permissions: {} directive (added in this run): lint.yml
Workflows that were missing the top-level permissions: {} directive (added in this run): codecoverage-main.yml (workflow-level contents: read instead of {} + mandated comments before jobs:)
Workflows that were missing the top-level permissions: {} directive (added in this run): brand-plugin-test-playwright.yml (workflow-level contents: read; comments/{} pattern missing)
Workflows that were missing the top-level permissions: {} directive (added in this run): i18n-crowdin-upload.yml
Workflows that were missing the top-level permissions: {} directive (added in this run): i18n-crowdin-download.yml (workflow-level contents: write / pull-requests: write instead of {} + mandated comments)
Workflows that were missing the top-level permissions: {} directive (added in this run): auto-translate.yml (permissions: {} present but without the two mandated comment lines before it)
Compliant with the required top-level pattern: newfold-prep-release.yml

Job-level permissions: additions

Workflow file Summary Job Permissions / notes
Not applicable — no changes committed. Jobs that lack a permissions: block immediately after runs-on: / uses: (or need tightening to least privilege with per-permission trailing comments as required by the audit policy): satis-update.yml: 1 job — webhook
Likely permissions: {} for the default GITHUB_TOKEN [3] — uses peter-evans/repository-dispatch with secrets.WEBHOOK_TOKEN (not github.token); no actions/checkout step.
lint.yml: 1 job — phpcs
Add contents: read [3] — actions/checkout needs clone access in a private repo.
codecoverage-main.yml: 1 job — get-repo-name
Add explicit permissions: {} [3] — no checkout / no GitHub API use in shown steps.
codecoverage-main.yml: job codecoverage already declares contents: write and pull-requests: write but without the required trailing inline comment on each key (should be brought in line with the audit’s “comment per permission” rule).
brand-plugin-test-playwright.yml: 2 jobs — setup, bluehost
setup: likely permissions: {} [3] — branch name echo only.
bluehost: at minimum contents: read for typical reusable clone patterns [2] — exact needs depend on newfold-labs/workflows module-plugin-test-playwright.yml@main (not fully verified from this repo).
i18n-crowdin-upload.yml: 1 job — call-crowdin-upload-workflow
Missing job permissions: entirely [2] — scope should be derived from newfold-labs/workflows i18n-crowdin-upload.yml@main (Crowdin + any PR/push behavior).
i18n-crowdin-download.yml: 1 job — call-crowdin-workflow
Move workflow-level contents: write / pull-requests: write to this job and add per-permission trailing comments [2] — behavior depends on reusable workflow (not fully verified from this repo).
auto-translate.yml: job translate already has write scopes but without per-permission trailing comments (should be aligned with the audit format).

Permissions corrections (previously incorrect)

# Correction
1 codecoverage-main.yml :: workflow: BEFORE permissions: contents: read (top-level) -> AFTER top-level permissions: {} plus job-scoped permissions (with get-repo-name explicit contents: read only if later steps add checkout/API use) — reason: audit requires default-deny at workflow level and least privilege on jobs — [3]
2 brand-plugin-test-playwright.yml :: workflow: BEFORE permissions: contents: read (top-level) -> AFTER top-level permissions: {} plus job-level scoped permissions — reason: same — [3]
3 i18n-crowdin-download.yml :: workflow: BEFORE contents: write + pull-requests: write at workflow level -> AFTER top-level permissions: {} and the same scopes on the job that calls the reusable workflow (with inline comments) — reason: workflow-wide elevation violates the audit’s default-deny pattern — [3]
4 auto-translate.yml :: workflow: BEFORE permissions: {} without mandated comment block -> AFTER two-line comment block + permissions: {} before jobs: — reason: audit mandates the exact preamble — [3]
5 newfold-prep-release.yml :: prep-release: BEFORE contents: write / pull-requests: write without per-line comments -> AFTER same keys with trailing comments (e.g. push branch, open PR) — reason: matches audit’s required comment style — [2]

timeout-minutes additions

Workflow Job Minutes / action Rationale
No job in any workflow currently sets timeout-minutes (repo-wide grep in .github/workflows: no matches). Suggested additions (not applied; pick values to match expected runtime — often 30 minutes):

Notes / blockers

# Note
1 Permissions for jobs that only uses: reusable workflows (newfold-labs/workflows@main) should be validated against those reusable workflow definitions plus GitHub workflow permissions documentation; this audit infers scopes from naming and typical patterns where the callee was not fetched.
2 Third-party actions in lint.yml (technote-space/get-diff-action, shivammathur/setup-php, actions/cache) were not traced line-by-line for default GITHUB_TOKEN usage beyond actions/checkout — if any step elevates implicitly, tighten job permissions accordingly after verification.
3 satis-update.yml: secrets.WEBHOOK_TOKEN is a PAT/application token to another repo; rotate/review secrecy separately from GITHUB_TOKEN hardening — still apply default workflow permissions: {} + explicit job scopes for completeness.
4 [REDACTED]

@desrosj desrosj self-assigned this May 14, 2026
@desrosj desrosj requested a review from Copilot May 14, 2026 23:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR tightens GitHub Actions security posture by default-denying GITHUB_TOKEN permissions at the workflow level and explicitly granting only the minimal permissions needed per job, while also adding job timeouts to prevent runaway CI usage.

Changes:

  • Added workflow-level permissions: {} (default deny) across workflows and moved required permissions to job scope.
  • Tightened/annotated job-level permissions for jobs that call reusable workflows (e.g., translations, Crowdin, code coverage, prep-release).
  • Added timeout-minutes to every job to cap runtime.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
.github/workflows/satis-update.yml Adds default-deny permissions and a job timeout for the webhook dispatch workflow.
.github/workflows/newfold-prep-release.yml Adds job timeout and documents job-level write permissions needed by the reusable release workflow.
.github/workflows/lint.yml Adds default-deny permissions, job-scoped permissions, and a timeout for PHPCS linting.
.github/workflows/i18n-crowdin-upload.yml Adds default-deny permissions, job-scoped contents: read, and a timeout for the reusable Crowdin upload workflow.
.github/workflows/i18n-crowdin-download.yml Moves elevated permissions off the workflow to job scope (default deny at workflow), and adds a timeout.
.github/workflows/codecoverage-main.yml Default-denies workflow permissions, scopes job permissions, and adds timeouts for both jobs.
.github/workflows/brand-plugin-test-playwright.yml Default-denies workflow permissions, scopes job permissions, and adds timeouts for setup and reusable Playwright test job.
.github/workflows/auto-translate.yml Keeps default-deny at workflow level, documents job write permissions, and adds a job timeout.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

phpcs:
name: Run PHP Code Sniffer
runs-on: ubuntu-latest
permissions:
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