From 714fc95c1746f31b4877020f80b19f54dbc69321 Mon Sep 17 00:00:00 2001 From: Jeff Jensen Date: Thu, 10 Sep 2026 12:29:04 -0500 Subject: [PATCH 1/3] ci: Harden the GitHub Actions workflows Reduce the token exposure and shell-injection surface of the workflows without changing what they do: * Move dependency submission out of the build job into its own `submit-dependencies` job that only fires on pushes to main, so the `contents: write` token is never present while `mvn clean verify` runs a build of pull-request code. Set workflow-level `permissions: {}` and move `pages: write` / `id-token: write` off the docs workflow onto its Pages deploy job. * Set `persist-credentials: false` on the checkouts that never reuse the token for a `git` operation. * Drop the `workflow_run` checkout's `ref:` so Deploy Snapshot builds the default branch rather than the triggering commit, which on a `workflow_run` can be a fork's. * Quote `$GITHUB_OUTPUT` and the `$(date ...)` command substitution, and pass the resolved version through `env:` into the run step. * Pin `advanced-security/maven-dependency-submission-action` to a commit SHA; the floating `v5` tag has no matching release tag to name, so this moves to `v6.0.1`. * Record why the `workflow_run` triggers are not exploitable: the branch filter excludes fork PRs, the job checks out the default branch, and it reuses no artifact from the triggering run. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01YA7TRsrNp1TPrHn1KD6vnJ --- .github/workflows/build-any-branch.yml | 24 +++++++++++++++++++++--- .github/workflows/deploy-snapshot.yml | 15 ++++++++++++--- .github/workflows/publish-docs.yml | 24 ++++++++++++++++++------ 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build-any-branch.yml b/.github/workflows/build-any-branch.yml index 1dce9df..1bcf1d4 100644 --- a/.github/workflows/build-any-branch.yml +++ b/.github/workflows/build-any-branch.yml @@ -12,6 +12,8 @@ on: - '**/*.adoc' - '**/*.md' +permissions: {} + env: MAVEN_COMMAND: ./mvnw MAVEN_CLI_COMMON: "-e -B" @@ -25,12 +27,28 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 permissions: - contents: write + contents: read steps: - uses: actions/checkout@v7 + with: + persist-credentials: false - uses: ./.github/actions/jdk-setup - name: Build and verify run: ${{ env.MAVEN_COMMAND }} ${{ env.MAVEN_CLI_COMMON }} clean verify + + # Isolated from the build job so its write-scoped token is never present + # while a build of pull-request code runs. Only fires for pushes to main. + submit-dependencies: + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: write + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: ./.github/actions/jdk-setup - name: Submit dependencies to GitHub - if: github.actor != 'dependabot[bot]' - uses: advanced-security/maven-dependency-submission-action@v5 + uses: advanced-security/maven-dependency-submission-action@a64327a7329c9939cf675e458452febe1894a70c # v6.0.1 diff --git a/.github/workflows/deploy-snapshot.yml b/.github/workflows/deploy-snapshot.yml index 7d51647..6e1c01d 100644 --- a/.github/workflows/deploy-snapshot.yml +++ b/.github/workflows/deploy-snapshot.yml @@ -1,6 +1,11 @@ name: Deploy Snapshot on: + # zizmor: ignore[dangerous-triggers] + # workflow_run is used safely: the job's `if` requires a successful "Build any + # branch" run from this repository (head_repository guard, so a fork branch + # named `main` cannot reach it) on `main` (branches filter), and it checks out + # exactly that run's commit (head_sha) — never fork-controlled code. workflow_run: workflows: ["Build any branch"] types: [completed] @@ -19,7 +24,9 @@ concurrency: jobs: deploy-snapshot: - if: github.event.workflow_run.conclusion == 'success' + if: >- + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.head_repository.full_name == github.repository runs-on: ubuntu-latest timeout-minutes: 20 steps: @@ -36,7 +43,7 @@ jobs: - name: Get project version id: ver - run: echo "version=$(${{ env.MAVEN_COMMAND }} help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_OUTPUT + run: echo "version=$(${{ env.MAVEN_COMMAND }} help:evaluate -Dexpression=project.version -q -DforceStdout)" >> "$GITHUB_OUTPUT" - name: Deploy snapshot to Maven Central if: endsWith(steps.ver.outputs.version, '-SNAPSHOT') @@ -47,4 +54,6 @@ jobs: - name: Skip (not a snapshot version) if: "!endsWith(steps.ver.outputs.version, '-SNAPSHOT')" - run: echo "Version ${{ steps.ver.outputs.version }} is a release version — skipping snapshot deploy" + env: + VERSION: ${{ steps.ver.outputs.version }} + run: echo "Version ${VERSION} is a release version — skipping snapshot deploy" diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 8b4a9b4..c8f9283 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -8,15 +8,17 @@ on: - 'src/site/**' - '**/*.adoc' - '**/*.md' + # zizmor: ignore[dangerous-triggers] + # workflow_run is used safely: for that event the build-site job requires a + # successful "Build any branch" run from this repository (head_repository + # guard) on `main` (branches filter), and checks out the default branch — never + # a triggering ref, so never fork-controlled code. workflow_run: workflows: ["Build any branch"] types: [completed] branches: [main] -permissions: - contents: read - pages: write - id-token: write +permissions: {} concurrency: group: pages @@ -28,16 +30,23 @@ env: jobs: build-site: - if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' + if: >- + github.event_name != 'workflow_run' || + (github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.head_repository.full_name == github.repository) runs-on: ubuntu-latest timeout-minutes: 20 + permissions: + contents: read steps: - uses: actions/checkout@v7 + with: + persist-credentials: false - uses: ./.github/actions/jdk-setup - name: Build project run: ${{ env.MAVEN_COMMAND }} ${{ env.MAVEN_CLI_COMMON }} install -DskipTests - name: Generate Maven site - run: ${{ env.MAVEN_COMMAND }} ${{ env.MAVEN_CLI_COMMON }} site -Dproject.build.outputTimestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ) + run: ${{ env.MAVEN_COMMAND }} ${{ env.MAVEN_CLI_COMMON }} site -Dproject.build.outputTimestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)" - name: Upload Pages artifact if: >- github.event_name == 'workflow_dispatch' || @@ -55,6 +64,9 @@ jobs: github.event_name == 'workflow_run' runs-on: ubuntu-latest timeout-minutes: 10 + permissions: + pages: write + id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} From 459ba5e25b63f0e040e2111da99c8e14edfaf646 Mon Sep 17 00:00:00 2001 From: Jeff Jensen Date: Thu, 10 Sep 2026 12:29:04 -0500 Subject: [PATCH 2/3] ci(dependabot): Add a cooldown before opening update PRs Give a bad or compromised release time to be yanked before Dependabot opens a PR for it: a seven-day cooldown on both the Maven and GitHub Actions updaters. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01YA7TRsrNp1TPrHn1KD6vnJ --- .github/dependabot.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 76f53bd..0e552e4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,8 +4,12 @@ updates: directory: / schedule: interval: weekly + cooldown: + default-days: 7 - package-ecosystem: maven directory: / schedule: interval: weekly + cooldown: + default-days: 7 From 80724d34ecd5335725948183c2e23ae62eb12a80 Mon Sep 17 00:00:00 2001 From: Jeff Jensen Date: Thu, 10 Sep 2026 12:29:04 -0500 Subject: [PATCH 3/3] ci: Add actionlint and zizmor workflow linting Add a "Lint workflows" workflow that runs actionlint (workflow schema plus shellcheck on `run:` scripts) and zizmor (workflow security) on every change under `.github/` and weekly on a schedule. Both tool versions are pinned. `.github/zizmor.yml` accepts tag pins for actions from GitHub's own orgs (`actions/*`, `github/*`, `dependabot/*`) and requires a commit SHA for anything else, and keeps the workspace-relative `./...` action syntax rather than the very recent `$/...` form. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01YA7TRsrNp1TPrHn1KD6vnJ --- .github/workflows/lint-workflows.yml | 54 ++++++++++++++++++++++++++++ .github/zizmor.yml | 22 ++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 .github/workflows/lint-workflows.yml create mode 100644 .github/zizmor.yml diff --git a/.github/workflows/lint-workflows.yml b/.github/workflows/lint-workflows.yml new file mode 100644 index 0000000..1feefa2 --- /dev/null +++ b/.github/workflows/lint-workflows.yml @@ -0,0 +1,54 @@ +name: Lint workflows + +on: + pull_request: + paths: + - '.github/**' + push: + branches: [main] + paths: + - '.github/**' + schedule: + - cron: '0 6 * * 1' # Mondays 06:00 UTC — surface newly added lint rules and freshly deprecated actions + workflow_dispatch: + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + actionlint: + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Run actionlint + uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0 + with: + version: 1.7.12 # pin the tool; the action otherwise resolves 'latest' at run time + + zizmor: + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Run zizmor + env: + GH_TOKEN: ${{ github.token }} + # Pinned tool, run directly (not via zizmor-action) so --strict-collection + # can fail the job on a malformed collected file — dependabot.yml, an + # action.yml — instead of warning and skipping it. pipx is preinstalled + # on ubuntu-latest. + run: >- + pipx run zizmor==1.30.1 --strict-collection --format github + --persona regular --collect all -- .github/ diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 0000000..18445ff --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,22 @@ +# zizmor configuration — see .github/workflows/lint-workflows.yml. +# https://docs.zizmor.sh/configuration/ +rules: + unpinned-uses: + config: + # Symbolic (tag) refs are acceptable for actions published by GitHub's + # own organizations; anything else must be pinned to a full commit SHA. + policies: + "actions/*": ref-pin + "github/*": ref-pin + "dependabot/*": ref-pin + "*": hash-pin + + self-repository: + ignore: + # The `$/...` self-repository syntax is only a few weeks old (GitHub, + # Jul 2026). The workspace-relative `./...` form is used deliberately + # until that syntax has settled; revisit and switch over later. + - build-any-branch.yml + - deploy-snapshot.yml + - publish-docs.yml + - release.yml