|
name: 'GitHub Actions Scan' |
|
|
|
on: |
|
workflow_call: |
|
inputs: |
|
wif_provider: |
|
type: 'string' |
|
zizmor_result_bucket: |
|
type: 'string' |
|
zizmor_config_bucket: |
|
type: 'string' |
|
zizmor_enforce: |
|
type: 'string' |
|
# zizmor: ignore[dangerous-triggers] Required to have `id-token: write` for GCP auth. |
|
pull_request_target: |
|
merge_group: |
|
|
|
permissions: {} |
|
|
|
env: |
|
ZIZMOR_VERSION: '1.25.2' |
|
ZIZMOR_DOCKER_DIGEST: 'sha256:14ea7f5cc7c67933394a35b5a38a277397818d232602635edb2010b313afb110' |
|
ZIZMOR_ENFORCE: >- |
|
${{ inputs.zizmor_enforce != '' && inputs.zizmor_enforce || 'false' }} |
|
ZIZMOR_RESULT_BUCKET: >- |
|
${{ inputs.zizmor_result_bucket != '' && inputs.zizmor_result_bucket || 'zizmor-7165' }} |
|
ZIZMOR_CONFIG_BUCKET: >- |
|
${{ inputs.zizmor_config_bucket != '' && inputs.zizmor_config_bucket || 'zizmor-configs-cc63' }} |
|
WIF_PROVIDER: >- |
|
${{ inputs.wif_provider != '' && inputs.wif_provider || 'projects/102295818544/locations/global/workloadIdentityPools/gitsec-gha-artifacts/providers/gitsec-gha-artifacts-provider' }} |
|
|
|
jobs: |
|
check-changes: |
|
runs-on: 'ubuntu-latest' |
|
if: github.event_name != 'merge_group' |
|
outputs: |
|
files: '${{ steps.check.outputs.files }}' |
|
permissions: |
|
contents: 'read' |
|
steps: |
|
- name: 'Print Help Link' |
|
shell: 'bash' |
|
run: | |
|
echo "For Googlers: Refer to go/github-zizmor-help for internal guidance on resolving issues." |
|
- name: 'Checkout source' |
|
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # ratchet:actions/checkout@v6 |
|
with: |
|
ref: '${{ github.event.pull_request.head.sha }}' |
|
fetch-depth: 0 |
|
persist-credentials: false |
|
- name: 'Check for workflow changes' |
|
id: 'check' |
|
shell: 'bash' |
|
env: |
|
GIT_HEAD_SHA: '${{ github.event.pull_request.head.sha }}' |
|
GIT_BASE_SHA: '${{ github.event.pull_request.base.sha }}' |
|
run: | |
|
files=$(git diff --name-only --diff-filter=d "${GIT_BASE_SHA}" "${GIT_HEAD_SHA}" | grep -E '^\.github/(workflows/.+\.ya?ml|dependabot\.ya?ml)$' | xargs || true) |
|
echo "files=$files" >> "$GITHUB_OUTPUT" |
|
|
|
zizmor-config: |
|
needs: ['check-changes'] |
|
runs-on: 'ubuntu-latest' |
|
if: >- |
|
needs.check-changes.outputs.files != '' |
|
permissions: |
|
id-token: 'write' |
|
outputs: |
|
mandatory-checks: '${{ steps.identify-mandatory.outputs.mandatory-checks }}' |
|
enforce: '${{ steps.identify-enforcement.outputs.enforce }}' |
|
steps: |
|
- name: 'Authenticate to GCP' |
|
uses: 'google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed' # ratchet:google-github-actions/auth@v2 |
|
id: 'gcp-auth' |
|
with: |
|
workload_identity_provider: '${{ env.WIF_PROVIDER }}' |
|
- name: 'Download zizmor config' |
|
shell: 'bash' |
|
env: |
|
REPO_PATH: '${{ github.repository }}' |
|
ORG_PATH: '${{ github.repository_owner }}' |
|
GOOGLE_ACCESS_TOKEN: '${{ steps.gcp-auth.outputs.auth_token }}' |
|
run: | |
|
if [ -z "${ZIZMOR_CONFIG_BUCKET}" ]; then |
|
echo "::error::No config bucket configured. Config bucket is required." |
|
exit 1 |
|
fi |
|
|
|
gcs_download() { |
|
local src_path="$1" |
|
local dst_path="$2" |
|
|
|
curl -f -sS -L \ |
|
-H "Authorization: Bearer ${GOOGLE_ACCESS_TOKEN}" \ |
|
-o "$dst_path" \ |
|
"https://storage.googleapis.com/${src_path}" |
|
} |
|
|
|
if gcs_download "${ZIZMOR_CONFIG_BUCKET}/${REPO_PATH}.yaml" zizmor-config.yaml &>/dev/null; then |
|
echo "Using repository config: ${ZIZMOR_CONFIG_BUCKET}/${REPO_PATH}.yaml" |
|
elif gcs_download "${ZIZMOR_CONFIG_BUCKET}/${ORG_PATH}.yaml" zizmor-config.yaml &>/dev/null; then |
|
echo "Using organization config: ${ZIZMOR_CONFIG_BUCKET}/${ORG_PATH}.yaml" |
|
else |
|
echo "Attempting to fetch default config: ${ZIZMOR_CONFIG_BUCKET}/default.yaml" |
|
if ! gcs_download "${ZIZMOR_CONFIG_BUCKET}/default.yaml" zizmor-config.yaml; then |
|
echo "::error::Failed to download default config. Crucial step failed." |
|
exit 1 |
|
fi |
|
fi |
|
- name: 'Identify mandatory checks' |
|
id: 'identify-mandatory' |
|
shell: 'bash' |
|
run: | |
|
MANDATORY_LIST=$(yq eval '[.rules | to_entries | .[] | select(.value.remap.severity == "mandatory" and .value.disable != true) | .key]' -o json --indent 0 zizmor-config.yaml) |
|
echo "mandatory-checks=${MANDATORY_LIST}" >> "$GITHUB_OUTPUT" |
|
- name: 'Identify enforcement status checks' |
|
id: 'identify-enforcement' |
|
shell: 'bash' |
|
run: | |
|
FINAL_ENFORCE="${ZIZMOR_ENFORCE}" |
|
CONFIG_ENFORCE="$(yq eval '.google.enforce' zizmor-config.yaml)" |
|
if [[ "${CONFIG_ENFORCE}" = "true" || "${CONFIG_ENFORCE}" = "false" ]]; then |
|
echo "Using enforcment value from config: ${CONFIG_ENFORCE}" |
|
FINAL_ENFORCE="${CONFIG_ENFORCE}" |
|
fi |
|
echo "enforce=${FINAL_ENFORCE}" >> "$GITHUB_OUTPUT" |
|
- name: 'Remove non-standard google key' |
|
shell: 'bash' |
|
run: | |
|
yq 'del(.google)' -i zizmor-config.yaml |
|
- name: 'Rewrite mandatory to high in config' |
|
shell: 'bash' |
|
run: | |
|
yq '(.rules.[] | select(.remap.severity == "mandatory") | .remap.severity) = "high"' -i zizmor-config.yaml |
|
- name: 'Save config' |
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # ratchet:actions/upload-artifact@v7 |
|
with: |
|
name: 'zizmor-config' |
|
path: 'zizmor-config.yaml' |
|
if-no-files-found: 'error' |
|
retention-days: 1 |
|
|
|
zizmor-scan: |
|
needs: ['check-changes', 'zizmor-config'] |
|
runs-on: 'ubuntu-latest' |
|
if: >- |
|
needs.check-changes.outputs.files != '' && |
|
( |
|
inputs.wif_provider != '' || |
|
( |
|
github.event_name == 'pull_request_target' && |
|
!startsWith(github.workflow_ref, format('{0}/', github.repository)) |
|
) |
|
) |
|
permissions: |
|
contents: 'read' |
|
outputs: |
|
offline: '${{ steps.run-zizmor.outputs.offline }}' |
|
steps: |
|
- name: 'Checkout source' |
|
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # ratchet:actions/checkout@v6 |
|
with: |
|
ref: '${{ github.event.pull_request.head.sha }}' |
|
persist-credentials: false |
|
- name: 'Download config' |
|
uses: 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' # ratchet:actions/download-artifact@v8 |
|
with: |
|
name: 'zizmor-config' |
|
- name: 'Run zizmor' |
|
id: 'run-zizmor' |
|
shell: 'bash' |
|
env: |
|
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}' |
|
CHANGED_FILES: '${{ needs.check-changes.outputs.files }}' |
|
run: | |
|
read -r -a FILES_ARR <<< "${CHANGED_FILES}" |
|
for i in "${!FILES_ARR[@]}"; do |
|
[ -n "${FILES_ARR[i]}" ] || unset "FILES_ARR[i]" |
|
done |
|
|
|
set +e |
|
docker run \ |
|
--rm \ |
|
--volume "${GITHUB_WORKSPACE}:/workspace:ro" \ |
|
--workdir "/workspace" \ |
|
--env GH_TOKEN="${GH_TOKEN}" \ |
|
"ghcr.io/zizmorcore/zizmor:${ZIZMOR_VERSION}@${ZIZMOR_DOCKER_DIGEST}" \ |
|
--config /workspace/zizmor-config.yaml \ |
|
--format sarif \ |
|
--no-ignores \ |
|
-- \ |
|
"${FILES_ARR[@]}" \ |
|
> zizmor.sarif.json 2> zizmor.err |
|
EXIT_CODE=$? |
|
set -e |
|
|
|
OFFLINE_MODE="false" |
|
if [ $EXIT_CODE -ne 0 ]; then |
|
if grep -q "missing or you have no access" zizmor.err; then |
|
echo "::warning::Zizmor token has insufficient permissions. Falling back to offline scan." |
|
docker run \ |
|
--rm \ |
|
--volume "${GITHUB_WORKSPACE}:/workspace:ro" \ |
|
--workdir "/workspace" \ |
|
"ghcr.io/zizmorcore/zizmor:${ZIZMOR_VERSION}@${ZIZMOR_DOCKER_DIGEST}" \ |
|
--config /workspace/zizmor-config.yaml \ |
|
--format sarif \ |
|
--no-ignores \ |
|
-- \ |
|
"${FILES_ARR[@]}" \ |
|
> zizmor.sarif.json |
|
OFFLINE_MODE="true" |
|
else |
|
echo "Zizmor scan failed with error:" >&2 |
|
cat zizmor.err >&2 |
|
exit $EXIT_CODE |
|
fi |
|
fi |
|
|
|
echo "offline=${OFFLINE_MODE}" >> "$GITHUB_OUTPUT" |
|
- name: 'Enrich SARIF with GitHub metadata' |
|
shell: 'bash' |
|
env: |
|
OFFLINE_MODE: '${{ steps.run-zizmor.outputs.offline }}' |
|
run: >- |
|
jq |
|
--arg uri "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" |
|
--arg sha "${GITHUB_SHA}" |
|
--argjson repo_id "${GITHUB_REPOSITORY_ID}" |
|
--argjson owner_id "${GITHUB_REPOSITORY_OWNER_ID}" |
|
--arg run_id "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/attempts/${GITHUB_RUN_ATTEMPT}" |
|
--argjson offline "${OFFLINE_MODE:-false}" |
|
'.runs[] |= . + { |
|
"versionControlProvenance": [ |
|
{ |
|
"repositoryUri": $uri, |
|
"revisionId": $sha, |
|
"properties": { |
|
"github_repository_id": $repo_id, |
|
"github_owner_id": $owner_id |
|
} |
|
} |
|
], |
|
"invocations": [ |
|
{ |
|
"executionSuccessful": true, |
|
"properties": { |
|
"execution_id": $run_id, |
|
"offline": $offline |
|
} |
|
} |
|
] |
|
}' zizmor.sarif.json > enriched.sarif.json |
|
- name: 'Save result' |
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # ratchet:actions/upload-artifact@v7 |
|
with: |
|
name: 'zizmor' |
|
path: 'enriched.sarif.json' |
|
if-no-files-found: 'error' |
|
retention-days: 1 |
|
|
|
zizmor-upload: |
|
needs: ['zizmor-scan'] |
|
runs-on: 'ubuntu-latest' |
|
permissions: |
|
id-token: 'write' |
|
steps: |
|
- name: 'Retrieve result' |
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # ratchet:actions/download-artifact@v8 |
|
with: |
|
name: 'zizmor' |
|
- name: 'Authenticate to GCP' |
|
id: 'gcp-auth' |
|
uses: 'google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed' # ratchet:google-github-actions/auth@v2 |
|
with: |
|
workload_identity_provider: '${{ env.WIF_PROVIDER }}' |
|
- name: 'Upload result' |
|
shell: 'bash' |
|
env: |
|
GITHUB_PULL_REQUEST_NUMBER: '${{ github.event.pull_request.number }}' |
|
GOOGLE_ACCESS_TOKEN: '${{ steps.gcp-auth.outputs.auth_token }}' |
|
run: | |
|
gcs_upload() { |
|
local src_path="$1" |
|
local dst_path="$2" |
|
|
|
curl -f -sS -L \ |
|
-T "$src_path" \ |
|
-H "Authorization: Bearer ${GOOGLE_ACCESS_TOKEN}" \ |
|
"https://storage.googleapis.com/${dst_path}" |
|
} |
|
|
|
gcs_upload \ |
|
enriched.sarif.json \ |
|
"${ZIZMOR_RESULT_BUCKET}/${GITHUB_REPOSITORY}/${GITHUB_PULL_REQUEST_NUMBER}_${GITHUB_RUN_ID}_${GITHUB_RUN_ATTEMPT}.sarif.json" |
|
|
|
zizmor-output: |
|
needs: ['check-changes', 'zizmor-scan', 'zizmor-config'] |
|
runs-on: 'ubuntu-latest' |
|
permissions: |
|
contents: 'read' |
|
if: >- |
|
needs.zizmor-config.outputs.enforce == 'true' |
|
steps: |
|
- name: 'Checkout source' |
|
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # ratchet:actions/checkout@v6 |
|
with: |
|
ref: '${{ github.event.pull_request.head.sha }}' |
|
persist-credentials: false |
|
- name: 'Download config' |
|
uses: 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' # ratchet:actions/download-artifact@v8 |
|
with: |
|
name: 'zizmor-config' |
|
- name: 'Retrieve result' |
|
uses: 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' # ratchet:actions/download-artifact@v8 |
|
with: |
|
name: 'zizmor' |
|
- name: 'Re-run zizmor with github format' |
|
id: 'run-zizmor' |
|
shell: 'bash' |
|
env: |
|
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}' |
|
CHANGED_FILES: '${{ needs.check-changes.outputs.files }}' |
|
OFFLINE_MODE: '${{ needs.zizmor-scan.outputs.offline }}' |
|
run: | |
|
set +e |
|
read -r -a FILES_ARR <<< "${CHANGED_FILES}" |
|
for i in "${!FILES_ARR[@]}"; do |
|
[ -n "${FILES_ARR[i]}" ] || unset "FILES_ARR[i]" |
|
done |
|
|
|
if [ "${OFFLINE_MODE}" = "true" ]; then |
|
echo "Running zizmor offline because the initial scan was offline." |
|
docker run \ |
|
--rm \ |
|
--volume "${GITHUB_WORKSPACE}:/workspace:ro" \ |
|
--workdir "/workspace" \ |
|
"ghcr.io/zizmorcore/zizmor:${ZIZMOR_VERSION}@${ZIZMOR_DOCKER_DIGEST}" \ |
|
--config /workspace/zizmor-config.yaml \ |
|
--format github \ |
|
--min-confidence medium \ |
|
-- \ |
|
"${FILES_ARR[@]}" |
|
else |
|
docker run \ |
|
--rm \ |
|
--volume "${GITHUB_WORKSPACE}:/workspace:ro" \ |
|
--workdir "/workspace" \ |
|
--env GH_TOKEN="${GH_TOKEN}" \ |
|
"ghcr.io/zizmorcore/zizmor:${ZIZMOR_VERSION}@${ZIZMOR_DOCKER_DIGEST}" \ |
|
--config /workspace/zizmor-config.yaml \ |
|
--format github \ |
|
--min-confidence medium \ |
|
-- \ |
|
"${FILES_ARR[@]}" |
|
fi |
|
ZIZMOR_EXIT_CODE=$? |
|
set -e |
|
echo "exit-code=$ZIZMOR_EXIT_CODE" >> "$GITHUB_OUTPUT" |
|
- name: 'Evaluate mandatory checks' |
|
if: 'always()' |
|
shell: 'bash' |
|
env: |
|
MANDATORY_CHECKS: '${{ needs.zizmor-config.outputs.mandatory-checks }}' |
|
ZIZMOR_EXIT_CODE: '${{ steps.run-zizmor.outputs.exit-code }}' |
|
run: | |
|
trap 'if [ "$?" -ne 0 ]; then echo ""; echo "For Googlers: Refer to go/github-zizmor-help for internal guidance on resolving issues."; fi' EXIT |
|
echo "Mandatory checks to evaluate: ${MANDATORY_CHECKS}" |
|
echo "Zizmor format github exit code: ${ZIZMOR_EXIT_CODE}" |
|
|
|
HAS_MANDATORY_FINDINGS="false" |
|
if [ -n "${MANDATORY_CHECKS}" ] && [ "${MANDATORY_CHECKS}" != "[]" ]; then |
|
FINDINGS=$(jq --argjson checks "${MANDATORY_CHECKS}" ' |
|
[.runs[]?.results[]? | select(.ruleId | split("/") | last as $rule | $checks | index($rule))] |
|
' enriched.sarif.json) |
|
NUM_FINDINGS=$(echo "${FINDINGS}" | jq 'length') |
|
if [ "${NUM_FINDINGS}" -gt 0 ]; then |
|
echo "::error::Found ${NUM_FINDINGS} findings for mandatory checks that must always succeed." |
|
echo "${FINDINGS}" | jq -r ' |
|
.[] |
|
| "::error file=\(.locations[0].logicalLocations[0]?.properties?.symbolic?.key?.Local?.given_path // .locations[0].physicalLocation.artifactLocation.uri),line=\(.locations[0].physicalLocation.region.startLine),title=\(.ruleId)::\(.message.text)" |
|
' |
|
echo "=========================================" |
|
echo "CRITICAL: The following mandatory checks failed:" |
|
echo "${FINDINGS}" | jq -r ' |
|
.[] | "- \(.ruleId) at line \(.locations[0].physicalLocation.region.startLine) of \(.locations[0].physicalLocation.artifactLocation.uri): \(.message.text)" |
|
' |
|
echo "=========================================" |
|
HAS_MANDATORY_FINDINGS="true" |
|
fi |
|
fi |
|
|
|
# Exit codes 11 and 12 are for Informational and Low findings, respectively. |
|
# Exit codes 13 and 14 represent Medium and High findings. |
|
# Fail if the exit code is greater than 12 (i.e. 13 for Medium, 14 for High). |
|
if [ "${ZIZMOR_EXIT_CODE}" -gt 12 ]; then |
|
echo "Failing due to zizmor finding severity of Medium/High (exit code: ${ZIZMOR_EXIT_CODE})." |
|
echo "" |
|
echo "See https://docs.zizmor.sh/audits/ for more info about these checks." |
|
echo "See https://docs.zizmor.sh/usage/#ignoring-results for ignoring checks that are false positive." |
|
exit 1 |
|
fi |
|
|
|
if [ "${HAS_MANDATORY_FINDINGS}" = "true" ]; then |
|
echo "Blocking due to failed mandatory checks. These checks cannot be ignored." |
|
exit 1 |
|
fi |
|
|
|
echo "All checks evaluated successfully. No mandatory or medium/high findings." |