Skip to main content

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

FieldDescriptionExample
Tenant IDDirectory (tenant) ID from the Azure portal87654321-4321-4321-4321-fedcba987654
Client IDApplication (client) ID from the Azure App Registration12345678-1234-1234-1234-123456789abc
Client SecretClient secret value from the Azure App Registrationabcd1234~efgh5678ijklMNOP9012qrst.UV
ScopeOAuth2 scope. Leave blank to use the default.https://graph.microsoft.com/.default

How to create an Azure App Registration:

  1. Sign in to the Azure portal
  2. Navigate to Microsoft Entra ID > App registrations
  3. Click New registration
  4. Provide a name (e.g., "NINA MD XDR Integration")
  5. Select Accounts in this organizational directory only
  6. Click Register
  7. Note the Application (client) ID and Directory (tenant) ID shown on the overview page
  8. Go to Certificates & secrets > Client secrets > New client secret
  9. Enter a description and expiration period, then click Add
  10. 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:

PermissionRequired for
SecurityAlert.Read.AllListing and retrieving alerts
SecurityAlert.ReadWrite.AllUpdating alerts and posting alert comments
SecurityIncident.Read.AllListing and retrieving incidents
SecurityIncident.ReadWrite.AllUpdating incidents and posting incident comments
ThreatHunting.Read.AllExecuting Advanced Hunting KQL queries
SecurityEvents.Read.AllRetrieving Secure Score and control profiles
SecurityEvents.ReadWrite.AllUpdating Secure Score control profiles

Note: SecurityAlert.ReadWrite.All is a superset of SecurityAlert.Read.All. For read-only alert workflows you can omit the ReadWrite permission. The same applies to SecurityIncident and SecurityEvents.

Creating a Microsoft Defender XDR Credential

  1. Navigate to the Credentials section in NINA
  2. Click Add New Credential
  3. 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)
  4. Click Test Connection to verify credentials — this calls GET /security/alerts_v2?$top=1 to confirm authentication and permissions
  5. 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.

OperationNameDescription
runQueryRun Hunting QueryExecute a KQL query against XDR telemetry and return results with schema metadata

Parameters for runQuery:

ParameterTypeRequiredDescription
querystringYesKQL query string targeting any Advanced Hunting table
timespanstringNoISO 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):

CategoryTables
Endpoint (MDE)DeviceEvents, DeviceProcessEvents, DeviceNetworkEvents, DeviceFileEvents, DeviceLogonEvents, DeviceRegistryEvents, DeviceImageLoadEvents
Endpoint TVMDeviceTvmSoftwareVulnerabilities, DeviceTvmSoftwareInventory, DeviceTvmSecureConfigurationAssessment
AlertsAlertInfo, AlertEvidence
Email (MDO)EmailEvents, EmailAttachmentInfo, EmailUrlInfo, EmailPostDeliveryEvents
Identity (MDI)IdentityLogonEvents, IdentityQueryEvents, IdentityDirectoryEvents
Cloud Apps (MDA)CloudAppEvents
Azure ADAADSignInEventsBeta, AADSpnSignInEventsBeta

Alert

Manage security alerts aggregated by Microsoft Defender XDR from all connected Defender products. Supports OData v4 $filter, $top, $select, and $orderby.

OperationNameDescription
getAllList AlertsRetrieve a collection of alerts with optional OData filtering, field selection, and sorting
getGet AlertRetrieve a single alert by its ID
updateUpdate AlertUpdate status, classification, determination, severity, or assignee
commentCreate CommentAppend a comment to an alert

Parameters for getAll:

ParameterTypeRequiredDescription
returnAllbooleanNoWhen true, fetches all pages automatically
limitnumberNoMaximum number of results (default: 50)
filter.filterStringstringNoOData $filter expression (e.g., severity eq 'high' and status eq 'new')
filter.selectstringNoComma-separated list of fields to return
filter.orderBystringNoOData $orderby expression (e.g., createdDateTime desc)

Parameters for get:

ParameterTypeRequiredDescription
alertIdstringYesAlert ID
additionalFields.selectstringNoComma-separated fields to return

Parameters for update:

ParameterTypeRequiredDescription
alertIdstringYesAlert ID
updateFields.statusstringNonew, inProgress, resolved
updateFields.assignedTostringNoAnalyst email address
updateFields.classificationstringNounknown, falsePositive, truePositive
updateFields.determinationstringNounknown, apt, malware, securityPersonnel, securityTesting, unwantedSoftware, other
updateFields.severitystringNounknown, low, medium, high

Parameters for comment:

ParameterTypeRequiredDescription
alertIdstringYesAlert ID
commentstringYesComment text to append

Incident

Manage XDR incidents — groups of correlated alerts representing a single attack campaign or security event.

