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 container: catthehacker/ubuntu:act-22.04 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."