From 99e68f82a20db7efc7ec53eeea93c3bd8d766c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=89=E6=AD=A3=E8=B6=85?= Date: Fri, 12 Jun 2026 11:33:23 +0800 Subject: [PATCH] ci(audit): report unpinned workflow actions (#3379) --- .github/workflows/audit.yml | 19 +++++- scripts/security/check_workflow_pins.sh | 87 +++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 2 deletions(-) create mode 100755 scripts/security/check_workflow_pins.sh diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index fb69bf371..42485d17c 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -21,14 +21,18 @@ on: - '**/Cargo.toml' - '**/Cargo.lock' - 'deny.toml' - - '.github/workflows/audit.yml' + - '.github/actions/**' + - '.github/workflows/**' + - 'scripts/security/check_workflow_pins.sh' pull_request: branches: [ main ] paths: - '**/Cargo.toml' - '**/Cargo.lock' - 'deny.toml' - - '.github/workflows/audit.yml' + - '.github/actions/**' + - '.github/workflows/**' + - 'scripts/security/check_workflow_pins.sh' schedule: - cron: '0 0 * * 0' # Weekly on Sunday at midnight UTC workflow_dispatch: @@ -86,6 +90,17 @@ jobs: - name: Run cargo-deny run: cargo deny check --hide-inclusion-graph advisories sources bans licenses + workflow-pin-report: + name: Workflow Pin Report + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Report unpinned GitHub Actions + run: ./scripts/security/check_workflow_pins.sh + dependency-review: name: Dependency Review runs-on: ubuntu-latest diff --git a/scripts/security/check_workflow_pins.sh b/scripts/security/check_workflow_pins.sh new file mode 100755 index 000000000..248e88788 --- /dev/null +++ b/scripts/security/check_workflow_pins.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +set -euo pipefail + +mode="report" +if [[ "${1:-}" == "--enforce" ]]; then + mode="enforce" +fi + +if [[ "${1:-}" != "" && "${1:-}" != "--enforce" ]]; then + echo "usage: $0 [--enforce]" >&2 + exit 2 +fi + +trim() { + local value="$1" + value="${value#"${value%%[![:space:]]*}"}" + value="${value%"${value##*[![:space:]]}"}" + printf '%s' "$value" +} + +report_unpinned() { + local file="$1" + local line_no="$2" + local action_ref="$3" + local reason="$4" + + printf '%s:%s: %s (%s)\n' "$file" "$line_no" "$action_ref" "$reason" + + if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + printf '::warning file=%s,line=%s::Unpinned third-party action %s (%s)\n' \ + "$file" "$line_no" "$action_ref" "$reason" + fi +} + +files=() +while IFS= read -r -d '' file; do + files+=("$file") +done < <(find .github/workflows .github/actions -type f \( -name '*.yml' -o -name '*.yaml' \) -print0 2>/dev/null | sort -z) + +unpinned=0 +for file in "${files[@]}"; do + line_no=0 + while IFS= read -r line || [[ -n "$line" ]]; do + line_no=$((line_no + 1)) + stripped="$(trim "$line")" + + [[ -z "$stripped" || "$stripped" == \#* ]] && continue + + if [[ "$stripped" =~ ^-?[[:space:]]*uses:[[:space:]]*(.+)$ ]]; then + action_ref="$(trim "${BASH_REMATCH[1]}")" + action_ref="$(trim "${action_ref%% #*}")" + action_ref="${action_ref%\"}" + action_ref="${action_ref#\"}" + action_ref="${action_ref%\'}" + action_ref="${action_ref#\'}" + + [[ -z "$action_ref" ]] && continue + [[ "$action_ref" == ./* ]] && continue + [[ "$action_ref" == docker://* ]] && continue + + if [[ "$action_ref" != *@* ]]; then + report_unpinned "$file" "$line_no" "$action_ref" "missing @ref" + unpinned=$((unpinned + 1)) + continue + fi + + ref="${action_ref##*@}" + if [[ ! "$ref" =~ ^[0-9a-fA-F]{40}$ ]]; then + report_unpinned "$file" "$line_no" "$action_ref" "ref is not a full-length commit SHA" + unpinned=$((unpinned + 1)) + fi + fi + done < "$file" +done + +if [[ "$unpinned" -eq 0 ]]; then + echo "All third-party GitHub Actions are pinned to full-length commit SHAs." + exit 0 +fi + +echo "Found $unpinned unpinned third-party GitHub Action reference(s)." + +if [[ "$mode" == "enforce" ]]; then + exit 1 +fi + +echo "Report-only mode; rerun with --enforce after existing refs are pinned."