From 6f1cfffdd79c145c3d9b3799c12fc4f9639c29ba Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 17:58:22 +0100 Subject: [PATCH] fix(security): Harden CI permissions (#1454) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial plan * chore(security): harden workflow permissions and pin Docker base image digests Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/2ddc8f2b-7355-48cf-9a0b-c06df66c3f47 * fix(security): restore permissions: {} on publish + release-candidate workflows These two release-publishing workflows had permissions: {} (the strictest valid form) before PR #1454, which replaced it with permissions: read-all. Every job in both files already declares its own permissions block, so the workflow-level default is only the safety net for future jobs added without one — read-all weakens that net for no benefit. Restore {} and the explanatory comment. Scorecard's TokenPermissions check accepts both forms, so this preserves U9 compliance. * fix(security): narrow permissions: read-all to contents: read on 13 workflows PR #1454 added permissions: read-all to 13 workflows that previously had no top-level permissions block. read-all is Scorecard-compliant but unnecessarily broad — every job in scope only needs contents:read at the workflow level (job-level blocks already grant the writes that any job actually performs). Snapshot of every job in the 13 workflows confirms contents:read is sufficient: - ci.yml: quality/tests/scope-parity have explicit contents:read job blocks; save-pr-meta uses upload-artifact only (no token scopes needed); ci-status is pure shell. - ci-e2e.yml, ci-quality.yml, ci-scope-parity.yml, ci-tests.yml: all jobs do checkout + npm + tsc/vitest/playwright/upload-artifact only; no API token scopes required. - claude.yml, codeql.yml, dependency-review.yml, docker.yml, gitleaks.yml, pr-labeler.yml, trivy.yml, workflow-lint.yml: all jobs already declare their own job-level blocks (security-events:write, pull-requests:write, packages:write, etc.) so the workflow-level default does not gate them. zizmor (--min-severity high) is clean on the resulting tree. Pre-existing medium findings (secrets-inherit, artipacked) are in unrelated workflows and untouched by this commit. scorecard.yml also uses read-all but pre-existed PR #1454 and is deferred to a follow-up PR per the plan's scope boundary. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Gergő Magyar --- .github/workflows/ci-e2e.yml | 3 +++ .github/workflows/ci-quality.yml | 3 +++ .github/workflows/ci-scope-parity.yml | 3 +++ .github/workflows/ci-tests.yml | 3 +++ .github/workflows/ci.yml | 3 +++ .github/workflows/claude.yml | 3 +++ .github/workflows/codeql.yml | 3 +++ .github/workflows/dependency-review.yml | 3 +++ .github/workflows/docker.yml | 3 +++ .github/workflows/gitleaks.yml | 3 +++ .github/workflows/pr-labeler.yml | 3 +++ .github/workflows/publish.yml | 1 + .github/workflows/trivy.yml | 3 +++ .github/workflows/workflow-lint.yml | 3 +++ 14 files changed, 40 insertions(+) diff --git a/.github/workflows/ci-e2e.yml b/.github/workflows/ci-e2e.yml index af0e2758d..96b0a4b26 100644 --- a/.github/workflows/ci-e2e.yml +++ b/.github/workflows/ci-e2e.yml @@ -3,6 +3,9 @@ name: E2E Tests on: workflow_call: +permissions: + contents: read + jobs: check-changes: name: Check web module changes diff --git a/.github/workflows/ci-quality.yml b/.github/workflows/ci-quality.yml index cc334fcaa..a81876d9d 100644 --- a/.github/workflows/ci-quality.yml +++ b/.github/workflows/ci-quality.yml @@ -3,6 +3,9 @@ name: Quality Checks on: workflow_call: +permissions: + contents: read + jobs: format: runs-on: ubuntu-latest diff --git a/.github/workflows/ci-scope-parity.yml b/.github/workflows/ci-scope-parity.yml index cffdeb341..8e2926ba8 100644 --- a/.github/workflows/ci-scope-parity.yml +++ b/.github/workflows/ci-scope-parity.yml @@ -28,6 +28,9 @@ name: Scope Resolution Parity on: workflow_call: +permissions: + contents: read + jobs: discover: name: Discover migrated languages diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 7010ee6f3..b044d9318 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -3,6 +3,9 @@ name: Tests on: workflow_call: +permissions: + contents: read + jobs: tests: name: ubuntu / coverage diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3059a34dd..dd07337b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: paths-ignore: ['**.md', 'docs/**', 'LICENSE'] workflow_call: +permissions: + contents: read + # Concurrency convention: see CONTRIBUTING.md → "GitHub Actions — Concurrency Convention". # Hardcoded `CI-` prefix (not `${{ github.workflow }}`) because this workflow is # invoked as a reusable workflow from publish.yml and release-candidate.yml. In diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index cfba3ecbc..fcafc0279 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -19,6 +19,9 @@ on: pull_request_review: types: [submitted] +permissions: + contents: read + # Concurrency convention: see CONTRIBUTING.md → "GitHub Actions — Concurrency Convention". # Serialize per-PR/issue to avoid racing comments. concurrency: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d1dda8fb0..41f964cbe 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -17,6 +17,9 @@ on: # already-merged code without waiting for the next PR. - cron: '0 6 * * 1' +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index ad06267c2..1e2ba1f19 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -10,6 +10,9 @@ on: pull_request: branches: [main] +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 9e5750210..93dcf9ce3 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -26,6 +26,9 @@ on: required: true type: string +permissions: + contents: read + # Concurrency convention: see CONTRIBUTING.md → "GitHub Actions — Concurrency Convention". # Tag refs are unique per release, so distinct tags run in parallel. # Re-pushes of the same tag serialize. cancel-in-progress: false — never cancel a publish mid-flight. diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml index cfe4d0856..7cfc3bc52 100644 --- a/.github/workflows/gitleaks.yml +++ b/.github/workflows/gitleaks.yml @@ -12,6 +12,9 @@ on: push: branches: [main] +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index 51664bba8..4a219bfab 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -35,6 +35,9 @@ on: pull_request_target: types: [opened, edited, reopened] +permissions: + contents: read + # Concurrency convention: see CONTRIBUTING.md → "GitHub Actions — Concurrency Convention". # Include `github.event_name` so `pull_request` (validate-title) and # `pull_request_target` (autolabel) runs for the same PR do NOT share a slot diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d5af63205..0694b5e4c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,6 +6,7 @@ on: - 'v*' # No workflow-level permissions — scoped per job below. +permissions: {} # Concurrency convention: see CONTRIBUTING.md → "GitHub Actions — Concurrency Convention". # Tag refs are unique per release, so distinct tags run in parallel. Re-pushes of the diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index f7fba75e9..1251fa4dd 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -20,6 +20,9 @@ on: - cron: '0 8 * * 1' workflow_dispatch: +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: false diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index 7b38b8ddd..803c4d0bd 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -15,6 +15,9 @@ on: paths: - '.github/**' +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true