Microsoft Graph Security Alerts
Overview
Microsoft Graph Security Alerts is a pull-based source that collects Microsoft Defender XDR alerts from the Microsoft Graph security API (GET /v1.0/security/alerts_v2). One source covers every product that raises alerts in the Microsoft Defender portal:
| Product | serviceSource value |
|---|---|
| Microsoft Defender for Endpoint | microsoftDefenderForEndpoint |
| Microsoft Defender for Identity | microsoftDefenderForIdentity |
| Microsoft Defender for Office 365 | microsoftDefenderForOffice365 |
| Microsoft Defender for Cloud Apps | microsoftDefenderForCloudApps |
| Microsoft Defender XDR (correlation alerts) | microsoft365Defender |
| Microsoft Entra ID Protection | azureAdIdentityProtection |
| App Governance | microsoftAppGovernance |
| Microsoft Purview Data Loss Prevention | dataLossPrevention |
| Microsoft Defender for Cloud | microsoftDefenderForCloud |
| Microsoft Sentinel (once connected to the Defender portal) | microsoftSentinel |
| Microsoft Threat Intelligence | microsoftThreatIntelligence |
| Microsoft Security for AI | microsoftSecurityForAI |
This is the API the Google SecOps native Microsoft Graph API Alerts feed reads. Each alert is sent exactly as Graph returns it, evidence included, so the Google SecOps MICROSOFT_GRAPH_ALERT parser receives the complete alert.
An alert is sent when it is created and again every time Defender updates it (status, classification, assignment, new evidence). Each update is sent once; nothing is sent twice.
Supported types: Logs
Supported platforms: Linux, Windows, macOS
Minimum collector version: 0.5.73
Prerequisites
- Microsoft Defender XDR in the tenant, for example through a Microsoft 365 E5 or E5 Security licence or the individual Defender plans. Without it the API returns no alerts.
- To include Microsoft Sentinel alerts, connect Sentinel to the Defender portal.
Authentication
Attach an Azure Auth (azureauth) credential. This is the shared Azure identity credential also used by the Microsoft Entra ID and Azure Event Hub sources. The node itself holds no secret.
The identity needs one Microsoft Graph application permission:
| Permission | Type |
|---|---|
SecurityAlert.Read.All | Application |
Set up an app registration (service principal)
- In the Microsoft Entra admin center, go to Identity → Applications → App registrations → New registration. Choose Accounts in this organizational directory only and leave the redirect URI empty.
- Copy the Application (client) ID and the Directory (tenant) ID.
- Under API permissions, choose Add a permission → Microsoft Graph → Application permissions, then add
SecurityAlert.Read.All. - Click Grant admin consent for your organization. The status column must show Granted.
- Under Certificates & secrets, create a client secret and copy its Value.
- In Praxis, create an
azureauthcredential with Auth Typeservice_principaland the tenant ID, client ID and secret.
It must be an application permission, not a delegated one. The collector signs in as the app itself, so a delegated permission grants nothing and every request fails with
403.
Managed identity (collector running in Azure)
A managed identity works the same way as for the Microsoft Entra ID source. Assign it the Graph app role with PowerShell:
Connect-MgGraph -Scopes "Application.Read.All","AppRoleAssignment.ReadWrite.All"
$miObjectId = "<managed identity object (principal) ID>"
$graph = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
$role = $graph.AppRoles | Where-Object { $_.Value -eq "SecurityAlert.Read.All" -and $_.AllowedMemberTypes -contains "Application" }
New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $miObjectId -PrincipalId $miObjectId -ResourceId $graph.Id -AppRoleId $role.Id
Where to see these alerts in Microsoft
Use these to confirm there is something to collect and to compare what arrives:
-
Microsoft Defender portal: security.microsoft.com → Incidents & alerts → Alerts. This is the same list the API returns, one row per alert.
-
Graph Explorer: developer.microsoft.com/graph/graph-explorer. Sign in, consent to
SecurityAlert.Read.All, and runGET https://graph.microsoft.com/v1.0/security/alerts_v2?$top=5to see the exact JSON the source sends. -
With the app's own credential (checks the permission end to end):
TOKEN=$(curl -s -X POST "https://login.microsoftonline.com/<tenant id>/oauth2/v2.0/token" \-d client_id=<client id> -d client_secret='<secret>' \-d scope=https://graph.microsoft.com/.default -d grant_type=client_credentials \| python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])')curl -s -H "Authorization: Bearer $TOKEN" 'https://graph.microsoft.com/v1.0/security/alerts_v2?$top=1' | head -c 600 -
To generate a test alert: on a device onboarded to Defender for Endpoint, run Microsoft's detection test or create an EICAR test file. The alert appears in the portal within minutes and is collected on the next poll.
Configuration
Alerts to collect
| Parameter | Type | Default | Description |
|---|---|---|---|
service_sources | list | (empty: every product) | Collect only alerts from these products (serviceSource values above). |
Schedule
| Parameter | Type | Default | Description |
|---|---|---|---|
poll_interval | seconds | 300 | How often new and updated alerts are fetched. 30 to 86400. |
lookback | seconds | 86400 | First start only: how far back to fetch when no saved position exists. At most 15552000 (180 days, Defender's alert retention). |
persist_offsets | bool | true | Persist Position. Save the read position so a restart resumes instead of fetching the lookback window again. |
advanced
| Parameter | Type | Default | Description |
|---|---|---|---|
include_unknown_enum_members | bool | true | Ask Graph for enum values added after its v1.0 schema. When off, Microsoft Sentinel, Defender for Cloud, Threat Intelligence and Security for AI alerts report serviceSource as unknownFutureValue, and those products cannot be selected. |
late_arrival_window | seconds | 600 | How far behind the current time the saved position stays, so alerts Microsoft publishes a few minutes after their update time are still collected. Alerts already sent are not sent again. 60 to 86400; measured on the earlier of Microsoft Graph's clock and the collector's. |
page_size | int | 500 | Alerts per Graph call, 1 to 1000. Alerts carry their evidence, so pages can be large. |
api_version | v1.0 | beta | v1.0 | v1.0 matches the Google SecOps native feed. |
timeout | seconds | 60 | Per-request timeout. |
tls.insecure_skip_verify, tls.ca_file | For TLS-inspecting proxies. |
Sending to Google SecOps
The source does not set a Google SecOps log type itself. Add a Google SecOps Standardization processor between this source and the Google SecOps destination:
| Field | Value |
|---|---|
| Log Type | MICROSOFT_GRAPH_ALERT |
| Condition | resource.attributes["msgraph.source"] == "alerts_v2" (needed only when other sources share the pipeline) |
Output mapping
Every alert version becomes one log record:
| Source | OTel destination |
|---|---|
| (whole alert JSON, as Graph returned it) | LogRecord.Body (string) |
lastUpdateDateTime | LogRecord.Timestamp |
severity | LogRecord.SeverityText (as is); SeverityNumber: high → ERROR, medium → WARN, others → INFO |
| (constants) | Resource.Attributes["vendor"] = "microsoft", Resource.Attributes["msgraph.source"] = "alerts_v2" |
| (Graph host) | Resource.Attributes["msgraph.graph_host"] |
id, providerAlertId, incidentId, severity, status, serviceSource, detectionSource, category, tenantId, createdDateTime | msgraph.alert.id, .provider_alert_id, .incident_id, .severity, .status, .service_source, .detection_source, .category, .tenant_id, .created_time |
The attributes exist for routing and filtering. The body is untouched.
Example configuration
{
"service_sources": ["microsoftDefenderForEndpoint", "microsoftDefenderForIdentity"], // default: every product
"poll_interval": 300, // default: 300
"lookback": 86400, // default: 86400 (first start only)
"persist_offsets": true, // default: true
"advanced": {
"include_unknown_enum_members": true, // default: true
"late_arrival_window": 600, // default: 600
"page_size": 500, // default: 500
"api_version": "v1.0", // default: v1.0
"timeout": 60 // default: 60
}
}
Operational notes
- Counting alerts. Because updates are re-sent, records ≠ alerts. Count distinct
msgraph.alert.idfor alert counts. - How it pages. Microsoft documents
@odata.nextLinkpaging for this API, but the API has been observed returning none, clamps$topto 1000 and limits$skip. The source therefore relies on none of these and reads in time windows: when a window holds more alerts than one page, it is split into smaller windows until each fits, whatever order Graph returns alerts in. Every alert version is tracked, so overlapping reads never produce duplicates. - Late alerts and clocks. The saved position trails the earlier of Microsoft Graph's clock and the collector's by the late arrival window, so a wrong clock on either side neither misses nor duplicates alerts.
- Restarts. With Persist Position on, the position is saved after every page the pipeline accepts. A crash or a downstream refusal replays from there: nothing is skipped and nothing is sent twice.
- Retries. Throttling (
429) and server errors (5xx) are retried up to 5 times, honoringRetry-After. A failed poll leaves the position unchanged; the next poll retries it. - Sovereign clouds.
alerts_v2is not available in the China cloud. US Government clouds are not exposed in the form.
Troubleshooting
| Symptom in the collector log | Cause | Fix |
|---|---|---|
Microsoft Graph refused the alerts request … 403 | SecurityAlert.Read.All is missing, delegated instead of application, or admin consent was not granted. | Add the application permission and click Grant admin consent. |
msgraph_security could not get an Entra ID token | Wrong tenant/client ID, the secret's ID entered instead of its Value, expired secret, or a managed identity not assigned to the VM. | Fix the azureauth credential. |
Polls complete with records_delivered: 0 | No alerts updated since the position, or service_sources filters them out. | Check the Defender portal's Alerts page for the same period and products. |
serviceSource is unknownFutureValue in records | Include Newer Enum Values is off. | Turn it on. |
Microsoft Graph rejected offset paging ($skip) | More alerts share one exact update time than Graph lets the source page through (a very large bulk update). | Raise Page Size (up to 1000). |
Metrics
| Metric Name | Description |
|---|---|
prx_src_records_received_total{signal="logs"} | Alert versions sent downstream. Re-read alerts that were already sent are not counted. |
prx_src_bytes_received_total | Bytes of the alerts sent. |
prx_src_pipeline_latency_seconds | Arrival to downstream handoff, one sample per page. |
prx_src_errors_total{reason="auth"} | Token failures and 401/403 from Graph. |
prx_src_errors_total{reason="io"} | Other Graph failures, including exhausted retries. |
prx_src_parse_errors_total{parser="json"} | A Graph response that was not a list, or alerts whose id or update time could not be read (those alerts are still sent). |
prx_src_records_dropped_total{reason="backpressure"} | Pages refused downstream; they are replayed on the next poll. |