From fd0999a8d26fd127486a7c83dbb03a1d04d8bc92 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Fri, 8 May 2026 19:33:20 +0200 Subject: [PATCH 1/5] Add cooldown configuration to Dependabot updates Adds 7-day cooldown period to all Dependabot package ecosystem entries to prevent excessive update frequency. Applied by: zizmor v1.24.1 --fix (dependabot-cooldown rule) --- .github/dependabot.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index da82eaa..f20c6bc 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,22 +4,32 @@ updates: directory: /canary-release schedule: interval: weekly + cooldown: + default-days: 7 - package-ecosystem: github-actions directory: /check-cla schedule: interval: weekly + cooldown: + default-days: 7 - package-ecosystem: github-actions directory: /read-yaml schedule: interval: weekly + cooldown: + default-days: 7 - package-ecosystem: github-actions directory: /set-commit-status schedule: interval: weekly + cooldown: + default-days: 7 - package-ecosystem: pip directory: / schedule: interval: weekly + cooldown: + default-days: 7 - package-ecosystem: github-actions directory: /.github/workflows schedule: @@ -28,6 +38,8 @@ updates: workflows: patterns: - '*' + cooldown: + default-days: 7 - package-ecosystem: pre-commit directory: / schedule: @@ -36,3 +48,5 @@ updates: pre-commit: patterns: - '*' + cooldown: + default-days: 7 From 7b481c836d8688f7473ab87b111ae74efdb36893 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Fri, 8 May 2026 19:37:11 +0200 Subject: [PATCH 2/5] Fix template injection vulnerabilities in actions Replace direct template expansions (${{ inputs.* }}) in run blocks, github-script steps, and python steps with environment variables. Inputs are now passed via env vars which are properly escaped by the shell/runtime. Detected by: zizmor v1.24.1 (template-injection rule) --- canary-release/action.yml | 23 +++++++++++++++-------- check-cla/action.yml | 18 +++++++++++++----- combine-durations/action.yml | 20 +++++++++++++------- create-fork/action.yml | 6 ++++-- read-file/action.yml | 13 ++++++++----- read-yaml/action.yml | 7 +++++-- set-commit-status/action.yml | 13 +++++++++---- template-files/action.yml | 6 ++++-- 8 files changed, 71 insertions(+), 35 deletions(-) diff --git a/canary-release/action.yml b/canary-release/action.yml index d126d97..aac52a7 100644 --- a/canary-release/action.yml +++ b/canary-release/action.yml @@ -65,6 +65,13 @@ runs: shell: bash -l {0} env: BINSTAR_API_TOKEN: ${{ inputs.anaconda-org-token }} + INPUT_BUILD_ARGS: ${{ inputs.conda-build-arguments }} + INPUT_BUILD_PATH: ${{ inputs.conda-build-path }} + INPUT_SUBDIR: ${{ inputs.subdir }} + INPUT_PACKAGE_NAME: ${{ inputs.package-name }} + INPUT_UPLOAD: ${{ inputs.upload }} + INPUT_CHANNEL: ${{ inputs.anaconda-org-channel }} + INPUT_LABEL: ${{ inputs.anaconda-org-label }} run: | echo "::group::Setting up environment" set -euo pipefail @@ -89,36 +96,36 @@ runs: echo "::endgroup::" echo "::group::Building package" - conda build --croot=./pkgs ${{ inputs.conda-build-arguments }} ${{ inputs.conda-build-path }} + conda build --croot=./pkgs ${INPUT_BUILD_ARGS} "${INPUT_BUILD_PATH}" echo "::endgroup::" echo "::group::Find packages" PACKAGES=( $( - find "./pkgs/${{ inputs.subdir }}" -type f \ + find "./pkgs/${INPUT_SUBDIR}" -type f \ \( \ - -name "${{ inputs.package-name }}-*.tar.bz2" -o \ - -name "${{ inputs.package-name }}-*.conda" \ + -name "${INPUT_PACKAGE_NAME}-*.tar.bz2" -o \ + -name "${INPUT_PACKAGE_NAME}-*.conda" \ \) ) ) echo "::endgroup::" - if [[ "${{ inputs.upload }}" == "true" ]]; then + if [[ "${INPUT_UPLOAD}" == "true" ]]; then echo "::group::Uploading package" anaconda \ upload \ --force \ --register \ --no-progress \ - --user="${{ inputs.anaconda-org-channel }}" \ - --label="${{ inputs.anaconda-org-label }}" \ + --user="${INPUT_CHANNEL}" \ + --label="${INPUT_LABEL}" \ "${PACKAGES[@]}" echo "Uploaded the following files:" basename -a "${PACKAGES[@]}" echo "::endgroup::" echo "Use this command to try out the build:" - echo "conda install -c ${{ inputs.anaconda-org-channel }}/label/${{ inputs.anaconda-org-label }} ${{ inputs.package-name }}" + echo "conda install -c ${INPUT_CHANNEL}/label/${INPUT_LABEL} ${INPUT_PACKAGE_NAME}" else echo "Skipping upload because 'upload != true'." fi diff --git a/check-cla/action.yml b/check-cla/action.yml index fc5d888..b869f65 100644 --- a/check-cla/action.yml +++ b/check-cla/action.yml @@ -53,6 +53,10 @@ runs: - name: Collect PR metadata uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 id: metadata + env: + INPUT_LABEL: ${{ inputs.label }} + INPUT_CLA_REPO: ${{ inputs.cla_repo }} + INPUT_CLA_PATH: ${{ inputs.cla_path }} with: github-token: ${{ inputs.token }} script: | @@ -70,15 +74,15 @@ runs: const labels = raw.data.labels.map(label => label.name); core.debug(`labels: ${labels}`); - const has_label = labels.includes('${{ inputs.label }}'); + const has_label = labels.includes(process.env.INPUT_LABEL); core.setOutput('has_label', has_label); core.debug(`has_label: ${has_label}`); - const cla_repo = '${{ inputs.cla_repo }}'.split('/', 2); + const cla_repo = process.env.INPUT_CLA_REPO.split('/', 2); const { content, encoding } = (await github.rest.repos.getContent({ owner: cla_repo[0], repo: cla_repo[1], - path: '${{ inputs.cla_path }}' + path: process.env.INPUT_CLA_PATH })).data; const contributors = JSON.parse( Buffer.from(content, encoding).toString('utf-8') @@ -125,13 +129,17 @@ runs: - name: Add contributor as a CLA signee shell: python if: steps.metadata.outputs.has_signed == 'false' + env: + INPUT_CLA_PATH: ${{ inputs.cla_path }} + INPUT_CONTRIBUTOR: ${{ steps.metadata.outputs.contributor }} run: | import json + import os from pathlib import Path - path = Path("${{ inputs.cla_path }}") + path = Path(os.environ["INPUT_CLA_PATH"]) signees = json.loads(path.read_text()) - signees["contributors"].append("${{ steps.metadata.outputs.contributor }}") + signees["contributors"].append(os.environ["INPUT_CONTRIBUTOR"]) signees["contributors"].sort(key=str.lower) path.write_text(json.dumps(signees, indent=2) + "\n") diff --git a/combine-durations/action.yml b/combine-durations/action.yml index d01d9f8..706538a 100644 --- a/combine-durations/action.yml +++ b/combine-durations/action.yml @@ -38,20 +38,24 @@ runs: shell: bash run: > gh run list - --repo ${{ inputs.repository }} - --branch ${{ inputs.branch }} - --workflow ${{ inputs.workflow }} + --repo "$INPUT_REPOSITORY" + --branch "$INPUT_BRANCH" + --workflow "$INPUT_WORKFLOW" --limit 10 --json databaseId --jq '.[].databaseId' | xargs -n 1 gh run download - --repo ${{ inputs.repository }} - --dir ${{ runner.temp }}/artifacts/ - --pattern '${{ inputs.pattern }}' + --repo "$INPUT_REPOSITORY" + --dir "${{ runner.temp }}/artifacts/" + --pattern "$INPUT_PATTERN" || true env: GITHUB_TOKEN: ${{ github.token }} + INPUT_REPOSITORY: ${{ inputs.repository }} + INPUT_BRANCH: ${{ inputs.branch }} + INPUT_WORKFLOW: ${{ inputs.workflow }} + INPUT_PATTERN: ${{ inputs.pattern }} # `hashFiles` only works on files within the working directory, since `requirements.txt` # is not in the working directory we need to manually compute the SHA256 hash @@ -84,5 +88,7 @@ runs: shell: bash run: > python ${{ github.action_path }}/combine_durations.py - --durations-dir=${{ inputs.durations-dir }} + --durations-dir="$INPUT_DURATIONS_DIR" --artifacts-dir=${{ runner.temp }}/artifacts/ + env: + INPUT_DURATIONS_DIR: ${{ inputs.durations-dir }} diff --git a/create-fork/action.yml b/create-fork/action.yml index 98f9689..7d7a64c 100644 --- a/create-fork/action.yml +++ b/create-fork/action.yml @@ -33,7 +33,7 @@ runs: RESPONSE=$(gh api \ -X POST \ -H "Accept: application/vnd.github+json" \ - "/repos/${{ inputs.repository }}/forks" \ + "/repos/${INPUT_REPOSITORY}/forks" \ -f default_branch_only=true) # extract values with jq @@ -43,9 +43,11 @@ runs: # wait a minute to ensure the fork is ready TIMESTAMP="$(date -d "${CREATED_AT}" +%s)" CURRENT="$(date +%s)" - [ $((CURRENT - TIMESTAMP)) -gt 60 ] || sleep ${{ inputs.timeout }} + [ $((CURRENT - TIMESTAMP)) -gt 60 ] || sleep "${INPUT_TIMEOUT}" # store values for subsequent usage echo fork="${FULL_NAME}" >> $GITHUB_OUTPUT env: GH_TOKEN: ${{ inputs.token }} + INPUT_REPOSITORY: ${{ inputs.repository }} + INPUT_TIMEOUT: ${{ inputs.timeout }} diff --git a/read-file/action.yml b/read-file/action.yml index 5398909..7309797 100644 --- a/read-file/action.yml +++ b/read-file/action.yml @@ -50,10 +50,13 @@ runs: - name: Read JSON id: read shell: bash - run: > - python ${{ github.action_path }}/read_file.py - ${{ inputs.path }} - ${{ inputs.parser && format('"--parser={0}"', inputs.parser) || '' }} - ${{ inputs.default && format('"--default={0}"', inputs.default) || '' }} + run: | + ARGS=("$INPUT_PATH") + [ -n "$INPUT_PARSER" ] && ARGS+=("--parser=$INPUT_PARSER") + [ -n "$INPUT_DEFAULT" ] && ARGS+=("--default=$INPUT_DEFAULT") + python ${{ github.action_path }}/read_file.py "${ARGS[@]}" env: GITHUB_TOKEN: ${{ github.token }} + INPUT_PATH: ${{ inputs.path }} + INPUT_PARSER: ${{ inputs.parser }} + INPUT_DEFAULT: ${{ inputs.default }} diff --git a/read-yaml/action.yml b/read-yaml/action.yml index f1b2c90..03c88a4 100644 --- a/read-yaml/action.yml +++ b/read-yaml/action.yml @@ -22,6 +22,9 @@ runs: shell: bash -l {0} - id: read_yaml uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_PATH: ${{ inputs.path }} + INPUT_KEY: ${{ inputs.key }} with: script: | const yaml = require('js-yaml'); @@ -90,8 +93,8 @@ runs: } async function main() { - const path = "${{ inputs.path }}"; - const key = `${{ inputs.key }}`.trim(); + const path = process.env.INPUT_PATH; + const key = (process.env.INPUT_KEY || '').trim(); let value = await readYaml(path); if (key) { diff --git a/set-commit-status/action.yml b/set-commit-status/action.yml index a842f63..13f05dc 100644 --- a/set-commit-status/action.yml +++ b/set-commit-status/action.yml @@ -25,6 +25,11 @@ runs: using: composite steps: - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_CONTEXT: ${{ inputs.context }} + INPUT_DESCRIPTION: ${{ inputs.description }} + INPUT_STATE: ${{ inputs.state }} + INPUT_TARGET_URL: ${{ inputs.target_url }} with: github-token: ${{ inputs.token }} script: | @@ -39,12 +44,12 @@ runs: core.debug(`sha: ${sha}`); const { context: name, state } = (await github.rest.repos.createCommitStatus({ - context: '${{ inputs.context }}', - description: '${{ inputs.description }}', + context: process.env.INPUT_CONTEXT, + description: process.env.INPUT_DESCRIPTION, owner: owner, repo: repo, sha: sha, - state: '${{ inputs.state }}', - target_url: '${{ inputs.target_url }}' + state: process.env.INPUT_STATE, + target_url: process.env.INPUT_TARGET_URL })).data; core.info(`${name} is ${state}`); diff --git a/template-files/action.yml b/template-files/action.yml index 73ca89a..c9b35b2 100644 --- a/template-files/action.yml +++ b/template-files/action.yml @@ -56,7 +56,9 @@ runs: shell: bash run: > python ${{ github.action_path }}/template_files.py - --config ${{ inputs.config }} - --stubs ${{ inputs.stubs }} + --config "$INPUT_CONFIG" + --stubs "$INPUT_STUBS" env: GITHUB_TOKEN: ${{ github.token }} + INPUT_CONFIG: ${{ inputs.config }} + INPUT_STUBS: ${{ inputs.stubs }} From 105a56017d35b5b12c6575756d554c4958b263be Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Fri, 8 May 2026 19:37:17 +0200 Subject: [PATCH 3/5] Add persist-credentials and explicit permissions to tests.yml - Add persist-credentials: false to all checkout steps - Add workflow-level permissions: contents: read - Add job-level permissions for template-files and analyze jobs that need write access to issues/pull-requests - Fix template injection in read-file test assertions by using environment variables instead of direct template expansion Detected by: zizmor v1.24.1 (artipacked, excessive-permissions, template-injection rules) --- .github/workflows/tests.yml | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1e697dd..c24aec8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,6 +13,9 @@ on: # https://crontab.guru/#15_14_*_*_* - cron: 15 14 * * * +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -23,6 +26,8 @@ jobs: steps: - name: Checkout Source uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Cache Pip uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 @@ -52,6 +57,8 @@ jobs: steps: - name: Checkout Source uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Read Remote JSON id: json @@ -80,15 +87,27 @@ jobs: - name: Run Tests shell: python + env: + JSON_CONTENT: ${{ steps.json.outputs.content }} + YAML_CONTENT: ${{ steps.yaml.outputs.content }} + JSON_FOO: ${{ fromJSON(steps.json.outputs.content)['foo'] }} + YAML_FOO: ${{ fromJSON(steps.yaml.outputs.content)['foo'] }} run: | - assert '''${{ steps.json.outputs.content }}''' == '''${{ steps.yaml.outputs.content }}''' - assert '''${{ fromJSON(steps.json.outputs.content)['foo'] }}''' == '''${{ fromJSON(steps.yaml.outputs.content)['foo'] }}''' + import os + assert os.environ['JSON_CONTENT'] == os.environ['YAML_CONTENT'] + assert os.environ['JSON_FOO'] == os.environ['YAML_FOO'] template-files: runs-on: ubuntu-latest + permissions: + contents: read + issues: write + pull-requests: write steps: - name: Checkout Source uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Template Success id: templates-success @@ -136,6 +155,9 @@ jobs: needs: [pytest, read-file, template-files] if: '!cancelled()' runs-on: ubuntu-latest + permissions: + contents: read + issues: write steps: - name: Determine Success uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 @@ -146,6 +168,8 @@ jobs: - name: Checkout our source if: always() && github.event_name != 'pull_request' && steps.alls-green.outputs.result == 'failure' uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Report failures if: always() && github.event_name != 'pull_request' && steps.alls-green.outputs.result == 'failure' From d88b54e075b70c1138a7d53ebca36bf169fe7470 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Fri, 8 May 2026 19:37:34 +0200 Subject: [PATCH 4/5] Add zizmor.yml to suppress findings in synced workflows Workflows synced from conda/infrastructure (cla.yml, issues.yml, labels.yml, project.yml, stale.yml) cannot be modified in this repo. Suppress their findings here; fixes should be applied upstream. --- check-cla/action.yml | 1 + zizmor.yml | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 zizmor.yml diff --git a/check-cla/action.yml b/check-cla/action.yml index b869f65..b631711 100644 --- a/check-cla/action.yml +++ b/check-cla/action.yml @@ -124,6 +124,7 @@ runs: if: steps.metadata.outputs.has_signed == 'false' with: repository: ${{ inputs.cla_repo }} + persist-credentials: false # if unsigned, update cla_path - name: Add contributor as a CLA signee diff --git a/zizmor.yml b/zizmor.yml new file mode 100644 index 0000000..9c53ad4 --- /dev/null +++ b/zizmor.yml @@ -0,0 +1,43 @@ +# zizmor configuration file +# https://docs.zizmor.sh/configuration/ +# +# Workflows synced from conda/infrastructure (cla.yml, issues.yml, +# labels.yml, project.yml, stale.yml) cannot be modified in this +# repo. Findings in those files are suppressed here; fixes should +# be applied upstream in conda/infrastructure. + +rules: + dangerous-triggers: + ignore: + # cla.yml uses pull_request_target but does not check out + # untrusted PR code; it only reacts to issue_comment and + # pull_request_target events to run the CLA check action. + # Synced from conda/infrastructure. + - cla.yml + + # project.yml uses pull_request_target but does not check out + # any code; it only adds PRs to a GitHub project. + # Synced from conda/infrastructure. + - project.yml + + excessive-permissions: + ignore: + # Synced from conda/infrastructure; permissions should be + # tightened upstream. + - cla.yml + - issues.yml + - labels.yml + - project.yml + - update.yml + + artipacked: + ignore: + # Synced from conda/infrastructure. + - labels.yml + - update.yml + + template-injection: + ignore: + # Synced from conda/infrastructure. + - stale.yml + - update.yml From 0825b40192f9f3769ebd0b943537607b1445c0ed Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Fri, 8 May 2026 19:39:09 +0200 Subject: [PATCH 5/5] Add zizmor and actionlint pre-commit hooks Catches GitHub Actions security and syntax issues before they reach CI. Suggested by @dbast in #361. --- .pre-commit-config.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5da94e8..543e891 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -51,6 +51,15 @@ repos: hooks: - id: codespell args: [--write-changes] + - repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: a4727cbbcd26d7098e96b9cb738169b59711ae51 # frozen: v1.24.1 + hooks: + - id: zizmor + - repo: https://github.com/rhysd/actionlint + rev: 914e7df21a07ef503a81201c76d2b11c789d3fca # frozen: v1.7.12 + hooks: + - id: actionlint + exclude: ^\.github/workflows/(stale|cla|issues|labels|project|update)\.yml$ - repo: meta # see https://pre-commit.com/#meta-hooks hooks: