ci: Add poutine custom rule for unpinned GitHub Actions detection (#24577)

This commit is contained in:
Artem Sorokin 2026-01-20 16:10:04 +01:00 committed by GitHub
parent 2b4596eb66
commit 924817d9fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 65 additions and 8 deletions

View file

@ -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)
}

View file

@ -18,7 +18,7 @@ jobs:
steps: steps:
- name: Validate user permissions and collect PR data - name: Validate user permissions and collect PR data
id: check_permissions id: check_permissions
uses: actions/github-script@v7 uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
script: | script: |

View file

@ -25,7 +25,7 @@ jobs:
steps: steps:
- name: Create pending check run on PR - name: Create pending check run on PR
id: create id: create
uses: actions/github-script@v7 uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
script: | script: |
@ -88,7 +88,7 @@ jobs:
steps: steps:
- name: Update check run on PR (if triggered from PR comment) - name: Update check run on PR (if triggered from PR comment)
if: inputs.pr_number != '' if: inputs.pr_number != ''
uses: actions/github-script@v7 uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
script: | script: |

View file

@ -23,7 +23,7 @@ jobs:
contents: write contents: write
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Validate inputs - name: Validate inputs

View file

@ -27,6 +27,16 @@ jobs:
- name: Run Poutine Security Scanner - name: Run Poutine Security Scanner
uses: boostsecurityio/poutine-action@84c0a0d32e8d57ae12651222be1eb15351429228 # v0.15.2 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 - name: Upload SARIF results
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
if: always() if: always()

View file

@ -16,7 +16,7 @@ jobs:
runs-on: blacksmith-2vcpu-ubuntu-2204 runs-on: blacksmith-2vcpu-ubuntu-2204
steps: steps:
- name: Determine changed files - name: Determine changed files
uses: tomi/paths-filter-action@v3.0.2 uses: tomi/paths-filter-action@32c62f5ca100c1110406e3477d5b3ecef4666fec # v3.0.2
id: changed id: changed
if: github.event_name == 'pull_request_review' if: github.event_name == 'pull_request_review'
with: with:

View file

@ -21,7 +21,7 @@ jobs:
steps: steps:
- name: Validate User, Get PR Details, and React - name: Validate User, Get PR Details, and React
id: pr_check_and_details id: pr_check_and_details
uses: actions/github-script@v7 uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
script: | script: |

View file

@ -25,12 +25,12 @@ jobs:
id-token: write id-token: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Run Claude PR Action - name: Run Claude PR Action
uses: anthropics/claude-code-action@beta uses: anthropics/claude-code-action@de8e0b9c42c6cb58e904c857f164aa072244c1ac # beta
with: with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# Or use OAuth token instead: # Or use OAuth token instead:

View file

@ -4,6 +4,10 @@
# This file defines skip rules for known-safe patterns. # This file defines skip rules for known-safe patterns.
# Add new entries only after security review. # Add new entries only after security review.
# Custom rules for additional security checks
include:
- path: .github/poutine-rules
skip: skip:
# === SELF-HOSTED RUNNERS === # === SELF-HOSTED RUNNERS ===
# We use Blacksmith (trusted CI provider) for self-hosted runners. # We use Blacksmith (trusted CI provider) for self-hosted runners.