From 355b95889eb86c2f70555c773e8d4785cbabb0e5 Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sat, 26 Sep 2026 16:03:58 -0600 Subject: [PATCH 1/4] Harden SimpleAPI GitHub workflows --- .github/workflows/maven.yml | 24 ++++--- .github/workflows/publish-javadoc.yml | 93 +++++++++++++++++++++++---- 2 files changed, 95 insertions(+), 22 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index db4d34e1..a1298388 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -10,16 +10,16 @@ on: branches: [ "main" ] permissions: - contents: write # required for dependency submission + contents: read jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 with: java-version: '21' distribution: 'temurin' @@ -28,10 +28,18 @@ jobs: - name: Build with Maven run: mvn -B -f SimpleAPI/pom.xml package - # Only run dependency graph submission on push (not pull_request) + # Dependency submission needs write access, so keep it out of the build job. + submit-dependency-snapshot: + if: github.event_name == 'push' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - name: Submit Dependency Snapshot - if: github.event_name == 'push' - uses: advanced-security/maven-dependency-submission-action@v5 + uses: advanced-security/maven-dependency-submission-action@b275d12641ac2d2108b2cbb7598b154ad2f2cee8 # v5.0.0 with: - directory: SimpleAPI # path to pom.xml - # optional: maven-args: "-DskipTests" + directory: SimpleAPI diff --git a/.github/workflows/publish-javadoc.yml b/.github/workflows/publish-javadoc.yml index 6044a4fe..6c31847c 100644 --- a/.github/workflows/publish-javadoc.yml +++ b/.github/workflows/publish-javadoc.yml @@ -3,22 +3,87 @@ name: Deploy Javadoc on: release: types: [published] - branches: - - master - - main + +permissions: + contents: read + +concurrency: + group: github-pages + cancel-in-progress: false jobs: - publish: + verify-release: + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + eligible: ${{ steps.eligibility.outputs.eligible }} + commit: ${{ steps.eligibility.outputs.commit }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: main + fetch-depth: 0 + persist-credentials: false + + - name: Verify release tag is on main + id: eligibility + env: + PRERELEASE: ${{ github.event.release.prerelease }} + TAG_NAME: ${{ github.event.release.tag_name }} + run: | + set -euo pipefail + if [[ "$PRERELEASE" == "true" ]]; then + echo "eligible=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + tag_commit="$(git rev-parse --verify "refs/tags/${TAG_NAME}^{commit}")" + if git merge-base --is-ancestor "$tag_commit" origin/main; then + echo "eligible=true" >> "$GITHUB_OUTPUT" + echo "commit=$tag_commit" >> "$GITHUB_OUTPUT" + else + echo "eligible=false" >> "$GITHUB_OUTPUT" + fi + + build: + needs: verify-release + if: needs.verify-release.outputs.eligible == 'true' runs-on: ubuntu-latest + permissions: + contents: read steps: - - name: Deploy JavaDoc 🚀 - uses: MathieuSoysal/Javadoc-publisher.yml@main + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - javadoc-branch: gh-pages - java-version: 21 - target-folder: '' - project: maven # or gradle - # subdirectories: moduleA moduleB #for subdirectories support, needs to be run with custom command - custom-command: mvn -f SimpleAPI/pom.xml deploy -P javadoc javadoc:aggregate - javadoc-source-folder: 'SimpleAPI/target/reports/apidocs' + ref: ${{ needs.verify-release.outputs.commit }} + persist-credentials: false + + - name: Set up JDK 21 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + with: + java-version: '21' + distribution: temurin + cache: maven + + - name: Build Javadoc + run: mvn -B -f SimpleAPI/pom.xml clean package -P javadoc javadoc:aggregate + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 + with: + path: SimpleAPI/target/reports/apidocs + + deploy: + needs: [verify-release, build] + if: needs.verify-release.outputs.eligible == 'true' + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + permissions: + pages: write + id-token: write + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 From 0939fd42ef8d4b627a574a0a52a4520ebfa5e05b Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sat, 26 Sep 2026 21:23:04 -0600 Subject: [PATCH 2/4] Harden release and dependency workflows --- .github/workflows/maven.yml | 16 ------- .github/workflows/publish-javadoc.yml | 9 ++-- .../build/BuildInputPinningTest.java | 45 +++++++++++++++++++ 3 files changed, 51 insertions(+), 19 deletions(-) create mode 100644 SimpleAPI/src/test/java/com/bencodez/simpleapi/build/BuildInputPinningTest.java diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index a1298388..206bbaae 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -27,19 +27,3 @@ jobs: - name: Build with Maven run: mvn -B -f SimpleAPI/pom.xml package - - # Dependency submission needs write access, so keep it out of the build job. - submit-dependency-snapshot: - if: github.event_name == 'push' - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - persist-credentials: false - - - name: Submit Dependency Snapshot - uses: advanced-security/maven-dependency-submission-action@b275d12641ac2d2108b2cbb7598b154ad2f2cee8 # v5.0.0 - with: - directory: SimpleAPI diff --git a/.github/workflows/publish-javadoc.yml b/.github/workflows/publish-javadoc.yml index 6c31847c..510fa836 100644 --- a/.github/workflows/publish-javadoc.yml +++ b/.github/workflows/publish-javadoc.yml @@ -2,7 +2,7 @@ name: Deploy Javadoc on: release: - types: [published] + types: [published, released] permissions: contents: read @@ -38,7 +38,8 @@ jobs: exit 0 fi - tag_commit="$(git rev-parse --verify "refs/tags/${TAG_NAME}^{commit}")" + git fetch --no-tags origin "refs/tags/${TAG_NAME}:refs/tags/release" + tag_commit="$(git rev-parse --verify 'refs/tags/release^{commit}')" if git merge-base --is-ancestor "$tag_commit" origin/main; then echo "eligible=true" >> "$GITHUB_OUTPUT" echo "commit=$tag_commit" >> "$GITHUB_OUTPUT" @@ -75,7 +76,9 @@ jobs: deploy: needs: [verify-release, build] - if: needs.verify-release.outputs.eligible == 'true' + if: >- + needs.verify-release.outputs.eligible == 'true' && + needs.build.result == 'success' runs-on: ubuntu-latest environment: name: github-pages diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/build/BuildInputPinningTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/build/BuildInputPinningTest.java new file mode 100644 index 00000000..bbf5afde --- /dev/null +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/build/BuildInputPinningTest.java @@ -0,0 +1,45 @@ +package com.bencodez.simpleapi.build; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.Test; + +class BuildInputPinningTest { + + @Test + void documentationUsesVerifiedReleaseCommitAndAggregateOutput() throws IOException { + String workflow = Files.readString(Path.of("..", ".github", "workflows", "publish-javadoc.yml")); + String verificationJob = job(workflow, "verify-release"); + String buildJob = job(workflow, "build"); + String deployJob = job(workflow, "deploy"); + + assertTrue(workflow.contains("types: [published, released]")); + assertTrue(verificationJob.contains("git merge-base --is-ancestor \"$tag_commit\" origin/main")); + assertFalse(workflow.contains("target_commitish")); + assertTrue(buildJob.contains("ref: ${{ needs.verify-release.outputs.commit }}")); + assertTrue(buildJob.contains("path: SimpleAPI/target/reports/apidocs")); + assertTrue(deployJob.contains("needs.build.result == 'success'")); + } + + @Test + void ordinaryBuildHasNoWriteScopedDependencySubmission() throws IOException { + String workflow = Files.readString(Path.of("..", ".github", "workflows", "maven.yml")); + + assertFalse(workflow.contains("contents: write")); + assertFalse(workflow.contains("maven-dependency-submission-action")); + } + + private static String job(String workflow, String name) { + Matcher matcher = Pattern.compile("(?ms)^ " + Pattern.quote(name) + + ":\\R(?.*?)(?=^ [A-Za-z0-9_-]+:\\R|\\z)").matcher(workflow); + assertTrue(matcher.find(), () -> "Missing " + name + " job"); + return matcher.group("job"); + } +} From 22a471c218a6dadf6880dfa9d19a9bc840786313 Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sat, 26 Sep 2026 21:50:20 -0600 Subject: [PATCH 3/4] Require read-only build workflow permissions --- .../java/com/bencodez/simpleapi/build/BuildInputPinningTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/build/BuildInputPinningTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/build/BuildInputPinningTest.java index bbf5afde..b70f4d36 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/build/BuildInputPinningTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/build/BuildInputPinningTest.java @@ -32,6 +32,7 @@ void documentationUsesVerifiedReleaseCommitAndAggregateOutput() throws IOExcepti void ordinaryBuildHasNoWriteScopedDependencySubmission() throws IOException { String workflow = Files.readString(Path.of("..", ".github", "workflows", "maven.yml")); + assertTrue(Pattern.compile("(?m)^permissions:\\R contents: read$").matcher(workflow).find()); assertFalse(workflow.contains("contents: write")); assertFalse(workflow.contains("maven-dependency-submission-action")); } From d7c5f7e47add5285bb0fcede133dbe7f0917fafd Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sat, 26 Sep 2026 22:28:41 -0600 Subject: [PATCH 4/4] Avoid temporary release tag collisions --- .github/workflows/publish-javadoc.yml | 4 ++-- .../com/bencodez/simpleapi/build/BuildInputPinningTest.java | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-javadoc.yml b/.github/workflows/publish-javadoc.yml index 510fa836..285710c5 100644 --- a/.github/workflows/publish-javadoc.yml +++ b/.github/workflows/publish-javadoc.yml @@ -38,8 +38,8 @@ jobs: exit 0 fi - git fetch --no-tags origin "refs/tags/${TAG_NAME}:refs/tags/release" - tag_commit="$(git rev-parse --verify 'refs/tags/release^{commit}')" + git fetch --no-tags origin "refs/tags/${TAG_NAME}" + tag_commit="$(git rev-parse --verify 'FETCH_HEAD^{commit}')" if git merge-base --is-ancestor "$tag_commit" origin/main; then echo "eligible=true" >> "$GITHUB_OUTPUT" echo "commit=$tag_commit" >> "$GITHUB_OUTPUT" diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/build/BuildInputPinningTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/build/BuildInputPinningTest.java index b70f4d36..a17d89f0 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/build/BuildInputPinningTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/build/BuildInputPinningTest.java @@ -23,6 +23,8 @@ void documentationUsesVerifiedReleaseCommitAndAggregateOutput() throws IOExcepti assertTrue(workflow.contains("types: [published, released]")); assertTrue(verificationJob.contains("git merge-base --is-ancestor \"$tag_commit\" origin/main")); assertFalse(workflow.contains("target_commitish")); + assertTrue(verificationJob.contains("FETCH_HEAD^{commit}")); + assertFalse(verificationJob.contains("refs/tags/release")); assertTrue(buildJob.contains("ref: ${{ needs.verify-release.outputs.commit }}")); assertTrue(buildJob.contains("path: SimpleAPI/target/reports/apidocs")); assertTrue(deployJob.contains("needs.build.result == 'success'"));