|
| 1 | +name: Security Advisory Discord Notification |
| 2 | + |
| 3 | +on: |
| 4 | + repository_advisory: |
| 5 | + types: [reported, created, published, submitted, reopened] |
| 6 | + |
| 7 | +permissions: |
| 8 | + contents: read |
| 9 | + |
| 10 | +jobs: |
| 11 | + notify-owners: |
| 12 | + runs-on: ubuntu-latest |
| 13 | + |
| 14 | + steps: |
| 15 | + - name: Ensure Discord webhook is configured |
| 16 | + id: check_secret |
| 17 | + env: |
| 18 | + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} |
| 19 | + run: | |
| 20 | + if [ -z "$DISCORD_WEBHOOK" ]; then |
| 21 | + echo "::warning::Missing required secret: DISCORD_WEBHOOK. Skipping advisory notification. Add a Discord webhook URL as the DISCORD_WEBHOOK secret in your repository or organization settings." |
| 22 | + echo "skip=true" >> "$GITHUB_OUTPUT" |
| 23 | + else |
| 24 | + echo "skip=false" >> "$GITHUB_OUTPUT" |
| 25 | + fi |
| 26 | +
|
| 27 | + - name: Send Discord notification |
| 28 | + if: steps.check_secret.outputs.skip == 'false' |
| 29 | + env: |
| 30 | + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} |
| 31 | + ADVISORY_TITLE: ${{ github.event.repository_advisory.summary }} |
| 32 | + ADVISORY_SEVERITY: ${{ github.event.repository_advisory.severity }} |
| 33 | + ADVISORY_STATE: ${{ github.event.repository_advisory.state }} |
| 34 | + ADVISORY_CVE: ${{ github.event.repository_advisory.cve_id }} |
| 35 | + ADVISORY_URL: ${{ github.event.repository_advisory.html_url }} |
| 36 | + ADVISORY_DESCRIPTION: ${{ github.event.repository_advisory.description }} |
| 37 | + ADVISORY_ACTION: ${{ github.event.action }} |
| 38 | + REPO: ${{ github.repository }} |
| 39 | + ACTOR: ${{ github.actor }} |
| 40 | + run: | |
| 41 | + # Map severity to a Discord embed colour (decimal RGB) |
| 42 | + case "${ADVISORY_SEVERITY,,}" in |
| 43 | + critical) COLOR=9831467 ;; # #960B0B dark red |
| 44 | + high) COLOR=15548997 ;; # #ED4245 red |
| 45 | + medium) COLOR=15105570 ;; # #E67E22 orange |
| 46 | + low) COLOR=16776960 ;; # #FFFF00 yellow |
| 47 | + *) COLOR=8421504 ;; # #808080 grey |
| 48 | + esac |
| 49 | +
|
| 50 | + # Build optional CVE field entry |
| 51 | + CVE_FIELD="" |
| 52 | + if [ -n "$ADVISORY_CVE" ]; then |
| 53 | + CVE_FIELD=$(jq -n --arg cve "$ADVISORY_CVE" \ |
| 54 | + '{name: "CVE ID", value: $cve, inline: true}') |
| 55 | + CVE_FIELD=",${CVE_FIELD}" |
| 56 | + fi |
| 57 | +
|
| 58 | + # Truncate description to Discord's 1024-char field limit |
| 59 | + DESCRIPTION="${ADVISORY_DESCRIPTION:0:1024}" |
| 60 | +
|
| 61 | + PAYLOAD=$(jq -n \ |
| 62 | + --arg title "🔒 Security Advisory ${ADVISORY_ACTION^} in \`${REPO}\`" \ |
| 63 | + --arg footer_text "GitHub Advisory · ${REPO}" \ |
| 64 | + --arg url "$ADVISORY_URL" \ |
| 65 | + --arg adv_title "$ADVISORY_TITLE" \ |
| 66 | + --arg severity "${ADVISORY_SEVERITY^}" \ |
| 67 | + --arg state "${ADVISORY_STATE^}" \ |
| 68 | + --arg actor "$ACTOR" \ |
| 69 | + --arg description "$DESCRIPTION" \ |
| 70 | + --argjson color "$COLOR" \ |
| 71 | + '{ |
| 72 | + embeds: [{ |
| 73 | + title: $title, |
| 74 | + url: $url, |
| 75 | + color: $color, |
| 76 | + fields: [ |
| 77 | + {name: "Title", value: $adv_title, inline: false}, |
| 78 | + {name: "Severity", value: $severity, inline: true}, |
| 79 | + {name: "State", value: $state, inline: true}, |
| 80 | + {name: "Reported by", value: $actor, inline: true}, |
| 81 | + {name: "Description", value: $description, inline: false} |
| 82 | + ], |
| 83 | + footer: {text: $footer_text} |
| 84 | + }] |
| 85 | + }') |
| 86 | +
|
| 87 | + # Inject optional CVE field before the description field |
| 88 | + if [ -n "$CVE_FIELD" ]; then |
| 89 | + PAYLOAD=$(echo "$PAYLOAD" | jq \ |
| 90 | + --argjson cve_field "$CVE_FIELD" \ |
| 91 | + '.embeds[0].fields |= .[:3] + [$cve_field] + .[3:]') |
| 92 | + fi |
| 93 | +
|
| 94 | + RESPONSE=$(curl -sS -o /tmp/discord_response.txt -w "%{http_code}" \ |
| 95 | + -X POST "$DISCORD_WEBHOOK" \ |
| 96 | + -H "Content-Type: application/json" \ |
| 97 | + -d "$PAYLOAD") |
| 98 | +
|
| 99 | + if [ "$RESPONSE" -lt 200 ] || [ "$RESPONSE" -ge 300 ]; then |
| 100 | + echo "::error::Discord webhook failed (HTTP $RESPONSE): $(cat /tmp/discord_response.txt)" |
| 101 | + exit 1 |
| 102 | + fi |
0 commit comments