OperationNameDescription
getAllList IncidentsRetrieve a collection of incidents with optional OData filtering, field selection, and sorting
getGet IncidentRetrieve a single incident by its ID (includes nested alerts and evidence)
updateUpdate IncidentUpdate status, classification, determination, severity, assignee, or custom tags
commentCreate CommentAppend a comment to an incident

Parameters for getAll:

ParameterTypeRequiredDescription
returnAllbooleanNoWhen true, fetches all pages automatically
limitnumberNoMaximum number of results (default: 50)
filter.filterStringstringNoOData $filter expression
filter.selectstringNoComma-separated fields to return
filter.orderBystringNoOData $orderby expression

Parameters for get:

ParameterTypeRequiredDescription
incidentIdstringYesIncident ID
additionalFields.selectstringNoComma-separated fields to return

Parameters for update:

ParameterTypeRequiredDescription
incidentIdstringYesIncident ID
updateFields.statusstringNoactive, inProgress, resolved
updateFields.assignedTostringNoAnalyst email address
updateFields.classificationstringNounknown, falsePositive, truePositive
updateFields.determinationstringNounknown, apt, malware, securityPersonnel, securityTesting, unwantedSoftware, other
updateFields.severitystringNounknown, low, medium, high
updateFields.tagsarrayNoArray of string tags to apply to the incident

Parameters for comment:

ParameterTypeRequiredDescription
incidentIdstringYesIncident ID
commentstringYesComment text to append

Note: The comment operation 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.

OperationNameDescription
getAllList Secure ScoresRetrieve a collection of Secure Score snapshots
getGet Secure ScoreRetrieve a specific Secure Score snapshot by ID

Parameters for getAll:

ParameterTypeRequiredDescription
returnAllbooleanNoWhen true, fetches all pages automatically
limitnumberNoMaximum number of results (default: 50)
filter.filterStringstringNoOData $filter expression
filter.selectstringNoComma-separated fields to return
filter.orderBystringNoOData $orderby expression (e.g., createdDateTime desc)

Parameters for get:

ParameterTypeRequiredDescription
secureScoreIdstringYesSecure 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.

OperationNameDescription
getAllList Control ProfilesRetrieve all Secure Score control profiles
getGet Control ProfileRetrieve a specific control profile by ID
updateUpdate Control ProfileUpdate the state, tier, assignee, or tenant note of a control profile

Parameters for getAll:

ParameterTypeRequiredDescription
returnAllbooleanNoWhen true, fetches all pages automatically
limitnumberNoMaximum number of results (default: 50)
filter.filterStringstringNoOData $filter expression
filter.selectstringNoComma-separated fields to return
filter.orderBystringNoOData $orderby expression

Parameters for get:

ParameterTypeRequiredDescription
controlProfileIdstringYesControl profile ID (e.g., MFARegistrationV2)

Parameters for update:

