From 924817d9fd39c3bafb3f95d5368191cc41fde832 Mon Sep 17 00:00:00 2001 From: Artem Sorokin <38620398+seemewalkin@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:10:04 +0100 Subject: [PATCH] ci: Add poutine custom rule for unpinned GitHub Actions detection (#24577) --- .github/poutine-rules/unpinned_action.rego | 43 +++++++++++++++++++ .../workflows/build-unit-test-pr-comment.yml | 2 +- .github/workflows/ci-manual-unit-tests.yml | 4 +- .../workflows/create-patch-release-branch.yml | 2 +- .github/workflows/sec-poutine-reusable.yml | 10 +++++ .github/workflows/test-visual-chromatic.yml | 2 +- .../workflows/test-workflows-pr-comment.yml | 2 +- .github/workflows/util-claude.yml | 4 +- .poutine.yml | 4 ++ 9 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 .github/poutine-rules/unpinned_action.rego diff --git a/.github/poutine-rules/unpinned_action.rego b/.github/poutine-rules/unpinned_action.rego new file mode 100644 index 00000000000..1f9fc36535d --- /dev/null +++ b/.github/poutine-rules/unpinned_action.rego @@ -0,0 +1,43 @@ +# METADATA +# title: Unpinned GitHub Action +# description: |- +# GitHub Action not pinned to full commit SHA. +# Pin actions to SHA for supply chain security. +# custom: +# level: error +package rules.unpinned_action + +import data.poutine +import rego.v1 + +rule := poutine.rule(rego.metadata.chain()) + +# Match 40-character hex SHA (Git) or 64-character sha256 digest (Docker) +is_sha_pinned(uses) if { + regex.match(`@(sha256:[a-f0-9]{64}|[a-f0-9]{40})`, uses) +} + +# Check if it's a local action (starts with ./) +is_local_action(uses) if { + startswith(uses, "./") +} + +# Check if it's a reusable workflow call +is_reusable_workflow(uses) if { + contains(uses, ".github/workflows/") +} + +results contains poutine.finding(rule, pkg.purl, { + "path": workflow.path, + "job": job.id, + "step": i, + "details": sprintf("Action '%s' should be pinned to a full commit SHA", [step.uses]), +}) if { + pkg := input.packages[_] + workflow := pkg.github_actions_workflows[_] + job := workflow.jobs[_] + step := job.steps[i] + step.uses + not is_sha_pinned(step.uses) + not is_local_action(step.uses) +} diff --git a/.github/workflows/build-unit-test-pr-comment.yml b/.github/workflows/build-unit-test-pr-comment.yml index 3337747c2cc..2030a281b73 100644 --- a/.github/workflows/build-unit-test-pr-comment.yml +++ b/.github/workflows/build-unit-test-pr-comment.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Validate user permissions and collect PR data id: check_permissions - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/ci-manual-unit-tests.yml b/.github/workflows/ci-manual-unit-tests.yml index df34b76234d..f4bbcfce72b 100644 --- a/.github/workflows/ci-manual-unit-tests.yml +++ b/.github/workflows/ci-manual-unit-tests.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Create pending check run on PR id: create - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | @@ -88,7 +88,7 @@ jobs: steps: - name: Update check run on PR (if triggered from PR comment) if: inputs.pr_number != '' - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/create-patch-release-branch.yml b/.github/workflows/create-patch-release-branch.yml index 557328ed537..c94f161e393 100644 --- a/.github/workflows/create-patch-release-branch.yml +++ b/.github/workflows/create-patch-release-branch.yml @@ -23,7 +23,7 @@ jobs: contents: write pull-requests: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 - name: Validate inputs diff --git a/.github/workflows/sec-poutine-reusable.yml b/.github/workflows/sec-poutine-reusable.yml index 8bc25641cf8..ceb64f2d8ea 100644 --- a/.github/workflows/sec-poutine-reusable.yml +++ b/.github/workflows/sec-poutine-reusable.yml @@ -27,6 +27,16 @@ jobs: - name: Run Poutine Security Scanner uses: boostsecurityio/poutine-action@84c0a0d32e8d57ae12651222be1eb15351429228 # v0.15.2 + - name: Fail on error-level findings + run: | + # Check SARIF for error-level findings + if jq -e '.runs[].results[] | select(.level == "error")' results.sarif > /dev/null 2>&1; then + echo "::error::Poutine found error-level security findings:" + jq -r '.runs[].results[] | select(.level == "error") | " - \(.ruleId): \(.message.text)"' results.sarif + exit 1 + fi + echo "No error-level findings detected" + - name: Upload SARIF results uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 if: always() diff --git a/.github/workflows/test-visual-chromatic.yml b/.github/workflows/test-visual-chromatic.yml index 382dbc7ee18..f3d5a2068bc 100644 --- a/.github/workflows/test-visual-chromatic.yml +++ b/.github/workflows/test-visual-chromatic.yml @@ -16,7 +16,7 @@ jobs: runs-on: blacksmith-2vcpu-ubuntu-2204 steps: - name: Determine changed files - uses: tomi/paths-filter-action@v3.0.2 + uses: tomi/paths-filter-action@32c62f5ca100c1110406e3477d5b3ecef4666fec # v3.0.2 id: changed if: github.event_name == 'pull_request_review' with: diff --git a/.github/workflows/test-workflows-pr-comment.yml b/.github/workflows/test-workflows-pr-comment.yml index 8f19f06f467..ea823d9d114 100644 --- a/.github/workflows/test-workflows-pr-comment.yml +++ b/.github/workflows/test-workflows-pr-comment.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Validate User, Get PR Details, and React id: pr_check_and_details - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/util-claude.yml b/.github/workflows/util-claude.yml index 94aadd8c00e..1d6951f2518 100644 --- a/.github/workflows/util-claude.yml +++ b/.github/workflows/util-claude.yml @@ -25,12 +25,12 @@ jobs: id-token: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 1 - name: Run Claude PR Action - uses: anthropics/claude-code-action@beta + uses: anthropics/claude-code-action@de8e0b9c42c6cb58e904c857f164aa072244c1ac # beta with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} # Or use OAuth token instead: diff --git a/.poutine.yml b/.poutine.yml index 84dc9fcc08a..0cb9b134a35 100644 --- a/.poutine.yml +++ b/.poutine.yml @@ -4,6 +4,10 @@ # This file defines skip rules for known-safe patterns. # Add new entries only after security review. +# Custom rules for additional security checks +include: + - path: .github/poutine-rules + skip: # === SELF-HOSTED RUNNERS === # We use Blacksmith (trusted CI provider) for self-hosted runners.