From 7383d4572046311b400a8e90c4cab8212e05830f Mon Sep 17 00:00:00 2001 From: Jeff Jensen Date: Thu, 10 Sep 2026 12:24:14 -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: * Set workflow-level `permissions: {}` on Build any branch and move `pages: write` / `id-token: write` off the docs workflow and onto its Pages deploy job; give each job only the scopes it uses. * Set `persist-credentials: false` on the checkouts that never reuse the token for a `git` operation. * Drop the `workflow_run` checkouts' `ref:` so Deploy Snapshot and the docs publish build 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 | 8 +++++++- .github/workflows/deploy-snapshot.yml | 15 ++++++++++++--- .github/workflows/publish-docs.yml | 23 +++++++++++++++++------ 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-any-branch.yml b/.github/workflows/build-any-branch.yml index 6d395ef..91ef663 100644 --- a/.github/workflows/build-any-branch.yml +++ b/.github/workflows/build-any-branch.yml @@ -14,6 +14,8 @@ on: - "**/*.adoc" - "**/*.md" +permissions: {} + env: MAVEN_COMMAND: ./mvnw MAVEN_CLI_COMMON: "-e -B" @@ -30,6 +32,8 @@ jobs: contents: read steps: - uses: actions/checkout@v7 + with: + persist-credentials: false - uses: ./.github/actions/jdk-setup - name: Compile and run unit and integration tests # install (not verify) so the archetype's generate-and-run IT resolves the freshly built integration artifact from the local repo. @@ -44,6 +48,8 @@ jobs: contents: write steps: - uses: actions/checkout@v7 + with: + persist-credentials: false - uses: ./.github/actions/jdk-setup - name: Submit dependencies to GitHub - 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 dce9ef2..5910adf 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 | tail -1)" >> $GITHUB_OUTPUT + run: echo "version=$(${{ env.MAVEN_COMMAND }} help:evaluate -Dexpression=project.version -q -DforceStdout | tail -1)" >> "$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 abe0ab1..ff83f51 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -2,15 +2,17 @@ name: Build and Deploy Maven Site to GitHub Pages on: workflow_dispatch: + # 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 exactly that run's commit + # (head_sha) — 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 @@ -22,13 +24,19 @@ env: jobs: build-site: - if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' + if: >- + github.event_name == 'workflow_dispatch' || + (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: ref: ${{ github.event.workflow_run.head_sha || github.sha }} + persist-credentials: false - uses: ./.github/actions/jdk-setup # Tests run (no -DskipTests) so JaCoCo writes target/jacoco.exec and target/jacoco-it.exec; # the site step renders the unit and integration coverage reports from them. @@ -39,7 +47,7 @@ jobs: # building.adoc documents for local use). The archetype module has no site and no # distributionManagement, which is what broke the previous reactor-wide `site site:stage`. - name: Generate Maven site (integration module) - run: ${{ env.MAVEN_COMMAND }} ${{ env.MAVEN_CLI_COMMON }} site -pl integration -Dproject.build.outputTimestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ) + run: ${{ env.MAVEN_COMMAND }} ${{ env.MAVEN_CLI_COMMON }} site -pl integration -Dproject.build.outputTimestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)" - name: Upload Pages artifact uses: actions/upload-pages-artifact@v5 with: @@ -49,6 +57,9 @@ jobs: needs: build-site runs-on: ubuntu-latest timeout-minutes: 10 + permissions: + pages: write + id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} From dd110934c5f1de1c53fca64108e8c0197047fab7 Mon Sep 17 00:00:00 2001 From: Jeff Jensen Date: Thu, 10 Sep 2026 12:24:15 -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: seven days for Maven dependencies, and a shorter three days for GitHub Actions so security-relevant pin bumps still land quickly. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01YA7TRsrNp1TPrHn1KD6vnJ --- .github/dependabot.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 68225ba..491a4f9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,6 +10,8 @@ updates: directory: "/" schedule: interval: "weekly" + cooldown: + default-days: 7 open-pull-requests-limit: 10 groups: # Maven build and reporting plugins @@ -28,15 +30,19 @@ updates: # ----------------------------------------------------------------------- # GitHub Actions — daily, all actions grouped into one PR. - # (Kept daily so security-relevant action pins are updated quickly.) + # Checked daily so security-relevant action pins are picked up quickly; + # a short 3-day cooldown still lets an obviously-bad release be pulled. # The second directory covers the composite action's setup-java pin. # ----------------------------------------------------------------------- + # zizmor: ignore[dependabot-cooldown] # 3 days is a deliberate tradeoff for actions - package-ecosystem: "github-actions" directories: - "/" - "/.github/actions/jdk-setup" schedule: interval: "daily" + cooldown: + default-days: 3 open-pull-requests-limit: 10 groups: actions-updates: # Group all action updates into one PR From 632808558b9fa06e3b9df176eda61b2c9c1e5a39 Mon Sep 17 00:00:00 2001 From: Jeff Jensen Date: Thu, 10 Sep 2026 12:24:15 -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