From ae34a8dcd6f53bdfab4b2057876cb08a18a81f4b Mon Sep 17 00:00:00 2001 From: Steve Beattie Date: Thu, 2 Jul 2026 00:12:06 -0700 Subject: [PATCH 1/6] fix(ci): move template expressions out of run blocks into env Route ${{ inputs.* }} and ${{ steps.*.outputs.* }} values used inside composite-action and reusable-workflow run: blocks through env: aliases and reference them as double-quoted shell variables, closing the template-injection taint path flagged by zizmor in .ci-build.yml and the build-and-publish-{osv,secdb,yaml} composite actions. Refs: PSEC-923 Generated-By: claude-guard chain a10a64a4173b72462b30c8b026a89893 Skills-Applied: template-injection Skills-Sha: d1a637a7f238e262da2698fbdbfd84d56a645e4cf4c63cdfcb3347544f7f2967 Image-Sha: sha256:3b5a6a2d7ac0edcd1da9473f63aef0922371ae7094a2583f4faa5bdef838bc1f --- .github/workflows/.ci-build.yml | 31 ++++++++++++++++++++--------- build-and-publish-osv/action.yaml | 8 ++++++-- build-and-publish-secdb/action.yaml | 11 ++++++++-- build-and-publish-yaml/action.yaml | 5 ++++- 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/.github/workflows/.ci-build.yml b/.github/workflows/.ci-build.yml index 9fe87eab..04b3803c 100644 --- a/.github/workflows/.ci-build.yml +++ b/.github/workflows/.ci-build.yml @@ -127,19 +127,26 @@ jobs: - name: 'Generate local signing key' shell: bash + env: + MELANGE_KEYNAME: ${{ inputs.melange_keyname }} run: | - make MELANGE="melange" ${{ inputs.melange_keyname }} + make MELANGE="melange" "$MELANGE_KEYNAME" - name: 'touch signing key' shell: bash if: ${{ inputs.enterprise == 'true' }} + env: + MELANGE_KEYNAME: ${{ inputs.melange_keyname }} run: | # Touch it with the epoch date to convince `make` that we don't need to # rebuild the targets that depend on this (all) - touch -d @0 ${{ inputs.melange_keyname }} + touch -d @0 "$MELANGE_KEYNAME" - name: 'Prepare package repository' if: ${{ inputs.enterprise == 'true' }} + env: + INPUTS_ARCH: ${{ inputs.arch }} + INPUTS_ENTERPRISE_BUCKET_NAME: ${{ inputs.enterprise_bucket_name }} run: | # yay wolfi! apk add gcsfuse @@ -148,24 +155,26 @@ jobs: # cheerful way to recreate the make targets (class A HEADs) locally # without syncing the whole bucket (class A+B). mkdir -p /gcsfuse/enterprise-registry - gcsfuse -o ro --implicit-dirs --only-dir os ${{ inputs.enterprise_bucket_name }} /gcsfuse/enterprise-registry + gcsfuse -o ro --implicit-dirs --only-dir os "$INPUTS_ENTERPRISE_BUCKET_NAME" /gcsfuse/enterprise-registry - mkdir -p ./packages/${{ inputs.arch }} + mkdir -p "./packages/${INPUTS_ARCH}" # Symlink the gcsfuse mount to ./packages/ and the pub key to workaround the Makefile CWD assumptions - ln -s /gcsfuse/enterprise-registry/${{ inputs.arch }}/*.apk ./packages/${{ inputs.arch }}/ + ln -s /gcsfuse/enterprise-registry/"${INPUTS_ARCH}"/*.apk "./packages/${INPUTS_ARCH}/" # Make a copy of the APKINDEX.* since we'll need to write to it on package builds - cp /gcsfuse/enterprise-registry/${{ inputs.arch }}/APKINDEX.* ./packages/${{ inputs.arch }}/ + cp /gcsfuse/enterprise-registry/"${INPUTS_ARCH}"/APKINDEX.* "./packages/${INPUTS_ARCH}/" # Ensure the file exists at all, so we can upload it as an artifact. touch packages.log - name: 'Build Wolfi' if: ${{ inputs.enterprise == 'false' }} + env: + INPUTS_PACKAGES: ${{ inputs.packages }} run: | # Setup the melange cache dir on the host so we can use that in subsequent builds mkdir ../.melangecache - IFS=',' read -ra packages <<< "${{ inputs.packages }}" + IFS=',' read -ra packages <<< "$INPUTS_PACKAGES" for package in "${packages[@]}"; do make MELANGE_EXTRA_OPTS="--create-build-log --cache-dir=$(pwd)/../.melangecache" REPO="$GITHUB_WORKSPACE/packages" package/"$package" -j1 make REPO="$GITHUB_WORKSPACE/packages" test/"$package" -j1 @@ -173,10 +182,12 @@ jobs: - name: 'Build Enterprise Packages' if: ${{ inputs.enterprise == 'true' }} + env: + INPUTS_PACKAGES: ${{ inputs.packages }} run: | # Setup the melange cache dir on the host so we can use that in subsequent builds mkdir ../.melangecache - IFS=',' read -ra packages <<< "${{ inputs.packages }}" + IFS=',' read -ra packages <<< "$INPUTS_PACKAGES" for package in "${packages[@]}"; do make MELANGE_EXTRA_OPTS="--create-build-log --cache-dir=$(pwd)/../.melangecache --keyring-append=/gcsfuse/enterprise-registry/chainguard-enterprise.rsa.pub" REPO="$GITHUB_WORKSPACE/packages" package/"$package" -j1 make REPO="$GITHUB_WORKSPACE/packages" MELANGE_EXTRA_OPTS="--keyring-append=/gcsfuse/enterprise-registry/chainguard-enterprise.rsa.pub" test/"$package" -j1 @@ -205,6 +216,8 @@ jobs: } >> "$GITHUB_OUTPUT" - name: "Check that packages can be installed with apk add" + env: + FILES_TO_UPLOAD: ${{ steps.list_built_packages.outputs.files_to_upload }} run: | # Create a fake linux fs under /tmp/emptyroot to pass to `apk --root`. mkdir -p /tmp/emptyroot/etc/apk @@ -218,7 +231,7 @@ jobs: apk update --root /tmp/emptyroot # Find .apk files and add them to the string - IFS=',' read -ra files <<< "${{ steps.list_built_packages.outputs.files_to_upload }}" + IFS=',' read -ra files <<< "$FILES_TO_UPLOAD" for f in "${files[@]}"; do tar -Oxf "$f" .PKGINFO apk add --root /tmp/emptyroot --repository ./packages --allow-untrusted --simulate "$f" diff --git a/build-and-publish-osv/action.yaml b/build-and-publish-osv/action.yaml index 3912188d..b359d35d 100644 --- a/build-and-publish-osv/action.yaml +++ b/build-and-publish-osv/action.yaml @@ -74,11 +74,15 @@ runs: - name: 'Upload the security database to a bucket' run: | # Don't cache the security.json. + # ENABLE_ACL_PUBLIC_READ is intentionally unquoted: it is an optional + # flag that is either empty or "--canned-acl=publicRead" and must + # word-split (or vanish when empty). gcloud --quiet alpha storage cp \ - ${{ inputs.enable_acl_public_read }} --cache-control=no-store \ + $ENABLE_ACL_PUBLIC_READ --cache-control=no-store \ './osv/**' \ - gs://${{ env.GCS_APK_BUCKET_NAME }}/${{ env.GCS_APK_DIRECTORY_NAME }}/osv/ + "gs://${GCS_APK_BUCKET_NAME}/${GCS_APK_DIRECTORY_NAME}/osv/" shell: bash env: + ENABLE_ACL_PUBLIC_READ: ${{ inputs.enable_acl_public_read }} GCS_APK_BUCKET_NAME: ${{ inputs.gcs_apk_bucket_name }} GCS_APK_DIRECTORY_NAME: ${{ inputs.gcs_apk_directory_name}} diff --git a/build-and-publish-secdb/action.yaml b/build-and-publish-secdb/action.yaml index edb907c6..1b3b66c7 100644 --- a/build-and-publish-secdb/action.yaml +++ b/build-and-publish-secdb/action.yaml @@ -69,8 +69,15 @@ runs: - name: 'Upload the security database to a bucket' run: | # Don't cache the security.json. + # ENABLE_ACL_PUBLIC_READ is intentionally unquoted: it is an optional + # flag that is either empty or "--canned-acl=publicRead" and must + # word-split (or vanish when empty). gcloud --quiet alpha storage cp \ - ${{ inputs.enable_acl_public_read }} --cache-control=no-store \ + $ENABLE_ACL_PUBLIC_READ --cache-control=no-store \ ./security.json \ - gs://${{ inputs.gcs_apk_bucket_name }}/${{ inputs.gcs_apk_directory_name}}/ + "gs://${GCS_APK_BUCKET_NAME}/${GCS_APK_DIRECTORY_NAME}/" shell: bash + env: + ENABLE_ACL_PUBLIC_READ: ${{ inputs.enable_acl_public_read }} + GCS_APK_BUCKET_NAME: ${{ inputs.gcs_apk_bucket_name }} + GCS_APK_DIRECTORY_NAME: ${{ inputs.gcs_apk_directory_name}} diff --git a/build-and-publish-yaml/action.yaml b/build-and-publish-yaml/action.yaml index 66850360..8b34c2d2 100644 --- a/build-and-publish-yaml/action.yaml +++ b/build-and-publish-yaml/action.yaml @@ -67,5 +67,8 @@ runs: gcloud --quiet alpha storage cp \ --cache-control=no-store \ ./advisories.yaml \ - gs://${{ inputs.gcs_apk_bucket_name }}/${{ inputs.gcs_apk_directory_name}}/ + "gs://${GCS_APK_BUCKET_NAME}/${GCS_APK_DIRECTORY_NAME}/" shell: bash + env: + GCS_APK_BUCKET_NAME: ${{ inputs.gcs_apk_bucket_name }} + GCS_APK_DIRECTORY_NAME: ${{ inputs.gcs_apk_directory_name}} From 431f88bfe3928512156f7b1153e8b421d3e505b4 Mon Sep 17 00:00:00 2001 From: Steve Beattie Date: Thu, 2 Jul 2026 00:12:11 -0700 Subject: [PATCH 2/6] fix(ci): set persist-credentials false on checkout steps Both actions/checkout steps in the ci-build reusable workflow omitted persist-credentials, leaving the GITHUB_TOKEN in the local git config for every subsequent step (zizmor artipacked). No downstream step relies on the git credential store: git operations are limited to a read-only 'git config --global --add safe.directory' and the PR-comment step passes its token explicitly, so persist-credentials: false is the correct fix per the artipacked decision tree. Refs: PSEC-923 Generated-By: claude-guard chain a10a64a4173b72462b30c8b026a89893 Skills-Applied: artipacked Skills-Sha: d1a637a7f238e262da2698fbdbfd84d56a645e4cf4c63cdfcb3347544f7f2967 Image-Sha: sha256:3b5a6a2d7ac0edcd1da9473f63aef0922371ae7094a2583f4faa5bdef838bc1f --- .github/workflows/.ci-build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/.ci-build.yml b/.github/workflows/.ci-build.yml index 04b3803c..336e4d42 100644 --- a/.github/workflows/.ci-build.yml +++ b/.github/workflows/.ci-build.yml @@ -90,6 +90,8 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 if: ${{ inputs.enterprise == 'false' }} + with: + persist-credentials: false - uses: octo-sts/action@f603d3be9d8dd9871a265776e625a27b00effe05 # v1.1.1 @@ -102,6 +104,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 if: ${{ inputs.enterprise == 'true' }} with: + persist-credentials: false ref: ${{ inputs.checkout_sha }} submodules: "recursive" token: ${{ steps.octo-sts.outputs.token }} From 1d0389d8467ad0cc697e406262a0fd81c039150f Mon Sep 17 00:00:00 2001 From: Steve Beattie Date: Thu, 2 Jul 2026 00:12:15 -0700 Subject: [PATCH 3/6] fix(ci): suppress cosmetic zizmor rules and set dependabot cooldown Extend .github/zizmor.yml to disable the cosmetic pedantic-only rules anonymous-definition and concurrency-limits (campaign convention; existing dependabot-cooldown config days:3 preserved). Companion fix for the dependabot-cooldown finding: add cooldown.default-days: 3 to the github-actions ecosystem in .github/dependabot.yml so the configured threshold and the actual cooldown agree. Refs: PSEC-923 Generated-By: claude-guard chain a10a64a4173b72462b30c8b026a89893 Skills-Applied: zizmor-config Skills-Sha: d1a637a7f238e262da2698fbdbfd84d56a645e4cf4c63cdfcb3347544f7f2967 Image-Sha: sha256:3b5a6a2d7ac0edcd1da9473f63aef0922371ae7094a2583f4faa5bdef838bc1f --- .github/dependabot.yml | 2 ++ .github/zizmor.yml | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 60750033..c5645240 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,3 +6,5 @@ updates: directory: "/" schedule: interval: "daily" + cooldown: + default-days: 3 diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 0812a039..e6e988cf 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -7,3 +7,8 @@ rules: dependabot-cooldown: config: days: 3 + # Cosmetic pedantic-only findings — suppressed across the campaign. + anonymous-definition: + disable: true + concurrency-limits: + disable: true From 7357dd2dd095832a6b6d5230fb69f8632ed51b1d Mon Sep 17 00:00:00 2001 From: Steve Beattie Date: Thu, 2 Jul 2026 00:12:19 -0700 Subject: [PATCH 4/6] fix(ci): pass optional gcloud ACL flag as an array (SC2086) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `enable_acl_public_read` input is an optional flag — empty, or a single token like `--canned-acl=publicRead`. It was interpolated unquoted so it would vanish when empty, which trips SC2086 (and quoting it would pass a spurious empty argument when unset). Build it as a bash array instead: `"${acl_args[@]}"` expands to zero args when empty and one quoted arg when set — shellcheck-clean and no reliance on unquoted-empty behavior. Applied to build-and-publish-osv and build-and-publish-secdb. Generated-By: claude-guard --- build-and-publish-osv/action.yaml | 10 ++++++---- build-and-publish-secdb/action.yaml | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/build-and-publish-osv/action.yaml b/build-and-publish-osv/action.yaml index b359d35d..26f02995 100644 --- a/build-and-publish-osv/action.yaml +++ b/build-and-publish-osv/action.yaml @@ -74,11 +74,13 @@ runs: - name: 'Upload the security database to a bucket' run: | # Don't cache the security.json. - # ENABLE_ACL_PUBLIC_READ is intentionally unquoted: it is an optional - # flag that is either empty or "--canned-acl=publicRead" and must - # word-split (or vanish when empty). + # ENABLE_ACL_PUBLIC_READ is an optional flag: empty, or a single token + # like "--canned-acl=publicRead". Build it as an array so it is passed + # as one quoted argument when set and omitted entirely when empty. + acl_args=() + [ -n "$ENABLE_ACL_PUBLIC_READ" ] && acl_args=("$ENABLE_ACL_PUBLIC_READ") gcloud --quiet alpha storage cp \ - $ENABLE_ACL_PUBLIC_READ --cache-control=no-store \ + "${acl_args[@]}" --cache-control=no-store \ './osv/**' \ "gs://${GCS_APK_BUCKET_NAME}/${GCS_APK_DIRECTORY_NAME}/osv/" shell: bash diff --git a/build-and-publish-secdb/action.yaml b/build-and-publish-secdb/action.yaml index 1b3b66c7..d5e71b28 100644 --- a/build-and-publish-secdb/action.yaml +++ b/build-and-publish-secdb/action.yaml @@ -69,11 +69,13 @@ runs: - name: 'Upload the security database to a bucket' run: | # Don't cache the security.json. - # ENABLE_ACL_PUBLIC_READ is intentionally unquoted: it is an optional - # flag that is either empty or "--canned-acl=publicRead" and must - # word-split (or vanish when empty). + # ENABLE_ACL_PUBLIC_READ is an optional flag: empty, or a single token + # like "--canned-acl=publicRead". Build it as an array so it is passed + # as one quoted argument when set and omitted entirely when empty. + acl_args=() + [ -n "$ENABLE_ACL_PUBLIC_READ" ] && acl_args=("$ENABLE_ACL_PUBLIC_READ") gcloud --quiet alpha storage cp \ - $ENABLE_ACL_PUBLIC_READ --cache-control=no-store \ + "${acl_args[@]}" --cache-control=no-store \ ./security.json \ "gs://${GCS_APK_BUCKET_NAME}/${GCS_APK_DIRECTORY_NAME}/" shell: bash From d3df9cc87502629fa738846ddd9f8aec9486ef78 Mon Sep 17 00:00:00 2001 From: Steve Beattie Date: Thu, 2 Jul 2026 00:12:23 -0700 Subject: [PATCH 5/6] fix(ci): quote $TMP and $GITHUB_PATH in install-wolfictl (SC2086) The install step left `$TMP` (a mktemp -d path) unquoted in `docker run -v $TMP:/out` and the `$GITHUB_PATH` runner sink unquoted in `echo "$TMP" >> $GITHUB_PATH`. Both are safe single-value paths; quote them ("$TMP", "$GITHUB_PATH") to satisfy SC2086 and guard against a path containing spaces. Generated-By: claude-guard --- install-wolfictl/action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install-wolfictl/action.yaml b/install-wolfictl/action.yaml index 4005552f..2401b92d 100644 --- a/install-wolfictl/action.yaml +++ b/install-wolfictl/action.yaml @@ -10,6 +10,6 @@ runs: run: | # Copy wolfictl out of the wolfictl image and onto PATH TMP=$(mktemp -d) - docker run --rm -i -v $TMP:/out --entrypoint /bin/sh ghcr.io/wolfi-dev/sdk:latest@sha256:bfe574a465dd54bdec763eac1180abd64beadb426638354a6b34c9948c7cde62 -c "cp /usr/bin/wolfictl /out" - echo "$TMP" >> $GITHUB_PATH + docker run --rm -i -v "$TMP":/out --entrypoint /bin/sh ghcr.io/wolfi-dev/sdk:latest@sha256:bfe574a465dd54bdec763eac1180abd64beadb426638354a6b34c9948c7cde62 -c "cp /usr/bin/wolfictl /out" + echo "$TMP" >> "$GITHUB_PATH" shell: bash From 75acbc69750a14b5367f012666fedfca9d203ece Mon Sep 17 00:00:00 2001 From: Steve Beattie Date: Thu, 2 Jul 2026 00:12:28 -0700 Subject: [PATCH 6/6] fix(ci): document benign github-env write in install-wolfictl (zizmor ignore) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit zizmor flags `echo "$TMP" >> "$GITHUB_PATH"` as github-env (dangerous env-file write). Here it is a false positive: $TMP is a `mktemp -d` path (runner-owned, mode 0700) that holds only wolfictl copied from the pinned wolfi-dev/sdk image — no attacker-controlled value reaches $GITHUB_PATH, and the temp dir added to PATH is not attacker-writable. Add a scoped `# zizmor: ignore[github-env]` with the justification inline, rather than leave a recurring HIGH on a reviewed-benign line. Generated-By: claude-guard --- install-wolfictl/action.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install-wolfictl/action.yaml b/install-wolfictl/action.yaml index 2401b92d..fd2dc001 100644 --- a/install-wolfictl/action.yaml +++ b/install-wolfictl/action.yaml @@ -7,7 +7,7 @@ runs: using: "composite" steps: - name: "Install wolfictl onto PATH" - run: | + run: | # zizmor: ignore[github-env] $TMP is a trusted mktemp -d path (runner-owned, 0700, holds only wolfictl from the pinned sdk image); no attacker-controlled value reaches $GITHUB_PATH # Copy wolfictl out of the wolfictl image and onto PATH TMP=$(mktemp -d) docker run --rm -i -v "$TMP":/out --entrypoint /bin/sh ghcr.io/wolfi-dev/sdk:latest@sha256:bfe574a465dd54bdec763eac1180abd64beadb426638354a6b34c9948c7cde62 -c "cp /usr/bin/wolfictl /out"