From 975b2ebab624b9f3e8f4d655f94f6ee2135064f8 Mon Sep 17 00:00:00 2001 From: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:05:38 -0700 Subject: [PATCH 1/4] ci: add reusable Security Scan workflow (zizmor), pin actions to SHAs Mirror the aws-deadline/.github zizmor setup: - Add reusable_security_scan.yml: a central, extensible security-scan umbrella workflow that member repos call from a thin stub. zizmor (GitHub Actions static analysis) is the first check; future checks are added here as sibling jobs so repos never need re-editing. Gated on high-severity findings (min-severity: high), reporting to the run log and PR annotations rather than the Security tab. - Add security_scan.yml: a self-caller so this repo scans itself on push and PR to mainline (and weekly), modeled on the existing codeql caller. - Add zizmor.yml: a single central config with an unpinned-uses policy (OpenJobDescription/* -> ref-pin, everything else -> hash-pin), consumed by reusable_security_scan.yml via --config. One edit here updates policy for every consuming repo. - Pin all third-party GitHub Actions to full commit SHAs with version comments across every reusable workflow and composite action. Our own OpenJobDescription/* reusable workflows keep their @mainline refs on purpose. Add dependabot.yml to keep the SHAs current. - Fix all high-severity template-injection findings by routing ${{ }} expansions through env vars referenced as shell variables, so the gate is green at min-severity: high. Signed-off-by: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> --- .github/actions/check-release/action.yml | 17 ++-- .github/actions/tag-release/action.yml | 22 +++--- .github/dependabot.yml | 10 +++ .github/workflows/reusable_bump.yml | 10 ++- .github/workflows/reusable_codeql.yml | 6 +- .../workflows/reusable_extract_pr_details.yml | 2 +- .github/workflows/reusable_label_pr.yml | 7 +- .../workflows/reusable_maturin_prerelease.yml | 28 ++++--- .github/workflows/reusable_prerelease.yml | 4 +- .github/workflows/reusable_publish.yml | 18 ++--- .github/workflows/reusable_publish_python.yml | 4 +- .github/workflows/reusable_python_build.yml | 10 +-- .../workflows/reusable_record_pr_details.yml | 2 +- .github/workflows/reusable_release.yml | 14 ++-- .github/workflows/reusable_security_scan.yml | 77 +++++++++++++++++++ .../reusable_stale_prs_and_issues.yml | 2 +- .github/workflows/reusable_tag_release.yml | 2 +- .github/workflows/security_scan.yml | 28 +++++++ zizmor.yml | 19 +++++ 19 files changed, 215 insertions(+), 67 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/reusable_security_scan.yml create mode 100644 .github/workflows/security_scan.yml create mode 100644 zizmor.yml diff --git a/.github/actions/check-release/action.yml b/.github/actions/check-release/action.yml index e8ad74e..ca07736 100644 --- a/.github/actions/check-release/action.yml +++ b/.github/actions/check-release/action.yml @@ -13,7 +13,7 @@ runs: using: 'composite' steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: refs/tags/${{ inputs.tag }} fetch-depth: 0 @@ -21,20 +21,21 @@ runs: - name: Check if release exists and verify author shell: bash run: | - echo "Checking if release exists for tag ${{ inputs.tag }}..." - if gh release view "${{ inputs.tag }}" &> /dev/null; then - echo "::error::Release for tag ${{ inputs.tag }} already exists!" + echo "Checking if release exists for tag ${TAG}..." + if gh release view "${TAG}" &> /dev/null; then + echo "::error::Release for tag ${TAG} already exists!" exit 1 fi - + echo "Verifying commit author..." - EXPECTED_AUTHOR="${{ inputs.expected_author }}" AUTHOR=$(git show -s --format='%ae' HEAD) if [[ $AUTHOR != $EXPECTED_AUTHOR ]]; then echo "::error::Expected author email to be '$EXPECTED_AUTHOR', but got '$AUTHOR'. Aborting release." exit 1 fi - + echo "All checks passed: Tag exists, no release found, and author verified." env: - GH_TOKEN: ${{ github.token }} \ No newline at end of file + GH_TOKEN: ${{ github.token }} + TAG: ${{ inputs.tag }} + EXPECTED_AUTHOR: ${{ inputs.expected_author }} \ No newline at end of file diff --git a/.github/actions/tag-release/action.yml b/.github/actions/tag-release/action.yml index 92da4ed..eae9a71 100644 --- a/.github/actions/tag-release/action.yml +++ b/.github/actions/tag-release/action.yml @@ -23,17 +23,21 @@ runs: - name: Determine tag id: determine-tag shell: bash + env: + INPUT_TAG: ${{ inputs.tag }} + EXPECTED_AUTHOR: ${{ inputs.email }} + GIT_USERNAME: ${{ inputs.username }} + GIT_EMAIL: ${{ inputs.email }} run: | - if [[ -n "${{ inputs.tag }}" ]]; then - echo "Using provided tag: ${{ inputs.tag }}" - echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT + if [[ -n "${INPUT_TAG}" ]]; then + echo "Using provided tag: ${INPUT_TAG}" + echo "tag=${INPUT_TAG}" >> $GITHUB_OUTPUT echo "is_new_tag=false" >> $GITHUB_OUTPUT else echo "No tag provided, will create a new one" echo "is_new_tag=true" >> $GITHUB_OUTPUT - + # Verify author - EXPECTED_AUTHOR="${{ inputs.email }}" AUTHOR=$(git show -s --format='%ae' HEAD) if [[ $AUTHOR != $EXPECTED_AUTHOR ]]; then echo "ERROR: Expected author email to be '$EXPECTED_AUTHOR', but got '$AUTHOR'. Aborting release." @@ -41,11 +45,11 @@ runs: else echo "Verified author email ($AUTHOR) is as expected ($EXPECTED_AUTHOR)" fi - + # Configure git - git config user.name "${{ inputs.username }}" - git config user.email "${{ inputs.email }}" - + git config user.name "${GIT_USERNAME}" + git config user.email "${GIT_EMAIL}" + COMMIT_TITLE=$(git show -s --format='%s' HEAD) NEXT_SEMVER=$(python -c 'import sys, re; print(re.match(r"chore\(release\): ([0-9]+\.[0-9]+\.[0-9]+).*", sys.argv[1]).group(1))' "$COMMIT_TITLE") diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..789a8ac --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +# Dependabot proposes commit-SHA bumps for the GitHub Actions pinned in this +# repo's reusable workflows and composite actions. A human reviews the bump here +# once and every consuming repo picks it up via its @mainline reference, so no +# per-consumer Dependabot config is needed. +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/reusable_bump.yml b/.github/workflows/reusable_bump.yml index 2b6ae23..3e23728 100644 --- a/.github/workflows/reusable_bump.yml +++ b/.github/workflows/reusable_bump.yml @@ -18,14 +18,14 @@ jobs: pull-requests: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: mainline fetch-depth: 0 token: ${{ secrets.CI_TOKEN }} - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.9' @@ -35,10 +35,12 @@ jobs: git config --local user.name ${{secrets.USER}} - name: NextSemver + env: + FORCE_VERSION_BUMP: ${{ inputs.force_version_bump }} run: | BUMP_ARGS="" - if [[ "${{ inputs.force_version_bump }}" != "" ]]; then - BUMP_ARGS="$BUMP_ARGS --${{ inputs.force_version_bump }}" + if [[ "${FORCE_VERSION_BUMP}" != "" ]]; then + BUMP_ARGS="$BUMP_ARGS --${FORCE_VERSION_BUMP}" fi # Backup actual changelog to preserve its contents diff --git a/.github/workflows/reusable_codeql.yml b/.github/workflows/reusable_codeql.yml index 7696960..106b8a8 100644 --- a/.github/workflows/reusable_codeql.yml +++ b/.github/workflows/reusable_codeql.yml @@ -23,15 +23,15 @@ jobs: security-events: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@411c4c9a36b3fca4d674f06b6396b2c6d23522c6 # v3.36.3 with: queries: +security-and-quality languages: ${{inputs.languages}} build-mode: ${{inputs.mode }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@411c4c9a36b3fca4d674f06b6396b2c6d23522c6 # v3.36.3 diff --git a/.github/workflows/reusable_extract_pr_details.yml b/.github/workflows/reusable_extract_pr_details.yml index e586f31..e68f07a 100644 --- a/.github/workflows/reusable_extract_pr_details.yml +++ b/.github/workflows/reusable_extract_pr_details.yml @@ -35,7 +35,7 @@ jobs: pr_action: ${{ steps.extract-pr-details.outputs.pr_action }} steps: - name: Download PR artifact - uses: actions/github-script@v6 + uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 env: WORKFLOW_ID: ${{ inputs.record_pr_workflow_id }} ARTIFACT_NAME: ${{ inputs.artifact_name }} diff --git a/.github/workflows/reusable_label_pr.yml b/.github/workflows/reusable_label_pr.yml index d28e28b..5522d0a 100644 --- a/.github/workflows/reusable_label_pr.yml +++ b/.github/workflows/reusable_label_pr.yml @@ -22,7 +22,10 @@ jobs: steps: - name: Add label to PR run: | - echo "Adding label '${{ inputs.label_name }}' to PR #${{ inputs.pr_number }}" - gh pr edit ${{ inputs.pr_number }} --add-label "${{ inputs.label_name }}" --repo ${{ github.repository }} + echo "Adding label '${LABEL_NAME}' to PR #${PR_NUMBER}" + gh pr edit "${PR_NUMBER}" --add-label "${LABEL_NAME}" --repo "${GH_REPO}" env: GITHUB_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ inputs.pr_number }} + LABEL_NAME: ${{ inputs.label_name }} + GH_REPO: ${{ github.repository }} diff --git a/.github/workflows/reusable_maturin_prerelease.yml b/.github/workflows/reusable_maturin_prerelease.yml index 573b650..63c23f1 100644 --- a/.github/workflows/reusable_maturin_prerelease.yml +++ b/.github/workflows/reusable_maturin_prerelease.yml @@ -41,23 +41,25 @@ jobs: platform: ${{ fromJson(inputs.platforms) }} steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: refs/tags/${{ inputs.tag }} fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Inject version if: inputs.version-script != '' - run: ${{ inputs.version-script }} + env: + VERSION_SCRIPT: ${{ inputs.version-script }} + run: "$VERSION_SCRIPT" shell: bash - name: Build wheel - uses: PyO3/maturin-action@v1 + uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: target: ${{ matrix.platform.target }} manylinux: ${{ matrix.platform.manylinux || 'auto' }} @@ -65,7 +67,7 @@ jobs: args: --release --out dist - name: Upload wheel - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels-${{ matrix.platform.os }}-${{ matrix.platform.target }} path: dist/*.whl @@ -76,29 +78,31 @@ jobs: timeout-minutes: 30 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: refs/tags/${{ inputs.tag }} fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Inject version if: inputs.version-script != '' - run: ${{ inputs.version-script }} + env: + VERSION_SCRIPT: ${{ inputs.version-script }} + run: "$VERSION_SCRIPT" shell: bash - name: Build sdist - uses: PyO3/maturin-action@v1 + uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: command: sdist args: --out dist - name: Upload sdist - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: sdist path: dist/*.tar.gz @@ -109,7 +113,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: path: dist merge-multiple: true @@ -123,7 +127,7 @@ jobs: # build-artifact/dist/* glob). A single path strips the directory name. - run: mkdir -p dist_extras - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: build-artifact path: | diff --git a/.github/workflows/reusable_prerelease.yml b/.github/workflows/reusable_prerelease.yml index cc4756d..9c3d701 100644 --- a/.github/workflows/reusable_prerelease.yml +++ b/.github/workflows/reusable_prerelease.yml @@ -17,7 +17,7 @@ jobs: contents: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: refs/tags/${{inputs.tag}} fetch-depth: 0 @@ -29,7 +29,7 @@ jobs: hatch -v build - name: UploadArtifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: build-artifact path: | diff --git a/.github/workflows/reusable_publish.yml b/.github/workflows/reusable_publish.yml index ff557e3..cb5244f 100644 --- a/.github/workflows/reusable_publish.yml +++ b/.github/workflows/reusable_publish.yml @@ -10,7 +10,7 @@ jobs: contents: read steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: ${{ github.ref }} fetch-depth: 0 @@ -35,14 +35,14 @@ jobs: contents: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: release fetch-depth: 0 token: ${{ secrets.CI_TOKEN }} - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.9' @@ -82,7 +82,7 @@ jobs: hatch -v build - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1 with: role-to-assume: ${{ secrets.AWS_PGP_KEY_SECRET_ROLE }} aws-region: us-west-2 @@ -119,14 +119,14 @@ jobs: id-token: write steps: - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1 with: role-to-assume: ${{ secrets.AWS_CODEBUILD_RELEASE_PUBLISH_ROLE }} aws-region: us-west-2 mask-aws-account-id: true - name: Run CodeBuild - uses: aws-actions/aws-codebuild-run-build@v1 + uses: aws-actions/aws-codebuild-run-build@7e46c3fa1c1f217e26a73712796b1f78938b534b # v1.0.18 with: project-name: ${{ github.event.repository.name }}-release-Publish hide-cloudwatch-logs: true @@ -147,20 +147,20 @@ jobs: CUSTOMER_REPOSITORY: ${{ secrets.CUSTOMER_REPOSITORY }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: release fetch-depth: 0 - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1 with: role-to-assume: ${{ secrets.AWS_CODEARTIFACT_ROLE }} aws-region: us-west-2 mask-aws-account-id: true - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.9' diff --git a/.github/workflows/reusable_publish_python.yml b/.github/workflows/reusable_publish_python.yml index da468e1..7158ad5 100644 --- a/.github/workflows/reusable_publish_python.yml +++ b/.github/workflows/reusable_publish_python.yml @@ -15,14 +15,14 @@ jobs: id-token: write steps: - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1 with: role-to-assume: ${{ secrets.AWS_CODEBUILD_RELEASE_PUBLISH_ROLE }} aws-region: us-west-2 mask-aws-account-id: true - name: Run CodeBuild - uses: aws-actions/aws-codebuild-run-build@v1 + uses: aws-actions/aws-codebuild-run-build@7e46c3fa1c1f217e26a73712796b1f78938b534b # v1.0.18 with: project-name: ${{ github.event.repository.name }}-release-Publish hide-cloudwatch-logs: true diff --git a/.github/workflows/reusable_python_build.yml b/.github/workflows/reusable_python_build.yml index cddbe1f..f14fbd3 100644 --- a/.github/workflows/reusable_python_build.yml +++ b/.github/workflows/reusable_python_build.yml @@ -28,27 +28,27 @@ jobs: env: PYTHON: ${{ inputs.python-version }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 if: ${{ !inputs.branch && !inputs.commit }} - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 if: ${{ inputs.branch }} with: ref: ${{ inputs.branch }} fetch-depth: 0 - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 if: ${{ inputs.commit }} with: ref: ${{ inputs.commit }} - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 if: ${{ inputs.ref }} with: ref: ${{ inputs.ref }} - name: Set up Python ${{ inputs.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ inputs.python-version }} diff --git a/.github/workflows/reusable_record_pr_details.yml b/.github/workflows/reusable_record_pr_details.yml index a3630f2..b102a8c 100644 --- a/.github/workflows/reusable_record_pr_details.yml +++ b/.github/workflows/reusable_record_pr_details.yml @@ -24,7 +24,7 @@ jobs: echo "$PR_ACTION" > ./pr-info/pr-action.txt - name: Upload PR info artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: pr-info path: ./pr-info/ diff --git a/.github/workflows/reusable_release.yml b/.github/workflows/reusable_release.yml index 50fc28c..bc40d33 100644 --- a/.github/workflows/reusable_release.yml +++ b/.github/workflows/reusable_release.yml @@ -18,22 +18,22 @@ jobs: environment: release steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: ${{inputs.tag}} fetch-depth: 0 token: ${{ secrets.CI_TOKEN }} - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.9' - name: Download - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1 with: role-to-assume: ${{ secrets.AWS_PGP_KEY_SECRET_ROLE }} aws-region: us-west-2 @@ -60,12 +60,12 @@ jobs: - name: PushRelease env: GH_TOKEN: ${{ secrets.CI_TOKEN }} + TAG: ${{ inputs.tag }} run: | RELEASE_NOTES=$(python .github/scripts/get_latest_changelog.py) - TAG=${{inputs.tag}} - + shopt -s nullglob - gh release create $TAG build-artifact/dist/* build-artifact/dist_extras/* --notes "$RELEASE_NOTES" + gh release create "$TAG" build-artifact/dist/* build-artifact/dist_extras/* --notes "$RELEASE_NOTES" \ No newline at end of file diff --git a/.github/workflows/reusable_security_scan.yml b/.github/workflows/reusable_security_scan.yml new file mode 100644 index 0000000..2405902 --- /dev/null +++ b/.github/workflows/reusable_security_scan.yml @@ -0,0 +1,77 @@ +# Reusable security-scan umbrella workflow. +# +# This is the single, central place where repo-agnostic security checks live. +# Repos call it from a thin stub (see security_scan.yml in each repo), so new +# checks can be added here as additional jobs WITHOUT having to edit every repo. +# +# Current checks: +# - zizmor: static analysis of GitHub Actions workflows (https://zizmor.sh) +# +# To add a future check: add a new job below, optionally gated behind a new +# `workflow_call` input (defaulting to enabled), and it will apply everywhere +# the stub is present on the next run. +name: "Security Scan" + +on: + workflow_call: + inputs: + zizmor: + description: "Run the zizmor GitHub Actions static analysis check." + required: false + type: boolean + default: true + min-severity: + description: >- + Minimum finding severity that fails the check. Findings at or above + this level are reported in the step log and fail the job; anything + below is not reported. Defaults to 'high' so PRs are gated on serious + findings without being red-walled by hygiene noise. Lower it (e.g. + 'medium') once a repo's high-severity backlog is at zero. + One of: unknown, informational, low, medium, high. + required: false + type: string + default: high + +permissions: {} + +jobs: + zizmor: + name: zizmor (GitHub Actions) + if: ${{ inputs.zizmor }} + runs-on: ubuntu-latest + permissions: + # Findings are reported to the step log / PR annotations, not the Security + # tab, so no security-events permission is needed. + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Fetch central zizmor config + # The zizmor.yml policy lives once in OpenJobDescription/.github and is + # shared by every repo, so a single edit there updates policy fleet-wide. + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: OpenJobDescription/.github + ref: mainline + path: .zizmor-config + persist-credentials: false + + - name: Run zizmor 🌈 + # zizmor exits non-zero when it reports findings, so the job fails on any + # finding at or above min-severity. No continue-on-error: the check is a + # gate, not advisory. + uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2 + with: + # Scope to .github/ so the nested central-config checkout at + # .zizmor-config/ is not itself scanned. All workflows and composite + # actions live under .github/, so coverage is complete. + inputs: .github/ + config: .zizmor-config/zizmor.yml + min-severity: ${{ inputs.min-severity }} + # Report findings in the run log and as inline PR annotations rather + # than uploading SARIF to the Security tab. + advanced-security: false + annotations: true diff --git a/.github/workflows/reusable_stale_prs_and_issues.yml b/.github/workflows/reusable_stale_prs_and_issues.yml index 1e7965d..4692802 100644 --- a/.github/workflows/reusable_stale_prs_and_issues.yml +++ b/.github/workflows/reusable_stale_prs_and_issues.yml @@ -11,7 +11,7 @@ jobs: stale-review-stage: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9 + - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 with: debug-only: false repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/reusable_tag_release.yml b/.github/workflows/reusable_tag_release.yml index 842f2a0..5e9e3e3 100644 --- a/.github/workflows/reusable_tag_release.yml +++ b/.github/workflows/reusable_tag_release.yml @@ -21,7 +21,7 @@ jobs: tag: ${{ steps.tag-release.outputs.tag }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: ${{ inputs.tag || github.sha }} fetch-depth: 0 diff --git a/.github/workflows/security_scan.yml b/.github/workflows/security_scan.yml new file mode 100644 index 0000000..06c0ee3 --- /dev/null +++ b/.github/workflows/security_scan.yml @@ -0,0 +1,28 @@ +# Self-caller for the reusable Security Scan workflow. +# +# This repo hosts the reusable workflow (reusable_security_scan.yml), so it +# scans itself here via a local reference. Other OpenJobDescription repos add +# an equivalent stub that calls this workflow by full path, e.g.: +# +# jobs: +# security-scan: +# uses: OpenJobDescription/.github/.github/workflows/reusable_security_scan.yml@mainline +# +# Keeping the scan on every push and PR to mainline means workflow changes are +# gated before they merge. +name: "Security Scan" + +on: + push: + branches: [ "mainline" ] + pull_request: + branches: [ "mainline" ] + schedule: + - cron: '0 8 * * MON' + +permissions: {} + +jobs: + security-scan: + name: Security Scan + uses: ./.github/workflows/reusable_security_scan.yml diff --git a/zizmor.yml b/zizmor.yml new file mode 100644 index 0000000..ef9d6cd --- /dev/null +++ b/zizmor.yml @@ -0,0 +1,19 @@ +# Central zizmor configuration for all OpenJobDescription repos. +# +# This file lives once in OpenJobDescription/.github and is consumed by the +# reusable Security Scan workflow (reusable_security_scan.yml), which passes it +# to zizmor via --config when scanning each member repo. Editing it here changes +# policy for every repo on the next run — no per-repo change needed. +# +# Docs: https://docs.zizmor.sh/configuration/ +rules: + unpinned-uses: + config: + policies: + # Our own reusable workflows and composite actions are referenced by + # branch (e.g. @mainline) on purpose: a single central bump then + # propagates to every consuming repo. Allow a ref (branch/tag) pin here. + OpenJobDescription/*: ref-pin + # Everything third-party MUST be pinned to a full commit SHA. Dependabot + # (see .github/dependabot.yml) proposes SHA bumps to keep these current. + "*": hash-pin From 9946a3c5a641586a6e10408c2dec394a0d7f27b7 Mon Sep 17 00:00:00 2001 From: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:07:22 -0700 Subject: [PATCH 2/4] ci: grant contents:read to Security Scan caller job Signed-off-by: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> --- .github/workflows/security_scan.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/security_scan.yml b/.github/workflows/security_scan.yml index 06c0ee3..983dc44 100644 --- a/.github/workflows/security_scan.yml +++ b/.github/workflows/security_scan.yml @@ -26,3 +26,7 @@ jobs: security-scan: name: Security Scan uses: ./.github/workflows/reusable_security_scan.yml + permissions: + # The reusable workflow's zizmor job checks out the repo (contents: read). + # A caller cannot grant a called workflow more than it declares here. + contents: read From 0933f581fb229dd76343ba5bd0519618ec11bcf1 Mon Sep 17 00:00:00 2001 From: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:09:17 -0700 Subject: [PATCH 3/4] ci: read zizmor config from commit under test in self-scan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reusable workflow fetched zizmor.yml from OpenJobDescription/.github@mainline, but the policy file does not exist on mainline until this PR merges — bootstrap failure. Add a config-ref input (default mainline for member repos) and have the .github self-caller pass the commit under test so the PR validates its own policy. Signed-off-by: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> --- .github/workflows/reusable_security_scan.yml | 11 ++++++++++- .github/workflows/security_scan.yml | 6 ++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/reusable_security_scan.yml b/.github/workflows/reusable_security_scan.yml index 2405902..b62b3b4 100644 --- a/.github/workflows/reusable_security_scan.yml +++ b/.github/workflows/reusable_security_scan.yml @@ -31,6 +31,15 @@ on: required: false type: string default: high + config-ref: + description: >- + Ref of OpenJobDescription/.github to read the central zizmor.yml + policy from. Defaults to 'mainline' so member repos always get the + latest published policy. The .github repo's own self-scan passes its + current ref so a PR that edits the policy validates against itself. + required: false + type: string + default: mainline permissions: {} @@ -55,7 +64,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: OpenJobDescription/.github - ref: mainline + ref: ${{ inputs.config-ref }} path: .zizmor-config persist-credentials: false diff --git a/.github/workflows/security_scan.yml b/.github/workflows/security_scan.yml index 983dc44..2495a00 100644 --- a/.github/workflows/security_scan.yml +++ b/.github/workflows/security_scan.yml @@ -30,3 +30,9 @@ jobs: # The reusable workflow's zizmor job checks out the repo (contents: read). # A caller cannot grant a called workflow more than it declares here. contents: read + with: + # This IS the .github repo, so validate against the zizmor.yml policy from + # the commit under test rather than the published mainline copy. That lets + # a PR editing the policy scan itself. Member repos omit this and default + # to the latest mainline policy. + config-ref: ${{ github.event.pull_request.head.sha || github.sha }} From f821ac7873e7d38b050380f0aeb6740dbe46a6fb Mon Sep 17 00:00:00 2001 From: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:18:08 -0700 Subject: [PATCH 4/4] ci: trigger Security Scan on 'main' as well as 'mainline' Some OpenJobDescription repos (e.g. openjd-rs) use 'main' as their default branch instead of 'mainline'. List both in the push/PR branch filters so the same self-caller stub works regardless of the repo's branch name. Signed-off-by: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> --- .github/workflows/security_scan.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/security_scan.yml b/.github/workflows/security_scan.yml index 2495a00..8d13018 100644 --- a/.github/workflows/security_scan.yml +++ b/.github/workflows/security_scan.yml @@ -8,15 +8,17 @@ # security-scan: # uses: OpenJobDescription/.github/.github/workflows/reusable_security_scan.yml@mainline # -# Keeping the scan on every push and PR to mainline means workflow changes are -# gated before they merge. +# Keeping the scan on every push and PR to the default branch means workflow +# changes are gated before they merge. Both "mainline" and "main" are listed so +# the same stub works across OpenJobDescription repos regardless of which name +# a given repo uses (e.g. openjd-rs uses "main"). name: "Security Scan" on: push: - branches: [ "mainline" ] + branches: [ "mainline", "main" ] pull_request: - branches: [ "mainline" ] + branches: [ "mainline", "main" ] schedule: - cron: '0 8 * * MON'