137 lines
5.0 KiB
YAML
137 lines
5.0 KiB
YAML
name: Secret Scan
|
|
|
|
on:
|
|
pull_request:
|
|
types: [opened, synchronize, reopened]
|
|
|
|
permissions:
|
|
pull-requests: write
|
|
contents: read
|
|
|
|
jobs:
|
|
scan:
|
|
name: Scan for secrets
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Fetch base branch
|
|
run: git fetch origin ${{ github.base_ref }}
|
|
|
|
- name: Scan diff for secrets
|
|
id: scan
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# Get only added lines from the diff (exclude deletions and context lines)
|
|
DIFF=$(git diff "origin/${{ github.base_ref }}"...HEAD -- \
|
|
':!*.lock' ':!uv.lock' ':!package-lock.json' ':!yarn.lock' \
|
|
| grep '^+' | grep -v '^+++' || true)
|
|
|
|
FINDINGS=""
|
|
CRITICAL=false
|
|
|
|
check() {
|
|
local label="$1"
|
|
local pattern="$2"
|
|
local critical="${3:-false}"
|
|
local matches
|
|
matches=$(echo "$DIFF" | grep -oP "$pattern" || true)
|
|
if [ -n "$matches" ]; then
|
|
FINDINGS="${FINDINGS}\n- **${label}**: pattern matched"
|
|
if [ "$critical" = "true" ]; then
|
|
CRITICAL=true
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# AWS keys — critical
|
|
check "AWS Access Key" 'AKIA[0-9A-Z]{16}' true
|
|
|
|
# Private key headers — critical
|
|
check "Private Key Header" '-----BEGIN (RSA|EC|DSA|OPENSSH|PGP) PRIVATE KEY' true
|
|
|
|
# OpenAI / Anthropic style keys
|
|
check "OpenAI-style API key (sk-)" 'sk-[a-zA-Z0-9]{20,}' false
|
|
|
|
# GitHub tokens
|
|
check "GitHub personal access token (ghp_)" 'ghp_[a-zA-Z0-9]{36}' true
|
|
check "GitHub fine-grained PAT (github_pat_)" 'github_pat_[a-zA-Z0-9_]{1,}' true
|
|
|
|
# Slack tokens
|
|
check "Slack bot token (xoxb-)" 'xoxb-[0-9A-Za-z\-]{10,}' true
|
|
check "Slack user token (xoxp-)" 'xoxp-[0-9A-Za-z\-]{10,}' true
|
|
|
|
# Generic assignment patterns — exclude obvious placeholders
|
|
GENERIC=$(echo "$DIFF" | grep -iP '(api_key|apikey|api-key|secret_key|access_token|auth_token)\s*[=:]\s*['"'"'"][^'"'"'"]{20,}['"'"'"]' \
|
|
| grep -ivP '(fake|mock|test|placeholder|example|dummy|your[_-]|xxx|<|>|\{\{)' || true)
|
|
if [ -n "$GENERIC" ]; then
|
|
FINDINGS="${FINDINGS}\n- **Generic credential assignment**: possible hardcoded secret"
|
|
fi
|
|
|
|
# .env additions with long values
|
|
ENV_DIFF=$(git diff "origin/${{ github.base_ref }}"...HEAD -- '*.env' '**/.env' '.env*' \
|
|
| grep '^+' | grep -v '^+++' || true)
|
|
ENV_MATCHES=$(echo "$ENV_DIFF" | grep -P '^[A-Z_]+=.{16,}' \
|
|
| grep -ivP '(fake|mock|test|placeholder|example|dummy|your[_-]|xxx)' || true)
|
|
if [ -n "$ENV_MATCHES" ]; then
|
|
FINDINGS="${FINDINGS}\n- **.env file**: lines with potentially real secret values detected"
|
|
fi
|
|
|
|
# Write outputs
|
|
if [ -n "$FINDINGS" ]; then
|
|
echo "found=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "found=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
if [ "$CRITICAL" = "true" ]; then
|
|
echo "critical=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "critical=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
# Store findings in a file to use in comment step
|
|
printf "%b" "$FINDINGS" > /tmp/secret-findings.txt
|
|
|
|
- name: Post PR comment with findings
|
|
if: steps.scan.outputs.found == 'true' && github.event_name == 'pull_request'
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
FINDINGS=$(cat /tmp/secret-findings.txt)
|
|
SEVERITY="warning"
|
|
if [ "${{ steps.scan.outputs.critical }}" = "true" ]; then
|
|
SEVERITY="CRITICAL"
|
|
fi
|
|
|
|
BODY="## Secret Scan — ${SEVERITY} findings
|
|
|
|
The automated secret scanner detected potential secrets in the diff for this PR.
|
|
|
|
### Findings
|
|
${FINDINGS}
|
|
|
|
### What to do
|
|
1. Remove any real credentials from the diff immediately.
|
|
2. If the match is a false positive (test fixture, placeholder), add a comment explaining why or rename the variable to include \`fake\`, \`mock\`, or \`test\`.
|
|
3. Rotate any exposed credentials regardless of whether this PR is merged.
|
|
|
|
---
|
|
*Automated scan by [secret-scan](/.github/workflows/secret-scan.yml)*"
|
|
|
|
gh pr comment "${{ github.event.pull_request.number }}" --body "$BODY"
|
|
|
|
- name: Fail on critical secrets
|
|
if: steps.scan.outputs.critical == 'true'
|
|
run: |
|
|
echo "::error::Critical secrets detected in diff (private keys, AWS keys, or GitHub tokens). Remove them before merging."
|
|
exit 1
|
|
|
|
- name: Warn on non-critical findings
|
|
if: steps.scan.outputs.found == 'true' && steps.scan.outputs.critical == 'false'
|
|
run: |
|
|
echo "::warning::Potential secrets detected in diff. Review the PR comment for details."
|