diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml index 0916f8c..edbea56 100644 --- a/.github/workflows/secret-scan.yml +++ b/.github/workflows/secret-scan.yml @@ -44,7 +44,6 @@ jobs: env: SECRET_SCAN_SLACK_BOT_TOKEN: ${{ secrets.SECRET_SCAN_SLACK_BOT_TOKEN }} PR_URL: ${{ github.event.pull_request.html_url }} - PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | set +e @@ -85,20 +84,35 @@ jobs: # commits contain findings. Historical findings alerted when they # were introduced and stay visible in the check run + step summary; # re-posting them on every push drowned the channel. + # Anchor on the current base tip, not event base.sha: base.sha is + # frozen at PR creation, so after a merge-from-main the stale range + # would re-attribute main's findings to this PR. gitleaks exits 0 on + # an unresolvable range, so validate it here and fall back to + # full-history alerting (fail closed) on any doubt. notify=/tmp/credentials.json scope="in git history" - if [ -n "$PR_BASE_SHA" ]; then - set +e - gitleaks git --redact --exit-code 2 . \ - --log-opts "${PR_BASE_SHA}..${PR_HEAD_SHA}" \ - --report-format json --report-path /tmp/credentials-new.json - set -e - notify=/tmp/credentials-new.json - scope="new in this PR, ${count} total in history" + if [ -n "$PR_HEAD_SHA" ] && [ -n "$SECRET_SCAN_SLACK_BOT_TOKEN" ]; then + base=$(git merge-base "origin/${GITHUB_BASE_REF}" "$PR_HEAD_SHA" 2>/dev/null || true) + if [ -n "$base" ]; then + set +e + gitleaks git --redact --exit-code 2 . \ + --log-opts "${base}..${PR_HEAD_SHA}" \ + --report-format json --report-path /tmp/credentials-new.json + inc_status=$? + set -e + if { [ "$inc_status" -eq 0 ] || [ "$inc_status" -eq 2 ]; } && [ -s /tmp/credentials-new.json ]; then + notify=/tmp/credentials-new.json + scope="new in this PR, ${count} total in history" + fi + fi fi notify_count=$(jq length "$notify" 2>/dev/null || echo 0) + notify_count=${notify_count:-0} if [ -n "$SECRET_SCAN_SLACK_BOT_TOKEN" ] && [ "$notify_count" -gt 0 ]; then - sample=$(jq -r '.[:5][] | "> \(.RuleID) \(.File):\(.StartLine)"' "$notify") + # Escape Slack mrkdwn entities: a crafted file name could otherwise + # render as a disguised clickable link in the alert channel. + sample=$(jq -r 'def esc: tostring | gsub("&";"&") | gsub("<";"<") | gsub(">";">"); + .[:5][] | "> \(.RuleID|esc) \(.File|esc):\(.StartLine)"' "$notify") txt=":rotating_light: *Credential leak detected* in \`${GITHUB_REPOSITORY}\` (${notify_count} ${scope}). Rotate immediately. ${sample} ${PR_URL:+PR: ${PR_URL} @@ -109,7 +123,7 @@ jobs: -d "$(jq -n --arg ch "$SLACK_CHANNEL_ID" --arg txt "$txt" \ '{channel: $ch, text: $txt}')" | jq -r '.ok // "post failed"' else - echo "Slack: skipped (no token, or findings are historical only)" + echo "Slack: skipped (no token or nothing new to report; notify_count=${notify_count})" fi elif [ "$status" -ne 0 ]; then exit "$status" @@ -151,7 +165,6 @@ jobs: env: SECRET_SCAN_SLACK_BOT_TOKEN: ${{ secrets.SECRET_SCAN_SLACK_BOT_TOKEN }} PR_URL: ${{ github.event.pull_request.html_url }} - PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | set +e @@ -180,20 +193,35 @@ jobs: || echo "could not create check run (read-only token?)" # Slack dedup: same rule as the credentials job: on PRs, # notify only for findings introduced by the PR's own commits. + # Anchor on the current base tip, not event base.sha: base.sha is + # frozen at PR creation, so after a merge-from-main the stale range + # would re-attribute main's findings to this PR. gitleaks exits 0 on + # an unresolvable range, so validate it here and fall back to + # full-history alerting (fail closed) on any doubt. notify=/tmp/internal-refs.json scope="in git history" - if [ -n "$PR_BASE_SHA" ]; then - set +e - gitleaks git --config /tmp/gitleaks-internal.toml --exit-code 2 . \ - --log-opts "${PR_BASE_SHA}..${PR_HEAD_SHA}" \ - --report-format json --report-path /tmp/internal-refs-new.json - set -e - notify=/tmp/internal-refs-new.json - scope="new in this PR, ${count} total in history" + if [ -n "$PR_HEAD_SHA" ] && [ -n "$SECRET_SCAN_SLACK_BOT_TOKEN" ]; then + base=$(git merge-base "origin/${GITHUB_BASE_REF}" "$PR_HEAD_SHA" 2>/dev/null || true) + if [ -n "$base" ]; then + set +e + gitleaks git --config /tmp/gitleaks-internal.toml --exit-code 2 . \ + --log-opts "${base}..${PR_HEAD_SHA}" \ + --report-format json --report-path /tmp/internal-refs-new.json + inc_status=$? + set -e + if { [ "$inc_status" -eq 0 ] || [ "$inc_status" -eq 2 ]; } && [ -s /tmp/internal-refs-new.json ]; then + notify=/tmp/internal-refs-new.json + scope="new in this PR, ${count} total in history" + fi + fi fi notify_count=$(jq length "$notify" 2>/dev/null || echo 0) + notify_count=${notify_count:-0} if [ -n "$SECRET_SCAN_SLACK_BOT_TOKEN" ] && [ "$notify_count" -gt 0 ]; then - sample=$(jq -r '.[:5][] | "> \(.RuleID) \(.File):\(.StartLine)"' "$notify") + # Escape Slack mrkdwn entities: a crafted file name could otherwise + # render as a disguised clickable link in the alert channel. + sample=$(jq -r 'def esc: tostring | gsub("&";"&") | gsub("<";"<") | gsub(">";">"); + .[:5][] | "> \(.RuleID|esc) \(.File|esc):\(.StartLine)"' "$notify") txt=":warning: Internal Astera references in \`${GITHUB_REPOSITORY}\` (${notify_count} ${scope}). Fine while private; must be resolved before the repo goes public. ${sample} ${PR_URL:+PR: ${PR_URL} @@ -204,7 +232,7 @@ jobs: -d "$(jq -n --arg ch "$SLACK_CHANNEL_ID" --arg txt "$txt" \ '{channel: $ch, text: $txt}')" | jq -r '.ok // "post failed"' else - echo "Slack: skipped (no token, or findings are historical only)" + echo "Slack: skipped (no token or nothing new to report; notify_count=${notify_count})" fi elif [ "$status" -ne 0 ]; then echo "gitleaks failed with status $status" >&2