ParameterTypeRequiredDescription
controlProfileIdstringYesControl profile ID
updateFields.assignedTostringNoAnalyst or team responsible for this control
updateFields.tenantNotestringNoTenant-specific note about this control
updateFields.tierstringNoundefined, basic, advanced, specialized
updateFields.statestringNoDefault, 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)

  1. Node Parameters: Parameters configured directly in the MD XDR Integration Node
  2. Extracted Parameters: Parameters automatically extracted from the input data
  3. 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"
}
}
}
{
"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

ValueDescription
newNewly detected, not yet reviewed
inProgressActively being investigated
resolvedInvestigation completed

Note: For incidents, the value active is also accepted as an alias for new.

Alert and Incident Classification Values

ValueDescription
unknownClassification not yet set
falsePositiveAlert is not a real threat
truePositiveAlert represents a real threat

Alert and Incident Determination Values

ValueDescription
unknownDetermination not yet set
aptAdvanced Persistent Threat activity
malwareMalware infection
securityPersonnelAction by authorized security personnel
securityTestingAuthorized penetration test or red team
unwantedSoftwarePotentially unwanted application (PUA/PUP)
otherConfirmed threat, other category

Severity Values

ValueDescription
unknownSeverity not yet determined
lowLow-severity finding
mediumMedium-severity finding
highHigh-severity finding

Secure Score Control Profile State Values

ValueDescription
DefaultNot yet reviewed — using the default state
IgnoredControl dismissed as not applicable to the organization
ThirdPartyControl satisfied by a third-party solution
ReviewedControl reviewed and accepted as-is

Secure Score Control Profile Tier Values

ValueDescription
undefinedTier not set
basicBasic security hygiene control
advancedAdvanced configuration control
specializedSpecialized or niche control

Common Workflow Patterns

Incident Triage and Escalation

  1. incident.getAll with filter.filterString: "status eq 'active'" — retrieve open incidents
  2. incident.get — retrieve full incident detail including nested alerts and evidence
  3. advanced-hunting.runQuery — hunt for related activity (lateral movement, persistence) using entity data from the incident
  4. incident.update — assign to SOC team, update severity, add classification tags
  5. incident.comment — record investigation findings
  6. alert.update — update individual alert statuses within the incident

Alert-Driven Threat Hunting

  1. Receive a high-severity alert trigger from an upstream node
  2. alert.get — retrieve full alert with entity details (device ID, file hash, IP, user)
  3. advanced-hunting.runQuery — pivot on extracted entities to find related activity
  4. Based on hunting findings, incident.update or alert.update — record verdict and severity
  5. alert.comment — document hunting evidence and chain of investigation

Security Posture Reporting

  1. secure-score.getAll with limit: 1 and orderBy: "createdDateTime desc" — get current score
  2. secure-score-control-profile.getAll with returnAll: true — retrieve all control profiles
  3. Filter profiles where state eq 'Default' and implementationStatus ne 'Implemented' — identify actionable improvements
  4. secure-score-control-profile.update — assign open controls to responsible teams with target tiers
  5. Aggregate score history over the last 30 days to generate a trend report

Bulk Alert Closure After Hunting Verdict

  1. alert.getAll with filter.filterString scoped to a specific campaign (e.g., by title or actorDisplayName)
  2. advanced-hunting.runQuery — confirm scope of activity and verify affected entities
  3. For each alert in the result set:
    • alert.update — set status: resolved, classification, and determination
    • alert.comment — append the investigation summary

IOC Pivot from Alert to Hunting

  1. alert.get — extract IoC values from alert evidence (fileHash, url, domainName, ipAddress)
  2. 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
  3. Use query results to identify additional affected devices
  4. incident.comment — document all affected devices and scope of compromise

Troubleshooting

IssueResolution
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 requiredThe advanced-hunting.runQuery operation requires a non-empty query parameter
alertId is requiredalert.get, alert.update, and alert.comment require a non-empty alertId parameter
incidentId is requiredincident.get, incident.update, and incident.comment require a non-empty incidentId parameter
invalid value for statusStatus must be one of the exact values listed in the Field Reference above. Values are case-sensitive.
invalid value for determinationDetermination must match one of the defined enum values. Values such as "malwareFamily" are not valid — use "malware" instead
KQL syntax errors from runQueryTest 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 huntingThe query executed successfully but returned no matching rows. Verify the table name, time range (timespan or KQL where Timestamp > filter), and predicate values
timespan ignoredWhen 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 appearingThe 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 recordsThe 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 reflectedSecure 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 limitThe 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

  1. Grant minimum required permissions: Request only the permissions needed for your workflow. Read-only triage workflows only need SecurityAlert.Read.All and SecurityIncident.Read.All. Add ReadWrite permissions only when your workflow modifies data.

  2. 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.

  3. Use timespan on Advanced Hunting queries: Always set timespan or include an explicit Timestamp filter in KQL to avoid full-table scans, which are slower and consume more quota.

  4. Limit KQL result size with limit or top: Advanced Hunting queries can return large result sets. Apply | limit N at the end of KQL to cap rows and reduce response latency in workflows.

  5. Prefer filter.filterString for precise alert/incident queries: Use OData filters to scope getAll calls rather than retrieving all records and filtering in downstream nodes. This reduces data transfer and API quota consumption.

  6. Use returnAll: true sparingly: For resources with large collections (e.g., secure-score-control-profile), returnAll: true fetches all pages. Use it only when you genuinely need the full dataset.

  7. Read-before-write for incident comments: The incident.comment operation 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.

  8. Keep hunting queries idempotent: Design hunting queries to be safely re-run on the same time window without side effects. Prefer project to select only needed fields and reduce response size.

  9. 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.

  10. Track Secure Score trends: Schedule a daily secure-score.getAll with limit: 1 and orderBy: "createdDateTime desc" to record the current score. Store results in a database for trend analysis and reporting.

  11. Assign control profiles before review cadences: Before quarterly security reviews, use secure-score-control-profile.getAll to identify unassigned (assignedTo is empty) controls and bulk-assign them to responsible teams via update.

  12. 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.

  13. 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 results or value arrays are empty.

  14. Document incident investigation steps as comments: Use incident.comment to record each investigation milestone. These comments appear in the Microsoft 365 Defender portal and create an audit trail for compliance purposes.

Security Considerations

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. Limit incident modification to authorized workflows: The incident.update and incident.comment operations change data visible to SOC analysts in the Microsoft 365 Defender portal. Ensure only authorized workflows hold SecurityIncident.ReadWrite.All permission.

  7. 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