From 1b34f6ea9c1d925e3dcbb647f2103ff283794a42 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:21:05 +0100 Subject: [PATCH] fix(security): least-privilege workflow permissions (SonarCloud New Code) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SonarCloud's Quality Gate has been red on 'C Security Rating on New Code' (required >= A) since before this programme started, and had not been looked at. Its public API lists the findings without needing dashboard access: https://sonarcloud.io/api/issues/search?componentKeys=hyperpolymath_krl&types=VULNERABILITY 15 vulnerabilities, all inside the new-code period. Two of them are addressed here, including one this programme introduced. * .github/workflows/e2e.yml — 'Replace "read-all" with specific permissions'. This one is mine: e2e.yml was rewritten in #53 and carried the template's `permissions: read-all`. Narrowed to `contents: read`, which is all four jobs need. * .github/workflows/pages.yml — three findings, 'Move this read/write permission from workflow level to job level'. The workflow granted contents:read + pages:write + id-token:write to BOTH jobs, but only deploy needs the pages and id-token scopes. Split per job so the build job — the one that runs a container image and executes the SSG over repository content — never holds deploy credentials. Not addressed here, and left for a separate pass so this PR stays reviewable: secret-scanner.yml:18 Only pass required secrets to this workflow mirror.yml:12 Only pass required secrets to this workflow release.yml:38 Dependencies without locked resolved versions release.yml:91 Not enforcing HTTPS static-analysis-gate.yml:11 Workflow-level permission static-analysis-gate.yml:30,31,229,230 Not enforcing HTTPS setup.sh:144,156 Not enforcing HTTPS The rating will not reach A until those are triaged too; several may be false positives (the HTTPS findings need reading in context before changing). Verified: actionlint 0, K9 0, A2ML 0, e2e 4/4, aspect 4/4, smoke 20/20. --- .github/workflows/e2e.yml | 3 ++- .github/workflows/pages.yml | 13 +++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 58ca643..a71d3ca 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -27,7 +27,8 @@ on: - 'examples/**' - '.github/workflows/e2e.yml' workflow_dispatch: -permissions: read-all +permissions: + contents: read concurrency: group: e2e-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 97d9d9d..d89fb86 100755 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -5,15 +5,16 @@ on: push: branches: [main, master] workflow_dispatch: -permissions: - contents: read - pages: write - id-token: write +# Permissions are declared per job rather than at workflow level, so the build +# job never holds the pages/id-token write scopes that only the deploy job +# needs. concurrency: group: "pages" cancel-in-progress: false jobs: build: + permissions: + contents: read runs-on: ubuntu-latest timeout-minutes: 15 container: @@ -43,6 +44,10 @@ jobs: with: path: '_site' deploy: + permissions: + contents: read + pages: write + id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }}