Skip to content
Notifications
Clear all

Step-by-step: Integrating MDE alerts into our existing PagerDuty setup.

1 Posts
1 Users
0 Reactions
0 Views
(@finnleyj)
Eminent Member
Joined: 2 days ago
Posts: 13
Topic starter   [#24541]

After spending the better part of a sprint fighting Microsoft's documentation and its "assumed context," I've finally managed to pipe Microsoft Defender for Endpoint (MDE) alerts into PagerDuty without relying on the half-baked, magic-box "connectors" that break during version updates. The goal was straightforward: treat MDE like any other signal source in our observability stack, with proper routing, deduplication, and actionable payloads in PagerDuty. What I found was a maze of Graph API endpoints, alert schemas that change silently, and a surprising lack of sensible webhook support out of the box.

Here's the working architecture we landed on, because you shouldn't have to burn cycles reinventing this particular wheel.

**Core Components:**
* A dedicated Azure App Registration (Service Principal) for API access.
* A lightweight Python service (could be a container, Azure Function, or even a script on a cron schedule) that polls for new alerts.
* Logic to filter, transform, and deduplicate before firing into PagerDuty.
* PagerDuty Events API v2 for creating incidents.

**Why not the native "connector"?** Because it's a black box that creates incidents with a useless, flat JSON blob as the description. It provides zero control over routing logic based on device groups, severity, or alert title. It also tends to create a new PagerDuty incident for *every* alert update, leading to alert storms. We needed logic.

**Step-by-Step Implementation Outline:**

1. **Service Principal Setup:**
* Create an App Registration in Azure AD.
* Grant it the `AdvancedHunting.Read.All` and `Alert.ReadWrite.All` permissions (Application type, not Delegated).
* Grant admin consent.
* Generate a client secret.

2. **The Polling Logic (Core Snippet):**
The API is paginated. You must track the last processed alert ID or time. We poll the `/alerts` endpoint, filtering for `alertCreationTime` greater than our last run.

```python
import requests
import time

tenant_id = "YOUR_TENANT_ID"
client_id = "YOUR_CLIENT_ID"
client_secret = "YOUR_CLIENT_SECRET"
resource = "https://api.securitycenter.microsoft.com"

# Get Token
token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
token_data = {
'client_id': client_id,
'scope': f'{resource}/.default',
'client_secret': client_secret,
'grant_type': 'client_credentials'
}
token_r = requests.post(token_url, data=token_data)
token = token_r.json().get('access_token')

headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}

# Fetch recent alerts (filter by time in query params)
alerts_url = f"{resource}/api/alerts"
params = {
'$filter': f'alertCreationTime ge {last_run_iso_format}',
'$orderby': 'alertCreationTime desc'
}
alerts_response = requests.get(alerts_url, headers=headers, params=params)
alerts = alerts_response.json().get('value', [])
```

3. **Transformation & Deduplication:**
* Extract key fields: `alertId`, `title`, `severity`, `status`, `description`, `machineName`, `category`.
* We create a PagerDuty "dedup_key" using a hash of `machineName` + `category` + `title` for a configurable period (e.g., 24h) to suppress repeat alerts on the same issue.
* Map MDE severity to PagerDuty severity (e.g., 'High' -> 'error', 'Medium' -> 'warning').

4. **Pushing to PagerDuty:**
Use the PagerDuty Events API v2. Structure the payload to make the on-call engineer's life bearable.

```json
{
"routing_key": "YOUR_PAGERDUTY_INTEGRATION_KEY",
"event_action": "trigger",
"dedup_key": "your_generated_hash",
"payload": {
"summary": "[MDE] High severity alert: Suspicious Process Execution on {{machineName}}",
"source": "{{machineName}}",
"severity": "error",
"custom_details": {
"alertId": "{{alertId}}",
"title": "{{title}}",
"category": "{{category}}",
"description": "{{description}}",
"severity": "{{severity}}",
"machineName": "{{machineName}}",
"investigationLink": "https://securitycenter.microsoft.com/alert/{{alertId}}"
}
}
}
```

**Pitfalls & Costs:**
* **API Throttling:** Microsoft enforces strict limits. Implement exponential backoff in your polling logic.
* **Schema Volatility:** The alert JSON schema *will* change. Validate fields defensively; don't assume they always exist.
* **Cost:** If you run this as an Azure Function, the cost is negligible. The real cost is the engineering time to build and maintain this versus the off-the-shelf connector. For us, the control and reliability were worth it.
* **Alert Closure Sync:** You'll likely want to close PagerDuty incidents when the MDE alert is resolved (`status eq 'Resolved'`). This requires a separate polling cycle and using the `event_action: resolve` call to PagerDuty with the matching `dedup_key`.

This approach gives us full control. We can now route workstation alerts to one team, server alerts to another, and suppress certain noisy but low-fidelity alerts entirely. The PagerDuty incidents contain structured, actionable data with a direct link back to the MDE portal.

just the data


latency is a liar


   
Quote