Skip to main content

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:

ProductserviceSource value
Microsoft Defender for EndpointmicrosoftDefenderForEndpoint
Microsoft Defender for IdentitymicrosoftDefenderForIdentity
Microsoft Defender for Office 365microsoftDefenderForOffice365
Microsoft Defender for Cloud AppsmicrosoftDefenderForCloudApps
Microsoft Defender XDR (correlation alerts)microsoft365Defender
Microsoft Entra ID ProtectionazureAdIdentityProtection
App GovernancemicrosoftAppGovernance
Microsoft Purview Data Loss PreventiondataLossPrevention
Microsoft Defender for CloudmicrosoftDefenderForCloud
Microsoft Sentinel (once connected to the Defender portal)microsoftSentinel
Microsoft Threat IntelligencemicrosoftThreatIntelligence
Microsoft Security for AImicrosoftSecurityForAI

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:

PermissionType
SecurityAlert.Read.AllApplication

Set up an app registration (service principal)​

  1. 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.
  2. Copy the Application (client) ID and the Directory (tenant) ID.
  3. Under API permissions, choose Add a permission → Microsoft Graph → Application permissions, then add SecurityAlert.Read.All.
  4. Click Grant admin consent for your organization. The status column must show Granted.
  5. Under Certificates & secrets, create a client secret and copy its Value.
  6. In Praxis, create an azureauth credential with Auth Type service_principal and 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 run GET https://graph.microsoft.com/v1.0/security/alerts_v2?$top=5 to 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​

ParameterTypeDefaultDescription
service_sourceslist(empty: every product)Collect only alerts from these products (serviceSource values above).

Schedule​

ParameterTypeDefaultDescription
poll_intervalseconds300How often new and updated alerts are fetched. 30 to 86400.
lookbackseconds86400First start only: how far back to fetch when no saved position exists. At most 15552000 (180 days, Defender's alert retention).
persist_offsetsbooltruePersist Position. Save the read position so a restart resumes instead of fetching the lookback window again.

advanced​

ParameterTypeDefaultDescription
include_unknown_enum_membersbooltrueAsk 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_windowseconds600How 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_sizeint500Alerts per Graph call, 1 to 1000. Alerts carry their evidence, so pages can be large.
api_versionv1.0 | betav1.0v1.0 matches the Google SecOps native feed.
timeoutseconds60Per-request timeout.
tls.insecure_skip_verify, tls.ca_fileFor 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:

FieldValue
Log TypeMICROSOFT_GRAPH_ALERT
Conditionresource.attributes["msgraph.source"] == "alerts_v2" (needed only when other sources share the pipeline)

Output mapping​

Every alert version becomes one log record:

SourceOTel destination
(whole alert JSON, as Graph returned it)LogRecord.Body (string)
lastUpdateDateTimeLogRecord.Timestamp
severityLogRecord.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, createdDateTimemsgraph.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.id for alert counts.
  • How it pages. Microsoft documents @odata.nextLink paging for this API, but the API has been observed returning none, clamps $top to 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, honoring Retry-After. A failed poll leaves the position unchanged; the next poll retries it.
  • Sovereign clouds. alerts_v2 is not available in the China cloud. US Government clouds are not exposed in the form.

Troubleshooting​

Symptom in the collector logCauseFix
Microsoft Graph refused the alerts request … 403SecurityAlert.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 tokenWrong 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: 0No 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 recordsInclude 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 NameDescription
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_totalBytes of the alerts sent.
prx_src_pipeline_latency_secondsArrival 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.