From 234af9c41786aa19aae927e0658fc16dce134aec Mon Sep 17 00:00:00 2001 From: Ryan Lahfa Date: Wed, 9 Sep 2026 19:19:41 +0200 Subject: [PATCH 1/5] npins/sources: update Required for zizmor. Signed-off-by: Ryan Lahfa --- npins/sources.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/npins/sources.json b/npins/sources.json index 1df009fd..5205aa2f 100644 --- a/npins/sources.json +++ b/npins/sources.json @@ -58,8 +58,8 @@ "nixpkgs": { "type": "Channel", "name": "nixos-26.05", - "url": "https://releases.nixos.org/nixos/26.05/nixos-26.05.1947.a0374025a863/nixexprs.tar.xz", - "hash": "sha256-PoRRDfm1khYEJCk5KyX4Snud03VzRcmxKil5n6drTI0=" + "url": "https://releases.nixos.org/nixos/26.05/nixos-26.05.9440.6aefcda9401b/nixexprs.tar.xz", + "hash": "sha256-JLopN/XSodSsJIZ7v7tEjpPZ+QXc883s3HoUIxHD8Io=" }, "portail": { "type": "Git", From e8c90f041b6b12fbc2dca0bc49fbde38468cd3c3 Mon Sep 17 00:00:00 2001 From: Ryan Lahfa Date: Mon, 7 Sep 2026 21:02:36 +0200 Subject: [PATCH 2/5] ci: add zizmor as a pre-push hook and fix existing workflows Add zizmor (static analysis for GitHub Actions) as a pre-push hook. Also address every zizmor finding in the existing workflows: - scope GITHUB_TOKEN permissions per-job and drop unneeded writes, - set `persist-credentials: false` on checkout where no push is needed, - add the REUSE SPDX header that was missing from several files. Signed-off-by: Ryan Lahfa --- .github/workflows/securix-cd-docs.yaml | 32 ++++++++++++------- .../workflows/securix-check-formatting.yaml | 22 +++++++++---- .../workflows/securix-check-licensing.yaml | 22 +++++++++---- .github/workflows/securix-ci-docs.yaml | 18 +++++++++-- .github/workflows/securix-cleanup-docs.yaml | 16 +++++++--- .github/workflows/securix-testsuite.yaml | 22 +++++++++---- default.nix | 8 +++++ 7 files changed, 104 insertions(+), 36 deletions(-) diff --git a/.github/workflows/securix-cd-docs.yaml b/.github/workflows/securix-cd-docs.yaml index 55777ff5..0d0c737c 100644 --- a/.github/workflows/securix-cd-docs.yaml +++ b/.github/workflows/securix-cd-docs.yaml @@ -1,18 +1,35 @@ -permissions: - contents: write - id-token: write - pages: write +# SPDX-FileCopyrightText: 2026 Ryan Lahfa +# +# SPDX-License-Identifier: MIT + +name: '[Sécurix] Publish documentation' + +on: + workflow_dispatch: + push: + branches: + - main + +permissions: {} + concurrency: group: pages-${{ github.ref }} cancel-in-progress: false + jobs: publish_docs: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} + permissions: + contents: read + id-token: write + pages: write runs-on: ubuntu-latest steps: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false - uses: samueldr/lix-gha-installer-action@f526e761e1ad201b0f4231f61a2a569f17bcc2ac # main - name: Setup Pages uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 @@ -25,10 +42,3 @@ jobs: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 -name: '[Sécurix] Publish documentation' -on: - workflow_dispatch: - push: - branches: - - main - diff --git a/.github/workflows/securix-check-formatting.yaml b/.github/workflows/securix-check-formatting.yaml index 2db33868..1d057bc3 100644 --- a/.github/workflows/securix-check-formatting.yaml +++ b/.github/workflows/securix-check-formatting.yaml @@ -1,16 +1,26 @@ +# SPDX-FileCopyrightText: 2026 Ryan Lahfa +# +# SPDX-License-Identifier: MIT + +name: '[Sécurix] Formatting check' + +on: + pull_request: + push: + branches: + - main + +permissions: {} + jobs: reuse_lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false - uses: samueldr/lix-gha-installer-action@f526e761e1ad201b0f4231f61a2a569f17bcc2ac # main - name: Check for statix run: nix-shell --run 'statix check --config statix.toml' - name: Check for formatting run: nix-shell --run 'nixfmt -sc $(find . -name "*.nix" -not -path "./npins/*" -not -path "./lib/default.nix")' -name: '[Sécurix] Formatting check' -on: - pull_request: - push: - branches: - - main diff --git a/.github/workflows/securix-check-licensing.yaml b/.github/workflows/securix-check-licensing.yaml index efa052c7..cff5b541 100644 --- a/.github/workflows/securix-check-licensing.yaml +++ b/.github/workflows/securix-check-licensing.yaml @@ -1,14 +1,24 @@ +# SPDX-FileCopyrightText: 2026 Ryan Lahfa +# +# SPDX-License-Identifier: MIT + +name: '[Sécurix] REUSE Licensing conformance' + +on: + pull_request: + push: + branches: + - main + +permissions: {} + jobs: reuse_lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false - uses: samueldr/lix-gha-installer-action@f526e761e1ad201b0f4231f61a2a569f17bcc2ac # main - name: Check for REUSE compliance run: nix-shell --run 'reuse --root . lint' -name: '[Sécurix] REUSE Licensing conformance' -on: - pull_request: - push: - branches: - - main diff --git a/.github/workflows/securix-ci-docs.yaml b/.github/workflows/securix-ci-docs.yaml index b33562b0..d34374b3 100644 --- a/.github/workflows/securix-ci-docs.yaml +++ b/.github/workflows/securix-ci-docs.yaml @@ -1,14 +1,26 @@ +# SPDX-FileCopyrightText: 2026 Ryan Lahfa +# +# SPDX-License-Identifier: MIT + +name: '[Sécurix] Test the docs' + +on: + pull_request: + +permissions: {} + concurrency: group: preview-pages-${{ github.ref }} cancel-in-progress: true + jobs: build_and_preview_manual_docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false - uses: samueldr/lix-gha-installer-action@f526e761e1ad201b0f4231f61a2a569f17bcc2ac # main - name: Build the manual documentation run: nix-build -A docs.all -name: '[Sécurix] Test the docs' -on: - pull_request: + diff --git a/.github/workflows/securix-cleanup-docs.yaml b/.github/workflows/securix-cleanup-docs.yaml index fc9c5109..ed07e9b3 100644 --- a/.github/workflows/securix-cleanup-docs.yaml +++ b/.github/workflows/securix-cleanup-docs.yaml @@ -1,21 +1,29 @@ +# SPDX-FileCopyrightText: 2026 Ryan Lahfa +# +# SPDX-License-Identifier: MIT + +name: '[Sécurix] Garbage collect documentation previews' + on: pull_request: types: - closed -permissions: - pull-requests: write - contents: write +permissions: {} concurrency: group: preview-pages-${{ github.ref }} -name: '[Sécurix] Garbage collect documentation previews' jobs: clean-up: + permissions: + contents: write + pull-requests: write runs-on: ubuntu-latest steps: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + # zizmor: ignore[artipacked] - the persisted credential is required to + # `git push` the cleaned-up gh-pages branch back to the remote. with: ref: gh-pages diff --git a/.github/workflows/securix-testsuite.yaml b/.github/workflows/securix-testsuite.yaml index 903a5590..b519b12b 100644 --- a/.github/workflows/securix-testsuite.yaml +++ b/.github/workflows/securix-testsuite.yaml @@ -1,3 +1,17 @@ +# SPDX-FileCopyrightText: 2026 Ryan Lahfa +# +# SPDX-License-Identifier: MIT + +name: '[Sécurix] Test suite' + +on: + pull_request: + push: + branches: + - main + +permissions: {} + jobs: tests: runs-on: ubuntu-latest @@ -8,6 +22,8 @@ jobs: HAS_CACHE_CREDENTIALS: ${{ secrets.AWS_ACCESS_KEY_ID != '' && secrets.AWS_SECRET_KEY != '' && secrets.NIX_SIGNING_PRIVATE_KEY != '' }} steps: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false - uses: samueldr/lix-gha-installer-action@f526e761e1ad201b0f4231f61a2a569f17bcc2ac # main - uses: zombiezen/setup-nix-cache-action@cacc7abf0a6636b0ef45ec2ae055a9734cdd4122 # main if: env.HAS_CACHE_CREDENTIALS == 'true' @@ -52,9 +68,3 @@ jobs: echo "Patched post-build-hook successfully." - name: Build tests run: nix-build -A tests -name: '[Sécurix] Test suite' -on: - pull_request: - push: - branches: - - main diff --git a/default.nix b/default.nix index a4464aff..59650924 100644 --- a/default.nix +++ b/default.nix @@ -44,6 +44,14 @@ let stages = [ "pre-push" ]; package = pkgs.reuse; }; + + zizmor = { + enable = true; + stages = [ "pre-push" ]; + package = pkgs.zizmor; + entry = "zizmor .github/workflows/ --offline"; + pass_filenames = false; + }; }; }; lib-securix = import ./lib { From 8348da531827fd80da2a49ad3d3b7dbf47f142d0 Mon Sep 17 00:00:00 2001 From: Ryan Lahfa Date: Mon, 7 Sep 2026 21:03:33 +0200 Subject: [PATCH 3/5] ci: add zizmor as a CI check Signed-off-by: Ryan Lahfa --- .github/workflows/zizmor.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/zizmor.yml diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 00000000..bf267cc1 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2026 Ryan Lahfa +# +# SPDX-License-Identifier: MIT + +name: '[Sécurix] Security linting on GHA' + +on: + pull_request: + push: + branches: + - main + +permissions: {} + +jobs: + zizmor: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false + - uses: samueldr/lix-gha-installer-action@f526e761e1ad201b0f4231f61a2a569f17bcc2ac # main + - name: Run zizmor + run: nix-shell -p zizmor --run "zizmor .github/workflows --offline" From e55da3174862b61e50735c3f6956648e39910f44 Mon Sep 17 00:00:00 2001 From: Ryan Lahfa Date: Mon, 7 Sep 2026 21:04:27 +0200 Subject: [PATCH 4/5] .github/workflows: auto-label pull requests by area MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a path-based labeler that applies A/* area labels to pull requests based on the files they touch, matching the existing labels of the Sécurix repository. The workflow is restricted to same-repository PRs, scopes its token to contents: read + pull-requests: write, and pins actions/labeler by SHA1. Signed-off-by: Ryan Lahfa --- .github/labeler.yml | 91 ++++++++++++++++++++++++++ .github/workflows/auto-label-areas.yml | 27 ++++++++ 2 files changed, 118 insertions(+) create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/auto-label-areas.yml diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 00000000..10ea2293 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,91 @@ +# SPDX-FileCopyrightText: 2026 Ryan Lahfa +# +# SPDX-License-Identifier: MIT + +# Path-based auto-labelling for A/* area labels. +# Used by .github/workflows/auto-label-areas.yml via actions/labeler@v5. +# One PR can match multiple areas (e.g. modules + testing). + +"A/hardening": + - changed-files: + - any-glob-to-any-file: + - "modules/anssi/**" + - "modules/auditd.nix" + +"A/auth": + - changed-files: + - any-glob-to-any-file: + - "modules/security-keys.nix" + - "modules/pam/**" + - "modules/ssh-tpm-agent.nix" + - "modules/superadmins/**" + - "modules/admins/**" + +"A/encryption": + - changed-files: + - any-glob-to-any-file: "modules/filesystems/**" + +"A/secure-boot": + - changed-files: + - any-glob-to-any-file: "modules/bootloader.nix" + +"A/networking": + - changed-files: + - any-glob-to-any-file: + - "modules/networking.nix" + - "modules/networkmanager-events.nix" + - "modules/vpn/**" + - "modules/http-proxy/**" + - "modules/bastion/**" + +"A/modules": + - changed-files: + - any-glob-to-any-file: + - "modules/chromium/**" + - "modules/graphical-interface/**" + - "modules/tools/**" + - "modules/console.nix" + - "modules/shells.nix" + - "modules/pki.nix" + +"A/nixos": + - changed-files: + - any-glob-to-any-file: + - "lib/**" + - "default.nix" + - "shell.nix" + - "npins/**" + - "pkgs/overlay.nix" + +"A/hardware": + - changed-files: + - any-glob-to-any-file: "hardware/**" + +"A/docs": + - changed-files: + - any-glob-to-any-file: + - "docs/**" + - "*.md" + - "LICENSES/**" + +"A/testing": + - changed-files: + - any-glob-to-any-file: + - "tests/**" + - ".github/workflows/securix-testsuite.yaml" + - "workflows/**" + +"A/lifecycle": + - changed-files: + - any-glob-to-any-file: + - "modules/updates/**" + - "modules/self.nix" + - "modules/distribution.nix" + +"A/infra": + - changed-files: + - any-glob-to-any-file: + - ".github/workflows/**" + - "config.yml" + - "statix.toml" + - ".pre-commit-config.yaml" diff --git a/.github/workflows/auto-label-areas.yml b/.github/workflows/auto-label-areas.yml new file mode 100644 index 00000000..280c8202 --- /dev/null +++ b/.github/workflows/auto-label-areas.yml @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2026 Ryan Lahfa +# +# SPDX-License-Identifier: MIT + +name: "Auto-label: areas" + +on: + pull_request_target: + types: [opened, reopened, synchronize, ready_for_review] + +permissions: {} + +jobs: + label: + # pull_request_target is safe here: labeler is pinned, configuration is + # read from the base branch, and the action only uses the GitHub API. + if: github.event.pull_request.head.repo.full_name == github.repository + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7 + with: + configuration-path: .github/labeler.yml + sync-labels: false + dot: false From 06cdd98c8984726866453ae75a0807e58df8ace1 Mon Sep 17 00:00:00 2001 From: Ryan Lahfa Date: Mon, 7 Sep 2026 21:05:30 +0200 Subject: [PATCH 5/5] .github/workflows: relabel pull requests by status Signed-off-by: Ryan Lahfa --- .github/workflows/auto-label-status.yml | 133 ++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 .github/workflows/auto-label-status.yml diff --git a/.github/workflows/auto-label-status.yml b/.github/workflows/auto-label-status.yml new file mode 100644 index 00000000..6bbee491 --- /dev/null +++ b/.github/workflows/auto-label-status.yml @@ -0,0 +1,133 @@ +# SPDX-FileCopyrightText: 2026 Ryan Lahfa +# +# SPDX-License-Identifier: MIT + +name: "Auto-label: status" + +on: + pull_request_target: # zizmor: ignore[dangerous-triggers] - safe: pinned action, no secrets, same-repo only + types: [opened, reopened, synchronize, ready_for_review] + pull_request_review: + types: [submitted] + pull_request_review_comment: + types: [created] + issues: + types: [opened] + +permissions: {} + +jobs: + pr-opened: + # pull_request_target is safe here: github-script is pinned, no secrets are + # used, and the action only calls the GitHub API to manage labels. + if: github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name == github.repository + permissions: + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@d746ffe35508b1917358783b479e04febd2b8f71 # v9.0.0 + with: + script: | + const pr = context.payload.pull_request; + if (!pr) return; + const issue_number = pr.number; + const repo = context.repo; + const labels = pr.labels.map(l => l.name); + const has = n => labels.includes(n); + + // On open/sync, ensure awaiting-maintainers is set and awaiting-author is cleared + // This implements: new PR -> awaiting-maintainers + if (!has('status: awaiting-maintainers') && !has('status: ready-to-merge') && !has('status: awaiting smoke testing') && !has('status: blocked')) { + await github.rest.issues.addLabels({ ...repo, issue_number, labels: ['status: awaiting-maintainers'] }); + } + if (has('status: awaiting-author')) { + try { await github.rest.issues.removeLabel({ ...repo, issue_number, name: 'status: awaiting-author' }); } catch(e) {} + } + // New author push after review -> flip back from awaiting-author is handled here via synchronize + // If PR was in awaiting-author and author pushed, the above remove + add covers it + + review: + if: github.event_name == 'pull_request_review' || github.event_name == 'pull_request_review_comment' + permissions: + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@d746ffe35508b1917358783b479e04febd2b8f71 # v9.0.0 + with: + script: | + const review = context.payload.review; + const pr = context.payload.pull_request || review?.pull_request; + if (!pr || !review) return; + const issue_number = pr.number; + const repo = context.repo; + const assoc = review.author_association; // OWNER, MEMBER, COLLABORATOR, etc. + const isMaintainer = ['OWNER','MEMBER'].includes(assoc); + if (!isMaintainer) return; + + const state = review.state; // approved, changes_requested, commented + // Fetch current labels (payload may be stale) + const { data: cur } = await github.rest.issues.get({ ...repo, issue_number }); + const has = n => cur.labels.some(l => l.name === n); + + if (state === 'changes_requested' || (state === 'commented' && has('status: awaiting-maintainers'))) { + // Maintainer reviewed -> awaiting-author + if (has('status: awaiting-maintainers')) { + try { await github.rest.issues.removeLabel({ ...repo, issue_number, name: 'status: awaiting-maintainers' }); } catch(e) {} + } + if (!has('status: awaiting-author') && !has('status: blocked')) { + await github.rest.issues.addLabels({ ...repo, issue_number, labels: ['status: awaiting-author'] }); + } + } + if (state === 'approved') { + if (has('status: awaiting-author')) { + try { await github.rest.issues.removeLabel({ ...repo, issue_number, name: 'status: awaiting-author' }); } catch(e) {} + } + // ready-to-merge is set after CI checks pass; we only ensure not stuck in awaiting-* + // If checks already green, mark ready; otherwise awaiting-maintainers will be set on next sync + } + + ci-result: + # workflow_run is safe here: github-script is pinned, no secrets are used, + # and the action only calls the GitHub API to manage labels. We only act on + # PRs from the same repository to avoid processing fork-sourced workflow runs. + if: github.event_name == 'workflow_run' && github.event.workflow_run.head_repository.full_name == github.repository + permissions: + pull-requests: write + issues: write + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@d746ffe35508b1917358783b479e04febd2b8f71 # v9.0.0 + with: + script: | + const run = context.payload.workflow_run; + if (!run) return; + // Only act on PRs + const prs = run.pull_requests; + if (!prs || prs.length === 0) return; + for (const pr of prs) { + const issue_number = pr.number; + const { data: cur } = await github.rest.issues.get({ ...context.repo, issue_number }); + const has = n => cur.labels.some(l => l.name === n); + if (run.conclusion === 'failure' || run.conclusion === 'timed_out') { + if (!has('status: awaiting-ci')) { + await github.rest.issues.addLabels({ ...context.repo, issue_number, labels: ['status: awaiting-ci'] }); + } + } else if (run.conclusion === 'success') { + if (has('status: awaiting-ci')) { + try { await github.rest.issues.removeLabel({ ...context.repo, issue_number, name: 'status: awaiting-ci' }); } catch(e) {} + } + // If PR is approved and CI just turned green, promote to ready-to-merge + // Check reviews: last review approved? + const { data: reviews } = await github.rest.pulls.listReviews({ ...context.repo, pull_number: issue_number, per_page: 20 }); + const last = [...reviews].reverse().find(r => ['OWNER','MEMBER'].includes(r.author_association)); + if (last && last.state === 'APPROVED' && !has('status: ready-to-merge') && !has('status: blocked')) { + await github.rest.issues.addLabels({ ...context.repo, issue_number, labels: ['status: ready-to-merge'] }); + if (has('status: awaiting-maintainers')) { + try { await github.rest.issues.removeLabel({ ...context.repo, issue_number, name: 'status: awaiting-maintainers' }); } catch(e) {} + } + if (has('status: awaiting-author')) { + try { await github.rest.issues.removeLabel({ ...context.repo, issue_number, name: 'status: awaiting-author' }); } catch(e) {} + } + } + } + }