Secret Scanning Alert Notification #1337
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Secret Scanning Alert Notification | |
| # Polls for new secret scanning alerts and sends private Slack notifications. | |
| # GitHub does not support secret_scanning_alert as an Actions workflow trigger, | |
| # so we poll on a schedule and track state via cache to avoid duplicates. | |
| on: | |
| schedule: | |
| - cron: "*/10 * * * *" | |
| workflow_dispatch: | |
| inputs: | |
| test_mode: | |
| description: "Send mock test notification instead of scanning real alerts" | |
| type: boolean | |
| default: true | |
| permissions: | |
| contents: read | |
| security-events: read | |
| jobs: | |
| check-alerts: | |
| name: Check for new secret scanning alerts | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Restore last seen alert ID | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: .last-alert-id | |
| key: secret-scanning-last-alert-${{ github.run_id }} | |
| restore-keys: | | |
| secret-scanning-last-alert- | |
| - name: Check for new alerts and notify | |
| id: scan | |
| env: | |
| GH_TOKEN: ${{ secrets.SECRET_SCANNING_TOKEN }} | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| SLACK_BOT_TOKEN: ${{ secrets.H2O_OPS_SLACK_BOT_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| IS_TEST: ${{ github.event_name == 'workflow_dispatch' && inputs.test_mode == true }} | |
| run: | | |
| if [[ "$IS_TEST" == "true" ]]; then | |
| PAYLOAD=$(jq -n \ | |
| --arg repo "$REPO" \ | |
| --arg url "https://github.com/${REPO}/security/secret-scanning" \ | |
| '{ | |
| channel: "h2o-ghas-alerts", | |
| text: "🧪 [TEST] Secret Scanning Alert", | |
| blocks: [ | |
| {type: "header", text: {type: "plain_text", text: "🧪 [TEST] Secret Scanning Alert"}}, | |
| {type: "section", fields: [ | |
| {type: "mrkdwn", text: ("*Repository:*\n" + $repo)}, | |
| {type: "mrkdwn", text: "*Secret Type:*\nTest Secret (mock)"} | |
| ]}, | |
| {type: "section", fields: [ | |
| {type: "mrkdwn", text: "*Committed By:*\nTest User"}, | |
| {type: "mrkdwn", text: "*Commit Date:*\n2026-07-27T00:00:00Z"} | |
| ]}, | |
| {type: "section", fields: [ | |
| {type: "mrkdwn", text: "*File:*\n`config/test-file.yml`"}, | |
| {type: "mrkdwn", text: "*Commit:*\n`abc1234`"} | |
| ]}, | |
| {type: "section", fields: [ | |
| {type: "mrkdwn", text: "*Detected:*\nN/A (test run)"}, | |
| {type: "mrkdwn", text: "*Push Protection Bypassed:*\nNo"} | |
| ]}, | |
| {type: "divider"}, | |
| {type: "section", text: {type: "mrkdwn", text: ("🧪 *This is a test notification.* No action required.\n\n<" + $url + "|View Secret Scanning in GitHub>")}} | |
| ] | |
| }') | |
| RESPONSE=$(curl -s -X POST "https://slack.com/api/chat.postMessage" \ | |
| -H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$PAYLOAD") | |
| OK=$(echo "$RESPONSE" | jq -r '.ok') | |
| if [[ "$OK" != "true" ]]; then | |
| echo "::error::Slack API error: $(echo "$RESPONSE" | jq -r '.error')" | |
| exit 1 | |
| fi | |
| echo "Test notification sent successfully" | |
| exit 0 | |
| fi | |
| LAST_SEEN_ID=0 | |
| if [[ -f .last-alert-id ]]; then | |
| LAST_SEEN_ID=$(cat .last-alert-id) | |
| fi | |
| echo "Last seen alert ID: ${LAST_SEEN_ID}" | |
| ALERTS=$(gh api "/repos/${REPO}/secret-scanning/alerts?state=open&sort=created&direction=desc&per_page=100" 2>&1) || { | |
| echo "::error::Failed to fetch secret scanning alerts. Check token permissions." | |
| echo "API response: ${ALERTS}" | |
| exit 1 | |
| } | |
| NEW_ALERTS=$(echo "$ALERTS" | jq --argjson last "$LAST_SEEN_ID" '[.[] | select(.number > $last)]') | |
| ALERT_COUNT=$(echo "$NEW_ALERTS" | jq 'length') | |
| echo "Found ${ALERT_COUNT} new alert(s)" | |
| if [[ "$ALERT_COUNT" -eq 0 ]]; then | |
| echo "No new secret scanning alerts" | |
| exit 0 | |
| fi | |
| HIGHEST_ID=$(echo "$NEW_ALERTS" | jq '[.[].number] | max') | |
| echo "$HIGHEST_ID" > .last-alert-id | |
| echo "updated=true" >> "$GITHUB_OUTPUT" | |
| echo "Updated last seen alert ID to: ${HIGHEST_ID}" | |
| echo "$NEW_ALERTS" | jq -c 'sort_by(.number) | .[]' | while read -r ALERT; do | |
| ALERT_NUMBER=$(echo "$ALERT" | jq -r '.number') | |
| SECRET_TYPE=$(echo "$ALERT" | jq -r '.secret_type_display_name') | |
| CREATED_AT=$(echo "$ALERT" | jq -r '.created_at') | |
| ALERT_URL=$(echo "$ALERT" | jq -r '.html_url') | |
| PUSH_BYPASSED=$(echo "$ALERT" | jq -r '.push_protection_bypassed // false') | |
| LOCATIONS=$(gh api "/repos/${REPO}/secret-scanning/alerts/${ALERT_NUMBER}/locations" 2>/dev/null) || LOCATIONS="[]" | |
| COMMIT_SHA=$(echo "$LOCATIONS" | jq -r '.[0].details.commit_sha // "unknown"') | |
| FILE_PATH=$(echo "$LOCATIONS" | jq -r '.[0].details.path // "unknown"') | |
| COMMITTER="unknown" | |
| COMMIT_DATE="unknown" | |
| SHORT_SHA="unknown" | |
| BRANCH="unknown" | |
| if [[ "$COMMIT_SHA" != "unknown" && "$COMMIT_SHA" != "null" ]]; then | |
| SHORT_SHA="${COMMIT_SHA:0:7}" | |
| # Use GITHUB_TOKEN for commit/branch lookups (SECRET_SCANNING_TOKEN lacks contents:read) | |
| COMMIT_DATA=$(GH_TOKEN="${GITHUB_TOKEN}" gh api "/repos/${REPO}/commits/${COMMIT_SHA}" 2>/dev/null) || COMMIT_DATA="{}" | |
| COMMITTER=$(echo "$COMMIT_DATA" | jq -r '.commit.author.name // "unknown"') | |
| COMMIT_DATE=$(echo "$COMMIT_DATA" | jq -r '.commit.author.date // "unknown"') | |
| # Check if commit is in default branch history | |
| DEFAULT_BRANCH=$(GH_TOKEN="${GITHUB_TOKEN}" gh api "/repos/${REPO}" --jq '.default_branch' 2>/dev/null) || DEFAULT_BRANCH="master" | |
| COMPARE_STATUS=$(GH_TOKEN="${GITHUB_TOKEN}" gh api "/repos/${REPO}/compare/${COMMIT_SHA}...${DEFAULT_BRANCH}" --jq '.status' 2>/dev/null) || COMPARE_STATUS="" | |
| if [[ "$COMPARE_STATUS" == "ahead" || "$COMPARE_STATUS" == "identical" ]]; then | |
| BRANCH="${DEFAULT_BRANCH}" | |
| else | |
| # For feature branches, check branches where commit is HEAD | |
| BRANCH=$(GH_TOKEN="${GITHUB_TOKEN}" gh api "/repos/${REPO}/commits/${COMMIT_SHA}/branches-where-head" --jq '[.[].name] | join(", ")' 2>/dev/null) | |
| if [[ -z "$BRANCH" ]]; then | |
| BRANCH="unknown" | |
| fi | |
| fi | |
| fi | |
| BYPASS_TEXT="No" | |
| if [[ "$PUSH_BYPASSED" == "true" ]]; then | |
| BYPASS_TEXT="Yes ⚠️" | |
| fi | |
| PAYLOAD=$(jq -n \ | |
| --arg repo "$REPO" \ | |
| --arg secret_type "$SECRET_TYPE" \ | |
| --arg committer "$COMMITTER" \ | |
| --arg commit_date "$COMMIT_DATE" \ | |
| --arg file_path "$FILE_PATH" \ | |
| --arg short_sha "$SHORT_SHA" \ | |
| --arg branch "$BRANCH" \ | |
| --arg created_at "$CREATED_AT" \ | |
| --arg bypass "$BYPASS_TEXT" \ | |
| --arg alert_url "$ALERT_URL" \ | |
| '{ | |
| channel: "h2o-ghas-alerts", | |
| text: ("🔐 Secret Exposed in " + $repo), | |
| blocks: [ | |
| {type: "header", text: {type: "plain_text", text: "🔐 Secret Scanning Alert"}}, | |
| {type: "section", fields: [ | |
| {type: "mrkdwn", text: ("*Repository:*\n" + $repo)}, | |
| {type: "mrkdwn", text: ("*Secret Type:*\n" + $secret_type)} | |
| ]}, | |
| {type: "section", fields: [ | |
| {type: "mrkdwn", text: ("*Committed By:*\n" + $committer)}, | |
| {type: "mrkdwn", text: ("*Commit Date:*\n" + $commit_date)} | |
| ]}, | |
| {type: "section", fields: [ | |
| {type: "mrkdwn", text: ("*File:*\n`" + $file_path + "`")}, | |
| {type: "mrkdwn", text: ("*Commit:*\n`" + $short_sha + "`")} | |
| ]}, | |
| {type: "section", fields: [ | |
| {type: "mrkdwn", text: ("*Branch:*\n" + $branch)}, | |
| {type: "mrkdwn", text: ("*Push Protection Bypassed:*\n" + $bypass)} | |
| ]}, | |
| {type: "section", fields: [ | |
| {type: "mrkdwn", text: ("*Detected:*\n" + $created_at)}, | |
| {type: "mrkdwn", text: " "} | |
| ]}, | |
| {type: "divider"}, | |
| {type: "section", text: {type: "mrkdwn", text: ("⚠️ *Action Required:* Rotate this credential immediately and revoke the exposed secret.\n\n<" + $alert_url + "|View Alert in GitHub>")}} | |
| ] | |
| }') | |
| curl -s -X POST "https://slack.com/api/chat.postMessage" \ | |
| -H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$PAYLOAD" | |
| echo "Sent alert #${ALERT_NUMBER}: ${SECRET_TYPE} in ${FILE_PATH}" | |
| done | |
| - name: Save last seen alert ID | |
| if: always() | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: .last-alert-id | |
| key: secret-scanning-last-alert-${{ github.run_id }} |