From 24e059d625a4d231c4ef93c865ec2ff47e051ad0 Mon Sep 17 00:00:00 2001 From: Matthew Watkins Date: Wed, 8 Jul 2026 14:13:52 +0100 Subject: [PATCH] Fix: resolve zizmor auditor persona findings Resolve the zizmor auditor-persona findings (min-severity low) that fail the organisation's required "Audit Workflows" gate and block Dependabot pull requests from merging. - Replace the boilerplate `tag-push` and `release-drafter` workflows with the canonical `actions-template` versions. This adds the workflow-level `concurrency` group (`concurrency-limits`), harden-runner block-mode egress via `harden-runner-block-action`, and documented permission scopes (`undocumented-permissions`). - testing.yaml: add a workflow-level `concurrency` group (`concurrency-limits`) and set `persist-credentials: false` on both checkout steps (`artipacked`). - action.yaml: hoist all `${{ inputs.* }}` expressions out of `run:` blocks into step-level environment variables and reference them as quoted shell variables (`template-injection`). Replace the dangerous `$GITHUB_ENV` writes with `$GITHUB_OUTPUT` step outputs (`github-env`): the setup step now publishes `path_prefix` and the dependency-install step publishes `kernel_flags` and `flags`; downstream steps consume these via `steps..outputs.*` hoisted into their own step environments. Shell behaviour (word splitting of flags, glob expansion of the notebook path) is preserved. Verified clean with: zizmor --persona auditor --min-severity low Co-Authored-By: Claude Signed-off-by: Matthew Watkins --- .github/workflows/release-drafter.yaml | 22 +++-- .github/workflows/tag-push.yaml | 43 +++++++-- .github/workflows/testing.yaml | 7 ++ action.yaml | 118 ++++++++++++++++--------- 4 files changed, 135 insertions(+), 55 deletions(-) diff --git a/.github/workflows/release-drafter.yaml b/.github/workflows/release-drafter.yaml index 89540cc..599855b 100644 --- a/.github/workflows/release-drafter.yaml +++ b/.github/workflows/release-drafter.yaml @@ -20,16 +20,26 @@ jobs: update_release_draft: name: 'Update Release Draft' permissions: - # write permission is required to create releases - contents: write + contents: write # Create and update draft releases runs-on: 'ubuntu-latest' timeout-minutes: 3 steps: - # Harden the runner used by this workflow + # Load the egress allow-list out-of-band from the + # organisation's .github repository and publish it as + # $CONNECTION_ALLOW_LIST for the harden-runner step below. # yamllint disable-line rule:line-length - - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + - uses: lfreleng-actions/harden-runner-block-action@6db537b3e6d060c3287c5a3ce2c28b55b0af330d # v0.2.1 with: - egress-policy: 'audit' + config: '@18d9c4446bea555d0783e850f6d295f844fe8f67' # v0.1.1 + + # Harden the runner with the just-loaded allow-list. + - name: 'Harden runner (block)' + # yamllint disable-line rule:line-length + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: 'block' + allowed-endpoints: > + ${{ env.CONNECTION_ALLOW_LIST }} # yamllint disable-line rule:line-length - - uses: release-drafter/release-drafter@ed4bc48ec97379be2258e7b7ac2624a3e26ab809 # v7.4.0 + - uses: release-drafter/release-drafter@4d75298e00d9e34c483e5ff8c68d0ea1c1940c1e # v7.5.1 diff --git a/.github/workflows/tag-push.yaml b/.github/workflows/tag-push.yaml index 1446467..93d716b 100644 --- a/.github/workflows/tag-push.yaml +++ b/.github/workflows/tag-push.yaml @@ -13,6 +13,13 @@ on: permissions: {} +# Serialise repeated runs for the same tag; never cancel an in-flight +# release promotion. Distinct tags use distinct groups and proceed +# independently. +concurrency: + group: '${{ github.workflow }}-${{ github.ref }}' + cancel-in-progress: false + jobs: validate_tag: name: 'Validate Tag' @@ -25,11 +32,22 @@ jobs: outputs: tag: "${{ steps.tag_validate.outputs.tag_name }}" steps: - # Harden the runner used by this workflow + # Load the egress allow-list out-of-band from the + # organisation's .github repository and publish it as + # $CONNECTION_ALLOW_LIST for the harden-runner step below. # yamllint disable-line rule:line-length - - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + - uses: lfreleng-actions/harden-runner-block-action@6db537b3e6d060c3287c5a3ce2c28b55b0af330d # v0.2.1 + with: + config: '@18d9c4446bea555d0783e850f6d295f844fe8f67' # v0.1.1 + + # Harden the runner with the just-loaded allow-list. + - name: 'Harden runner (block)' + # yamllint disable-line rule:line-length + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 with: - egress-policy: audit + egress-policy: 'block' + allowed-endpoints: > + ${{ env.CONNECTION_ALLOW_LIST }} # yamllint disable-line rule:line-length - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -53,14 +71,25 @@ jobs: needs: validate_tag runs-on: 'ubuntu-latest' permissions: - contents: write + contents: write # Promote (publish) the draft release for the tag timeout-minutes: 5 steps: - # Harden the runner used by this workflow + # Load the egress allow-list out-of-band from the + # organisation's .github repository and publish it as + # $CONNECTION_ALLOW_LIST for the harden-runner step below. # yamllint disable-line rule:line-length - - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + - uses: lfreleng-actions/harden-runner-block-action@6db537b3e6d060c3287c5a3ce2c28b55b0af330d # v0.2.1 + with: + config: '@18d9c4446bea555d0783e850f6d295f844fe8f67' # v0.1.1 + + # Harden the runner with the just-loaded allow-list. + - name: 'Harden runner (block)' + # yamllint disable-line rule:line-length + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 with: - egress-policy: audit + egress-policy: 'block' + allowed-endpoints: > + ${{ env.CONNECTION_ALLOW_LIST }} # yamllint disable-line rule:line-length - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 6fc0bcd..b9c9a7e 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -20,6 +20,10 @@ on: permissions: {} +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + env: python_version: "3.12" @@ -34,11 +38,14 @@ jobs: steps: - name: "Checkout repository" uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false # Perform setup prior to running test(s) - name: "Checkout sample project repository" uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: + persist-credentials: false repository: "lfreleng-actions/test-python-project" path: "test-python-project" diff --git a/action.yaml b/action.yaml index 03b5e30..09af70b 100644 --- a/action.yaml +++ b/action.yaml @@ -49,31 +49,43 @@ runs: using: 'composite' steps: - name: 'Setup action/environment' + id: setup shell: bash + env: + PYTHON_VERSION: "${{ inputs.python_version }}" + PATH_PREFIX: "${{ inputs.path_prefix }}" + PERMIT_FAIL: "${{ inputs.permit_fail }}" run: | # Setup action/environment - if [ -z "${{ inputs.python_version }}" ]; then + if [ -z "$PYTHON_VERSION" ]; then echo 'Error: Python version was not provided ❌'; exit 1 else - echo "Using Python: ${{ inputs.python_version }} 🐍" + echo "Using Python: $PYTHON_VERSION 🐍" fi # Handle path_prefix input consistently and when absent - path_prefix="${{ inputs.PATH_PREFIX }}" + path_prefix="$PATH_PREFIX" if [ -z "$path_prefix" ]; then # Set current directory as path prefix path_prefix="." else - # Strip any trailing slash in provided path + # Strip any trailing slash in provided path path_prefix="${path_prefix%/}" fi # Verify is a valid directory path if [ ! -d "$path_prefix" ]; then echo 'Error: invalid path/prefix to project directory ❌'; exit 1 fi - echo "path_prefix=$path_prefix" >> "$GITHUB_ENV" + # Use the heredoc output form with a unique delimiter to + # guard against output injection via crafted input values + delimiter="ghadelim_$$_${RANDOM}${RANDOM}${RANDOM}" + { + echo "path_prefix<<${delimiter}" + echo "$path_prefix" + echo "${delimiter}" + } >> "$GITHUB_OUTPUT" - if [ "f${{ inputs.PERMIT_FAIL }}" = "ftrue" ]; then + if [ "f$PERMIT_FAIL" = "ftrue" ]; then echo 'Warning: test failures will be permitted ⚠️' fi @@ -84,7 +96,7 @@ runs: python-version: ${{ inputs.python_version }} - name: 'Cache Python dependencies' - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v4.0.2 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.cache/pip @@ -102,7 +114,12 @@ runs: # yamllint enable rule:line-length - name: 'Install notebook/test dependencies' + id: flags shell: bash + env: + PATH_PREFIX: "${{ steps.setup.outputs.path_prefix }}" + NBMAKE_KERNEL: "${{ inputs.nbmake_kernel }}" + PARALLEL_TESTS: "${{ inputs.parallel_tests }}" run: | # Install notebook/test dependencies echo 'Installing: pytest, pytest-cov, nbmake ⬇️' @@ -111,26 +128,32 @@ runs: pytest pytest-cov nbmake echo 'Install project and dependencies' - if [ -f ${{ env.path_prefix }}/pyproject.toml ]; then - echo "Source: ${{ env.path_prefix }}/pyproject.toml ⬇️" - pip install -q "${{ env.path_prefix }}/" - elif [ -f ${{ env.path_prefix }}/requirements.txt ]; then - echo "Source: ${{ env.path_prefix }}/requirements.txt ⬇️" - pip install -q -r "${{ env.path_prefix }}/requirements.txt" + if [ -f "$PATH_PREFIX/pyproject.toml" ]; then + echo "Source: $PATH_PREFIX/pyproject.toml ⬇️" + pip install -q "$PATH_PREFIX/" + elif [ -f "$PATH_PREFIX/requirements.txt" ]; then + echo "Source: $PATH_PREFIX/requirements.txt ⬇️" + pip install -q -r "$PATH_PREFIX/requirements.txt" else - # Jupyter Notebooks are not necessarily Python projects + # Jupyter Notebooks are not necessarily Python projects echo 'No Python dependencies to install ⚠️' fi - # Set appropriate flags to nbmake - if [ -n "${{ inputs.nbmake_kernel }}" ]; then - echo "Forcing use of a custom kernel: ${{ inputs.nbmake_kernel }} 💬" - echo "kernel_flags=--nbmake-kernel=${{ inputs.nbmake_kernel }}" \ - >> "$GITHUB_ENV" + # Set appropriate flags to nbmake + if [ -n "$NBMAKE_KERNEL" ]; then + echo "Forcing use of a custom kernel: $NBMAKE_KERNEL 💬" + # Use the heredoc output form with a unique delimiter to + # guard against output injection via crafted input values + delimiter="ghadelim_$$_${RANDOM}${RANDOM}${RANDOM}" + { + echo "kernel_flags<<${delimiter}" + echo "--nbmake-kernel=$NBMAKE_KERNEL" + echo "${delimiter}" + } >> "$GITHUB_OUTPUT" fi - # Set flags for parallel testing - if [ "${{ inputs.parallel_tests }}" = 'true' ]; then - echo "flags=-n=auto" >> "$GITHUB_ENV" + # Set flags for parallel testing + if [ "$PARALLEL_TESTS" = 'true' ]; then + echo "flags=-n=auto" >> "$GITHUB_OUTPUT" fi - name: 'Install Pytest parallel test tool' @@ -152,32 +175,43 @@ runs: - name: 'Install nbmake custom kernel' if: inputs.install_kernel == 'true' shell: bash + env: + NBMAKE_KERNEL: "${{ inputs.nbmake_kernel }}" run: | - # Install parallel test tool - echo "Installing custom nbmake kernel: ${{ inputs.nbmake_kernel }} ⬇️" - python -m ipykernel install --user --name "${{ inputs.nbmake_kernel }}" + # Install nbmake custom kernel + echo "Installing custom nbmake kernel: $NBMAKE_KERNEL ⬇️" + python -m ipykernel install --user --name "$NBMAKE_KERNEL" - name: 'Build/test Jupyter Notebooks' shell: bash + env: + PATH_PREFIX: "${{ steps.setup.outputs.path_prefix }}" + PERMIT_FAIL: "${{ inputs.permit_fail }}" + PYTEST_FLAGS: "${{ inputs.pytest_flags }}" + KERNEL_FLAGS: "${{ steps.flags.outputs.kernel_flags }}" + FLAGS: "${{ steps.flags.outputs.flags }}" run: | # Build/Test Jupyter Notebooks echo 'Searching repository for Jupyter Notebooks' - find "${{ env.path_prefix }}/" -name '*.ipynb' - if [ $? -eq 0 ]; then - if [ "f${{ inputs.permit_fail }}" = 'ftrue' ]; then - echo 'Warning: flag set to permit test failures ⚠️' - pytest --nbmake \ - ${{ inputs.pytest_flags }} \ - ${{ env.kernel_flags }} \ - ${{ env.flags }} \ - ${{ env.path_prefix }}/**/*ipynb || true - else - pytest --nbmake \ - ${{ inputs.pytest_flags }} \ - ${{ env.kernel_flags }} \ - ${{ env.flags }} \ - ${{ env.path_prefix }}/**/*ipynb - fi - else + mapfile -t notebooks < <(find "$PATH_PREFIX/" -name '*.ipynb') + if [ "${#notebooks[@]}" -eq 0 ]; then echo 'Error: no Jupyter Notebooks were found ❌'; exit 1 fi + printf '%s\n' "${notebooks[@]}" + # Word splitting of the flag variables below is intentional + if [ "f$PERMIT_FAIL" = 'ftrue' ]; then + echo 'Warning: flag set to permit test failures ⚠️' + # shellcheck disable=SC2086 + pytest --nbmake \ + $PYTEST_FLAGS \ + $KERNEL_FLAGS \ + $FLAGS \ + "${notebooks[@]}" || true + else + # shellcheck disable=SC2086 + pytest --nbmake \ + $PYTEST_FLAGS \ + $KERNEL_FLAGS \ + $FLAGS \ + "${notebooks[@]}" + fi