Microsoft Defender XDR Integration Guide
Overview
The Microsoft Defender XDR (Extended Detection and Response) integration connects your NINA workflows to the Microsoft Graph Security API v1.0, enabling cross-product security operations across the entire Microsoft Defender suite. This integration lets you execute KQL threat-hunting queries against unified telemetry, manage security alerts and incidents aggregated from all Defender products, and track your organization's security posture through Secure Score — all directly from your workflows.
Microsoft Defender XDR is Microsoft's unified SecOps platform that correlates signals from Microsoft Defender for Endpoint (MDE), Microsoft Defender for Office 365 (MDO), Microsoft Defender for Identity (MDI), Microsoft Defender for Cloud Apps (MDA), Microsoft Entra ID Protection, and Microsoft Purview DLP/IRM into a single incident queue and hunting surface.
Status
We currently support 5 resources that cover the full MD XDR Graph API surface:
- Advanced Hunting: Execute KQL queries across the unified XDR telemetry schema — DeviceEvents, AlertInfo, AlertEvidence, EmailEvents, IdentityLogonEvents, CloudAppEvents, and all other cross-product tables
- Alert Management: List, retrieve, update status/classification/assignment, and post comments on alerts aggregated from all Defender products
- Incident Management: List, retrieve, update, and comment on XDR incidents that group related alerts into a single investigation unit
- Secure Score: List and retrieve Microsoft Secure Score snapshots for your organization
- Secure Score Control Profile: List, retrieve, and update Secure Score control profiles to manage improvement action state and assignment
Credential Configuration
Before using the Microsoft Defender XDR integration in your workflows, you need to configure OAuth2 credentials through an Azure App Registration. The integration uses the OAuth2 client credentials flow (no user interaction required) against the Azure AD token endpoint and the Microsoft Graph API.
Authentication Method
OAuth2 Client Credentials
| Field | Description | Example |
|---|---|---|
| Tenant ID | Directory (tenant) ID from the Azure portal | 87654321-4321-4321-4321-fedcba987654 |
| Client ID | Application (client) ID from the Azure App Registration | 12345678-1234-1234-1234-123456789abc |
| Client Secret | Client secret value from the Azure App Registration | abcd1234~efgh5678ijklMNOP9012qrst.UV |
| Scope | OAuth2 scope. Leave blank to use the default. | https://graph.microsoft.com/.default |
How to create an Azure App Registration:
- Sign in to the Azure portal
- Navigate to Microsoft Entra ID > App registrations
- Click New registration
- Provide a name (e.g., "NINA MD XDR Integration")
- Select Accounts in this organizational directory only
- Click Register
- Note the Application (client) ID and Directory (tenant) ID shown on the overview page
- Go to Certificates & secrets > Client secrets > New client secret
- Enter a description and expiration period, then click Add
- Copy the Value immediately — it will not be shown again
Required API Permissions (Application permissions on Microsoft Graph):
Grant the following application permissions and click Grant admin consent:
| Permission | Required for |
|---|---|
SecurityAlert.Read.All | Listing and retrieving alerts |
SecurityAlert.ReadWrite.All | Updating alerts and posting alert comments |
SecurityIncident.Read.All | Listing and retrieving incidents |
SecurityIncident.ReadWrite.All | Updating incidents and posting incident comments |
ThreatHunting.Read.All | Executing Advanced Hunting KQL queries |
SecurityEvents.Read.All | Retrieving Secure Score and control profiles |
SecurityEvents.ReadWrite.All | Updating Secure Score control profiles |
Note:
SecurityAlert.ReadWrite.Allis a superset ofSecurityAlert.Read.All. For read-only alert workflows you can omit theReadWritepermission. The same applies toSecurityIncidentandSecurityEvents.
Creating a Microsoft Defender XDR Credential
- Navigate to the Credentials section in NINA
- Click Add New Credential
- Fill in the credential details:
- Name: A descriptive name (e.g., "MD XDR Production")
- Description: Optional details about the credential's purpose
- Integration Service: Select "Microsoft Defender XDR"
- Auth Type: Select "OAuth2"
- Tenant ID: Enter your Directory (tenant) ID
- Client ID: Enter your Application (client) ID
- Client Secret: Enter your client secret value
- Scope: Leave blank (defaults to
https://graph.microsoft.com/.default)
- Click Test Connection to verify credentials — this calls
GET /security/alerts_v2?$top=1to confirm authentication and permissions - Click Save to store the credential
Supported Resources and Operations
Advanced Hunting
Execute KQL queries against the unified XDR telemetry schema. Results include schema metadata (column names and types) alongside the query result rows.
| Operation | Name | Description |
|---|---|---|
runQuery | Run Hunting Query | Execute a KQL query against XDR telemetry and return results with schema metadata |
Parameters for runQuery:
| Parameter | Type | Required | Description |
|---|---|---|---|
query | string | Yes | KQL query string targeting any Advanced Hunting table |
timespan | string | No | ISO 8601 duration limiting the query time range (e.g., P1D for last 24 h, PT4H for last 4 h). Overrides any timestamp filters in the KQL. |
Response structure:
{
"schema": [
{ "name": "Timestamp", "type": "DateTime" },
{ "name": "DeviceId", "type": "String" }
],
"results": [
{ "Timestamp": "2026-05-14T10:00:00Z", "DeviceId": "abc123" }
]
}
Available hunting tables (selection):
| Category | Tables |
|---|---|
| Endpoint (MDE) | DeviceEvents, DeviceProcessEvents, DeviceNetworkEvents, DeviceFileEvents, DeviceLogonEvents, DeviceRegistryEvents, DeviceImageLoadEvents |
| Endpoint TVM | DeviceTvmSoftwareVulnerabilities, DeviceTvmSoftwareInventory, DeviceTvmSecureConfigurationAssessment |
| Alerts | AlertInfo, AlertEvidence |
| Email (MDO) | EmailEvents, EmailAttachmentInfo, EmailUrlInfo, EmailPostDeliveryEvents |
| Identity (MDI) | IdentityLogonEvents, IdentityQueryEvents, IdentityDirectoryEvents |
| Cloud Apps (MDA) | CloudAppEvents |
| Azure AD | AADSignInEventsBeta, AADSpnSignInEventsBeta |
Alert
Manage security alerts aggregated by Microsoft Defender XDR from all connected Defender products. Supports OData v4 $filter, $top, $select, and $orderby.
| Operation | Name | Description |
|---|---|---|
getAll | List Alerts | Retrieve a collection of alerts with optional OData filtering, field selection, and sorting |
get | Get Alert | Retrieve a single alert by its ID |
update | Update Alert | Update status, classification, determination, severity, or assignee |
comment | Create Comment | Append a comment to an alert |
Parameters for getAll:
| Parameter | Type | Required | Description |
|---|---|---|---|
returnAll | boolean | No | When true, fetches all pages automatically |
limit | number | No | Maximum number of results (default: 50) |
filter.filterString | string | No | OData $filter expression (e.g., severity eq 'high' and status eq 'new') |
filter.select | string | No | Comma-separated list of fields to return |
filter.orderBy | string | No | OData $orderby expression (e.g., createdDateTime desc) |
Parameters for get:
| Parameter | Type | Required | Description |
|---|---|---|---|
alertId | string | Yes | Alert ID |
additionalFields.select | string | No | Comma-separated fields to return |
Parameters for update:
| Parameter | Type | Required | Description |
|---|---|---|---|
alertId | string | Yes | Alert ID |
updateFields.status | string | No | new, inProgress, resolved |
updateFields.assignedTo | string | No | Analyst email address |
updateFields.classification | string | No | unknown, falsePositive, truePositive |
updateFields.determination | string | No | unknown, apt, malware, securityPersonnel, securityTesting, unwantedSoftware, other |
updateFields.severity | string | No | unknown, low, medium, high |
Parameters for comment:
| Parameter | Type | Required | Description |
|---|---|---|---|
alertId | string | Yes | Alert ID |
comment | string | Yes | Comment text to append |
Incident
Manage XDR incidents — groups of correlated alerts representing a single attack campaign or security event.
| Operation | Name | Description |
|---|---|---|
getAll | List Incidents | Retrieve a collection of incidents with optional OData filtering, field selection, and sorting |
get | Get Incident | Retrieve a single incident by its ID (includes nested alerts and evidence) |
update | Update Incident | Update status, classification, determination, severity, assignee, or custom tags |
comment | Create Comment | Append a comment to an incident |
Parameters for getAll:
| Parameter | Type | Required | Description |
|---|---|---|---|
returnAll | boolean | No | When true, fetches all pages automatically |
limit | number | No | Maximum number of results (default: 50) |
filter.filterString | string | No | OData $filter expression |
filter.select | string | No | Comma-separated fields to return |
filter.orderBy | string | No | OData $orderby expression |
Parameters for get:
| Parameter | Type | Required | Description |
|---|---|---|---|
incidentId | string | Yes | Incident ID |
additionalFields.select | string | No | Comma-separated fields to return |
Parameters for update:
| Parameter | Type | Required | Description |
|---|---|---|---|
incidentId | string | Yes | Incident ID |
updateFields.status | string | No | active, inProgress, resolved |
updateFields.assignedTo | string | No | Analyst email address |
updateFields.classification | string | No | unknown, falsePositive, truePositive |
updateFields.determination | string | No | unknown, apt, malware, securityPersonnel, securityTesting, unwantedSoftware, other |
updateFields.severity | string | No | unknown, low, medium, high |
updateFields.tags | array | No | Array of string tags to apply to the incident |
Parameters for comment:
| Parameter | Type | Required | Description |
|---|---|---|---|
incidentId | string | Yes | Incident ID |
comment | string | Yes | Comment text to append |
Note: The
commentoperation on incidents performs a GET to retrieve existing comments, appends the new comment, and PATCHes the incident back. This is a read-modify-write pattern due to the Graph API's incident comment design.
Secure Score
Retrieve Microsoft Secure Score history for your organization. Each score snapshot records the overall score, comparison scores, and per-product breakdowns at a point in time.
| Operation | Name | Description |
|---|---|---|
getAll | List Secure Scores | Retrieve a collection of Secure Score snapshots |
get | Get Secure Score | Retrieve a specific Secure Score snapshot by ID |
Parameters for getAll:
| Parameter | Type | Required | Description |
|---|---|---|---|
returnAll | boolean | No | When true, fetches all pages automatically |
limit | number | No | Maximum number of results (default: 50) |
filter.filterString | string | No | OData $filter expression |
filter.select | string | No | Comma-separated fields to return |
filter.orderBy | string | No | OData $orderby expression (e.g., createdDateTime desc) |
Parameters for get:
| Parameter | Type | Required | Description |
|---|---|---|---|
secureScoreId | string | Yes | Secure Score snapshot ID |
Secure Score Control Profile
Manage Secure Score control profiles — the individual improvement actions that contribute to your organization's Secure Score. Each profile has an assignee, a current state, and a tier classification.
| Operation | Name | Description |
|---|---|---|
getAll | List Control Profiles | Retrieve all Secure Score control profiles |
get | Get Control Profile | Retrieve a specific control profile by ID |
update | Update Control Profile | Update the state, tier, assignee, or tenant note of a control profile |
Parameters for getAll:
| Parameter | Type | Required | Description |
|---|---|---|---|
returnAll | boolean | No | When true, fetches all pages automatically |
limit | number | No | Maximum number of results (default: 50) |
filter.filterString | string | No | OData $filter expression |
filter.select | string | No | Comma-separated fields to return |
filter.orderBy | string | No | OData $orderby expression |
Parameters for get:
| Parameter | Type | Required | Description |
|---|---|---|---|
controlProfileId | string | Yes | Control profile ID (e.g., MFARegistrationV2) |
Parameters for update:
| Parameter | Type | Required | Description |
|---|---|---|---|
controlProfileId | string | Yes | Control profile ID |
updateFields.assignedTo | string | No | Analyst or team responsible for this control |
updateFields.tenantNote | string | No | Tenant-specific note about this control |
updateFields.tier | string | No | undefined, basic, advanced, specialized |
updateFields.state | string | No | Default, Ignored, ThirdParty, Reviewed |
Parameter Merging and Templating
The Microsoft Defender XDR integration takes full advantage of NINA's parameter merging and templating capabilities:
Parameter Sources (in order of precedence)
- Node Parameters: Parameters configured directly in the MD XDR Integration Node
- Extracted Parameters: Parameters automatically extracted from the input data
- Input Data: The complete input data from upstream nodes
When an MD XDR Integration Node executes:
- It combines parameters from all sources
- Node parameters take precedence over extracted parameters
- Template variables within parameters are processed using
{{variable_name}}syntax - The combined parameters are used to execute the XDR operation
OData Filtering
The filter.filterString parameter accepts standard OData v4 expressions. String values must use single quotes:
severity eq 'high' and status eq 'new'
createdDateTime ge 2026-05-01T00:00:00Z
assignedTo eq '[email protected]'
Example: Advanced Hunting
Hunting for Suspicious PowerShell Execution
{
"integration_service": "microsoft-defender-xdr",
"resource": "advanced-hunting",
"operation": "runQuery",
"parameters": {
"query": "DeviceProcessEvents | where FileName =~ 'powershell.exe' and ProcessCommandLine has '-EncodedCommand' | project Timestamp, DeviceId, DeviceName, AccountName, ProcessCommandLine | limit 50",
"timespan": "P1D"
}
}
Hunting for Alerts Linked to a Specific Device
{
"integration_service": "microsoft-defender-xdr",
"resource": "advanced-hunting",
"operation": "runQuery",
"parameters": {
"query": "AlertInfo | join AlertEvidence on AlertId | where DeviceId == '{{device_id}}' | project Timestamp, AlertId, Title, Severity, Category | order by Timestamp desc | limit 20"
}
}
Hunting for Lateral Movement via Network Events
Input Data from Previous Node:
{
"source_ip": "10.10.5.42",
"detection_window": "PT4H"
}
Node Configuration:
{
"integration_service": "microsoft-defender-xdr",
"resource": "advanced-hunting",
"operation": "runQuery",
"parameters": {
"query": "DeviceNetworkEvents | where LocalIP == '{{source_ip}}' and RemotePort in (445, 135, 5985, 5986) | summarize ConnectionCount = count(), Targets = make_set(RemoteIP) by DeviceId, DeviceName | where ConnectionCount > 3",
"timespan": "{{detection_window}}"
}
}
Hunting for Compromised Credential Use
{
"integration_service": "microsoft-defender-xdr",
"resource": "advanced-hunting",
"operation": "runQuery",
"parameters": {
"query": "IdentityLogonEvents | where AccountUpn == '{{compromised_account}}' and LogonType in ('Interactive', 'RemoteInteractive') | project Timestamp, DeviceName, IPAddress, Location, IsExternalAddress | order by Timestamp desc",
"timespan": "P7D"
}
}
Example: Alert Management
Listing New High-Severity Alerts
{
"integration_service": "microsoft-defender-xdr",
"resource": "alert",
"operation": "getAll",
"parameters": {
"limit": 100,
"filter": {
"filterString": "severity eq 'high' and status eq 'new'",
"orderBy": "createdDateTime desc"
}
}
}
Getting a Specific Alert
{
"integration_service": "microsoft-defender-xdr",
"resource": "alert",
"operation": "get",
"parameters": {
"alertId": "{{alert_id}}"
}
}
Assigning and Escalating an Alert
Input Data from Previous Node:
{
"alert_id": "da637773775833477842_-2109939563",
"analyst": "[email protected]"
}
Node Configuration:
{
"integration_service": "microsoft-defender-xdr",
"resource": "alert",
"operation": "update",
"parameters": {
"alertId": "{{alert_id}}",
"updateFields": {
"status": "inProgress",
"assignedTo": "{{analyst}}",
"severity": "high"
}
}
}
Closing a False Positive Alert
{
"integration_service": "microsoft-defender-xdr",
"resource": "alert",
"operation": "update",
"parameters": {
"alertId": "{{alert_id}}",
"updateFields": {
"status": "resolved",
"classification": "falsePositive",
"determination": "securityTesting",
"assignedTo": "{{analyst}}"
}
}
}
Posting an Investigation Comment
{
"integration_service": "microsoft-defender-xdr",
"resource": "alert",
"operation": "comment",
"parameters": {
"alertId": "{{alert_id}}",
"comment": "Investigated by {{analyst}}. Confirmed malicious PowerShell activity — user account disabled, device isolation initiated via MDE."
}
}
Example: Incident Management
Listing Active Incidents
{
"integration_service": "microsoft-defender-xdr",
"resource": "incident",
"operation": "getAll",
"parameters": {
"limit": 50,
"filter": {
"filterString": "status eq 'active'",
"orderBy": "lastUpdateDateTime desc"
}
}
}
Getting an Incident with All Related Alerts
{
"integration_service": "microsoft-defender-xdr",
"resource": "incident",
"operation": "get",
"parameters": {
"incidentId": "{{incident_id}}"
}
}
Assigning an Incident to a SOC Team
{
"integration_service": "microsoft-defender-xdr",
"resource": "incident",
"operation": "update",
"parameters": {
"incidentId": "{{incident_id}}",
"updateFields": {
"status": "inProgress",
"assignedTo": "[email protected]",
"tags": ["escalated", "critical-asset"]
}
}
}
Resolving a Confirmed True Positive Incident
Input Data:
{
"incident_id": "2345",
"analyst": "[email protected]",
"threat_family": "apt"
}
Node Configuration:
{
"integration_service": "microsoft-defender-xdr",
"resource": "incident",
"operation": "update",
"parameters": {
"incidentId": "{{incident_id}}",
"updateFields": {
"status": "resolved",
"classification": "truePositive",
"determination": "{{threat_family}}",
"assignedTo": "{{analyst}}"
}
}
}
Adding a Closure Note to an Incident
{
"integration_service": "microsoft-defender-xdr",
"resource": "incident",
"operation": "comment",
"parameters": {
"incidentId": "{{incident_id}}",
"comment": "Incident resolved. Root cause: compromised service account used for lateral movement. Affected accounts disabled, devices isolated and reimaged. Post-incident report filed under INC-2026-0042."
}
}
Example: Secure Score
Retrieving the Most Recent Secure Score
{
"integration_service": "microsoft-defender-xdr",
"resource": "secure-score",
"operation": "getAll",
"parameters": {
"limit": 1,
"filter": {
"orderBy": "createdDateTime desc"
}
}
}
Listing the Last 30 Days of Score History
{
"integration_service": "microsoft-defender-xdr",
"resource": "secure-score",
"operation": "getAll",
"parameters": {
"limit": 30,
"filter": {
"filterString": "createdDateTime ge 2026-04-14T00:00:00Z",
"orderBy": "createdDateTime asc"
}
}
}
Example: Secure Score Control Profiles
Listing All Control Profiles with Their Current State
{
"integration_service": "microsoft-defender-xdr",
"resource": "secure-score-control-profile",
"operation": "getAll",
"parameters": {
"returnAll": true
}
}
Marking a Control as Third-Party Managed
{
"integration_service": "microsoft-defender-xdr",
"resource": "secure-score-control-profile",
"operation": "update",
"parameters": {
"controlProfileId": "MFARegistrationV2",
"updateFields": {
"state": "ThirdParty",
"assignedTo": "[email protected]",
"tenantNote": "MFA enforced via Okta — not applicable to Microsoft MFA control"
}
}
}
Assigning a Control Profile to a Team
{
"integration_service": "microsoft-defender-xdr",
"resource": "secure-score-control-profile",
"operation": "update",
"parameters": {
"controlProfileId": "{{control_id}}",
"updateFields": {
"assignedTo": "{{owner_email}}",
"tier": "advanced",
"tenantNote": "{{remediation_note}}"
}
}
}
Field Reference
Alert and Incident Status Values
| Value | Description |
|---|---|
new | Newly detected, not yet reviewed |
inProgress | Actively being investigated |
resolved | Investigation completed |
Note: For incidents, the value
activeis also accepted as an alias fornew.
Alert and Incident Classification Values
| Value | Description |
|---|---|
unknown | Classification not yet set |
falsePositive | Alert is not a real threat |
truePositive | Alert represents a real threat |
Alert and Incident Determination Values
| Value | Description |
|---|---|
unknown | Determination not yet set |
apt | Advanced Persistent Threat activity |
malware | Malware infection |
securityPersonnel | Action by authorized security personnel |
securityTesting | Authorized penetration test or red team |
unwantedSoftware | Potentially unwanted application (PUA/PUP) |
other | Confirmed threat, other category |
Severity Values
| Value | Description |
|---|---|
unknown | Severity not yet determined |
low | Low-severity finding |
medium | Medium-severity finding |
high | High-severity finding |
Secure Score Control Profile State Values
| Value | Description |
|---|---|
Default | Not yet reviewed — using the default state |
Ignored | Control dismissed as not applicable to the organization |
ThirdParty | Control satisfied by a third-party solution |
Reviewed | Control reviewed and accepted as-is |
Secure Score Control Profile Tier Values
| Value | Description |
|---|---|
undefined | Tier not set |
basic | Basic security hygiene control |
advanced | Advanced configuration control |
specialized | Specialized or niche control |
Common Workflow Patterns
Incident Triage and Escalation
incident.getAllwithfilter.filterString: "status eq 'active'"— retrieve open incidentsincident.get— retrieve full incident detail including nested alerts and evidenceadvanced-hunting.runQuery— hunt for related activity (lateral movement, persistence) using entity data from the incidentincident.update— assign to SOC team, update severity, add classification tagsincident.comment— record investigation findingsalert.update— update individual alert statuses within the incident
Alert-Driven Threat Hunting
- Receive a high-severity alert trigger from an upstream node
alert.get— retrieve full alert with entity details (device ID, file hash, IP, user)advanced-hunting.runQuery— pivot on extracted entities to find related activity- Based on hunting findings,
incident.updateoralert.update— record verdict and severity alert.comment— document hunting evidence and chain of investigation
Security Posture Reporting
secure-score.getAllwithlimit: 1andorderBy: "createdDateTime desc"— get current scoresecure-score-control-profile.getAllwithreturnAll: true— retrieve all control profiles- Filter profiles where
state eq 'Default'andimplementationStatus ne 'Implemented'— identify actionable improvements secure-score-control-profile.update— assign open controls to responsible teams with target tiers- Aggregate score history over the last 30 days to generate a trend report
Bulk Alert Closure After Hunting Verdict
alert.getAllwithfilter.filterStringscoped to a specific campaign (e.g., bytitleoractorDisplayName)advanced-hunting.runQuery— confirm scope of activity and verify affected entities- For each alert in the result set:
alert.update— setstatus: resolved,classification, anddeterminationalert.comment— append the investigation summary
IOC Pivot from Alert to Hunting
alert.get— extract IoC values from alert evidence (fileHash,url,domainName,ipAddress)advanced-hunting.runQuery— search for the IoC across historical telemetry:DeviceNetworkEvents
| where RemoteUrl has "{{domain}}" or RemoteIP == "{{ip}}"
| project Timestamp, DeviceId, DeviceName, RemoteIP, RemoteUrl
| order by Timestamp desc- Use query results to identify additional affected devices
incident.comment— document all affected devices and scope of compromise
Troubleshooting
| Issue | Resolution |
|---|---|
authentication failed (401) | Verify that Client ID, Client Secret, and Tenant ID are correct and that the client secret has not expired |
access denied (403) | Confirm that the required application permissions are configured in the Azure App Registration and that an admin has granted consent |
query is required | The advanced-hunting.runQuery operation requires a non-empty query parameter |
alertId is required | alert.get, alert.update, and alert.comment require a non-empty alertId parameter |
incidentId is required | incident.get, incident.update, and incident.comment require a non-empty incidentId parameter |
invalid value for status | Status must be one of the exact values listed in the Field Reference above. Values are case-sensitive. |
invalid value for determination | Determination must match one of the defined enum values. Values such as "malwareFamily" are not valid — use "malware" instead |
KQL syntax errors from runQuery | Test the KQL in the Microsoft 365 Defender portal (Advanced Hunting) before including it in a workflow. The Graph API returns raw KQL parse errors |
Empty results array from hunting | The query executed successfully but returned no matching rows. Verify the table name, time range (timespan or KQL where Timestamp > filter), and predicate values |
timespan ignored | When a KQL query contains an explicit Timestamp filter and timespan is also set, timespan takes precedence. Remove the KQL timestamp filter to avoid conflicts |
| Incident comment not appearing | The comment operation performs GET → PATCH. If the incident was modified concurrently between these two calls, the PATCH may overwrite the concurrent change. Retry if comments are not visible |
| Secure Score returns 0 records | The Secure Score is calculated daily. If queried immediately after tenant provisioning, scores may not yet exist. Use orderBy: "createdDateTime desc" to retrieve the most recent snapshot |
| Control profile update not reflected | Secure Score control states are eventually consistent and may take up to 24 hours to reflect in the score calculation after an update |
| HTTP 429 rate limit | The Microsoft Graph Security API is subject to per-tenant throttling limits. NINA retries automatically with exponential back-off. For bulk workflows, introduce delays between calls |
Best Practices
-
Grant minimum required permissions: Request only the permissions needed for your workflow. Read-only triage workflows only need
SecurityAlert.Read.AllandSecurityIncident.Read.All. AddReadWritepermissions only when your workflow modifies data. -
Rotate client secrets before expiry: Azure App Registration client secrets expire. Set calendar reminders 30 days before expiry and update the NINA credential proactively to avoid workflow disruptions.
-
Use
timespanon Advanced Hunting queries: Always settimespanor include an explicitTimestampfilter in KQL to avoid full-table scans, which are slower and consume more quota. -
Limit KQL result size with
limitortop: Advanced Hunting queries can return large result sets. Apply| limit Nat the end of KQL to cap rows and reduce response latency in workflows. -
Prefer
filter.filterStringfor precise alert/incident queries: Use OData filters to scopegetAllcalls rather than retrieving all records and filtering in downstream nodes. This reduces data transfer and API quota consumption. -
Use
returnAll: truesparingly: For resources with large collections (e.g.,secure-score-control-profile),returnAll: truefetches all pages. Use it only when you genuinely need the full dataset. -
Read-before-write for incident comments: The
incident.commentoperation performs a GET to retrieve existing comments before PATCHing. Avoid calling this operation concurrently on the same incident to prevent comment loss due to race conditions. -
Keep hunting queries idempotent: Design hunting queries to be safely re-run on the same time window without side effects. Prefer
projectto select only needed fields and reduce response size. -
Use template variables for entity pivoting: Extract entity IDs (alert IDs, incident IDs, device IDs) from upstream node results and pass them into downstream XDR operations using
{{variable_name}}syntax to build investigation chains. -
Track Secure Score trends: Schedule a daily
secure-score.getAllwithlimit: 1andorderBy: "createdDateTime desc"to record the current score. Store results in a database for trend analysis and reporting. -
Assign control profiles before review cadences: Before quarterly security reviews, use
secure-score-control-profile.getAllto identify unassigned (assignedTois empty) controls and bulk-assign them to responsible teams viaupdate. -
Test KQL in the Defender portal first: Validate KQL syntax and result shape in the Microsoft 365 Defender portal's Advanced Hunting tab before embedding queries in workflows. This avoids difficult-to-diagnose KQL parse errors in production.
-
Handle empty results gracefully: Hunting queries and list operations may return empty result sets. Add null-checks or conditional branching in downstream nodes to handle the case where
resultsorvaluearrays are empty. -
Document incident investigation steps as comments: Use
incident.commentto record each investigation milestone. These comments appear in the Microsoft 365 Defender portal and create an audit trail for compliance purposes.
Security Considerations
-
Protect client secrets: Store credentials exclusively through NINA's credential manager. Never include Client IDs, secrets, or tenant IDs in workflow parameters, KQL queries, log messages, or comments.
-
Use dedicated service accounts: Register a dedicated Azure App for this integration rather than reusing credentials across multiple systems. This limits the blast radius if credentials are compromised and simplifies permission auditing.
-
Restrict App Registration to a single tenant: Ensure the App Registration is configured for Accounts in this organizational directory only. Multi-tenant registrations increase the attack surface.
-
Audit API activity via Azure AD sign-in logs: Microsoft Entra ID logs all token acquisitions and API calls made by the service principal. Review these logs periodically for unexpected access patterns.
-
Review Advanced Hunting query content: KQL queries are executed with the permissions of the service principal. Avoid embedding user-supplied input directly into KQL without validation to prevent KQL injection that could exfiltrate unintended data.
-
Limit incident modification to authorized workflows: The
incident.updateandincident.commentoperations change data visible to SOC analysts in the Microsoft 365 Defender portal. Ensure only authorized workflows holdSecurityIncident.ReadWrite.Allpermission. -
Monitor for permission scope creep: Periodically review the App Registration's API permissions in the Azure portal to ensure no permissions have been added beyond the minimum required set documented in this guide.
Updated: 2026-05-18