Skip to content

feat(webhooks): [APLAT-589] wire PagerDuty webhook end-to-end#51

Merged
martiner merged 1 commit into
martiner:masterfrom
shivanshtamrakar-s1:pd-slack-int
Jun 8, 2026
Merged

feat(webhooks): [APLAT-589] wire PagerDuty webhook end-to-end#51
martiner merged 1 commit into
martiner:masterfrom
shivanshtamrakar-s1:pd-slack-int

Conversation

@shivanshtamrakar-s1

Copy link
Copy Markdown
Add POST /webhooks/pagerduty/incident — verifies the X-PagerDuty-
Signature, parses the v3 incident.responder.added payload, filters for
the watched escalation policy, and posts a Slack notification when it
matches. Returns 401 on bad/missing signature, 200 on signed bodies
regardless of match (200-on-no-match prevents PagerDuty retry storms).

Components: PagerDutyWebhookController (HTTP entrypoint),
PagerDutyWebhookService (verify-parse-filter-notify orchestration),
WebhookBodySizeFilter (rejects /webhooks/* POSTs >64KB with 413 to
mitigate DoS via large bodies).

Security wiring: WebSecurityConfig CSRF-exempts the single exact path
/webhooks/pagerduty/incident (tighter than a wildcard). Controller
reads raw bytes and decodes UTF-8 explicitly so signature verification
matches what PagerDuty signed regardless of Content-Type charset.
PagerDutySignatureVerifier now logs ERROR (was WARN) when the secret
is unconfigured and rejects all requests instead of throwing during
context initialization.

application.properties adds three properties with empty env-var
defaults so the application context still starts when secrets are not
provisioned locally.

5 web slice tests (200 match, 200 no-match, 401 missing sig, 401 bad
sig, CSRF-exempt path), 3 filter unit tests, plus one new verifier
case covering the empty-secret behavior. Full suite goes from a
pre-existing failure to 81/81 green — the empty-secret tolerance
also fixes SpdreportApplicationTests.contextLoads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

fun send(incident: MatchedIncident) {
val subject = ":pager: Escalation policy $watchedPolicyId added to incident — ${incident.title}"
val email = Email(
recipient = InternetAddress(channelEmail),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could throw an exception - better move it into the try block

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved this to try block

properties = [
"pagerduty.webhook.secret=test-secret",
"pagerduty.escalation-policy.watch-id=PQ5P8WD",
"slack.webhook.url=http://slack.test/hook",

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems unused

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed it

spring.mail.properties[mail.smtp.starttls.enable]=true

pagerduty.webhook.secret=${PAGERDUTY_WEBHOOK_SECRET:}
pagerduty.escalation-policy.watch-id=${WATCH_ID:}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pagerduty.escalation-policy.watch-id=${WATCH_ID:}
pagerduty.escalation-policy.watch-id=${PAGERDUTY_WATCH_ID:}

for consistency

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread src/main/kotlin/cz/geek/spdreport/pagerduty/PagerDutySignatureVerifier.kt Outdated
Comment on lines +29 to +31
pagerduty.webhook.secret=${PAGERDUTY_WEBHOOK_SECRET:}
pagerduty.escalation-policy.watch-id=${PAGERDUTY_WATCH_ID:}
slack.channel.email=${SLACK_CHANNEL_EMAIL:}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pagerduty.webhook.secret=${PAGERDUTY_WEBHOOK_SECRET:}
pagerduty.escalation-policy.watch-id=${PAGERDUTY_WATCH_ID:}
slack.channel.email=${SLACK_CHANNEL_EMAIL:}
pagerduty.webhook.secret=${PAGERDUTY_WEBHOOK_SECRET}
pagerduty.escalation-policy.watch-id=${PAGERDUTY_WATCH_ID}
slack.channel.email=${SLACK_CHANNEL_EMAIL}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@shivanshtamrakar-s1 shivanshtamrakar-s1 force-pushed the pd-slack-int branch 5 times, most recently from fcfbeea to ce4e0ce Compare June 4, 2026 10:04
private fun sendEmail(subject: String, htmlBody: String, logContext: String) {
try {
val email = Email(
recipient = InternetAddress(channelEmail),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems a different exception AddressException is thrown here. Maybe we can move it to constructor:

private val channelEmail: InternetAddress = InternetAddress(slackProperties.channel.email)

and throw the error right on the app start

) {

private val keySpec: SecretKeySpec? = if (properties.webhook.secret.isEmpty()) {
logger.info { "pagerduty.webhook.secret is empty; all PagerDuty webhook deliveries will be rejected with 401" }

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is now unreachable code, with @ConfigurationProperties the "missing" value would be ${PAGERDUTY_WEBHOOK_SECRET}. There is a test, but it tests artificial behaviour that doesn't happen in the real world 😄

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my bad , removing this one. thanks

@shivanshtamrakar-s1 shivanshtamrakar-s1 force-pushed the pd-slack-int branch 3 times, most recently from 94ef613 to d0941f7 Compare June 8, 2026 12:41
    Add POST /webhooks/pagerduty/incident — verifies the X-PagerDuty-
    Signature, parses the v3 incident.responder.added payload, filters for
    the watched escalation policy, and posts a Slack notification when it
    matches. Returns 401 on bad/missing signature, 200 on signed bodies
    regardless of match (200-on-no-match prevents PagerDuty retry storms).

    Components: PagerDutyWebhookController (HTTP entrypoint),
    PagerDutyWebhookService (verify-parse-filter-notify orchestration),
    WebhookBodySizeFilter (rejects /webhooks/* POSTs >64KB with 413 to
    mitigate DoS via large bodies).

    Security wiring: WebSecurityConfig CSRF-exempts the single exact path
    /webhooks/pagerduty/incident (tighter than a wildcard). Controller
    reads raw bytes and decodes UTF-8 explicitly so signature verification
    matches what PagerDuty signed regardless of Content-Type charset.
    PagerDutySignatureVerifier now logs ERROR (was WARN) when the secret
    is unconfigured and rejects all requests instead of throwing during
    context initialization.

    application.properties adds three properties with empty env-var
    defaults so the application context still starts when secrets are not
    provisioned locally.

    5 web slice tests (200 match, 200 no-match, 401 missing sig, 401 bad
    sig, CSRF-exempt path), 3 filter unit tests, plus one new verifier
    case covering the empty-secret behavior. Full suite goes from a
    pre-existing failure to 81/81 green — the empty-secret tolerance
    also fixes SpdreportApplicationTests.contextLoads.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@martiner martiner merged commit c7ac367 into martiner:master Jun 8, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants