Skip to Content
CheatsheetsDefender XDR

Microsoft Defender XDR Cheat Sheet

Microsoft Defender XDR is not one product but a family of surfaces that share the unified https://graph.microsoft.com/v1.0/security API and the Defender portal. This sheet covers the four surfaces you will automate against from the command line: posture (Defender for Cloud), endpoint (Defender for Endpoint / XDR), the built-in Windows Defender Antivirus engine, and Defender for Endpoint on Linux.

Scope: Blue-team and platform-engineering automation - SOC tooling, incident response runbooks, and posture-as-code. Assumes PowerShell 7+ for cross-platform automation, Bash for Linux endpoints, and Python 3.12+ for service integrations.

Versions: Microsoft Defender XDR (2024+) · Microsoft Sentinel · Graph Security API v1.0 · Defender for Endpoint API (api.securitycenter.microsoft.com) · mdatp 101.x+ · Azure CLI 2.60+

Last reviewed: June 2026


The Four Surfaces

SurfaceWhat it coversPrimary interfaceAuth
Defender for CloudCloud posture, secure score, regulatory compliance, plan pricingaz security CLIAzure RBAC (Az context)
Defender for Endpoint / XDRAlerts, incidents, advanced hunting, device response actionsGraph Security API + Defender for Endpoint APIEntra app or delegated Graph token
Defender AntivirusThe on-device AV engine on WindowsBuilt-in Defender PowerShell module (Get-MpComputerStatus, etc.)Local admin on the host
Defender for Endpoint on LinuxEDR + AV agent on Linux hostsmdatp CLILocal root/sudo on the host

See also: KQL / Microsoft Defender for the full advanced-hunting table reference and threat-hunting query library that this sheet links into.


Authentication

Everything else on this page assumes you have a token. The catch with Defender is that the surfaces sit behind different token audiences - a token for Microsoft Graph will not work against the Defender for Endpoint API, and neither works against Azure Resource Manager. Acquire a token per resource.

Token audiences

Service / APIToken audience (resource)What it covers
Defender for Cloud, Sentinel, Log Analytics managementhttps://management.azure.comaz security, watchlists, incidents, workbooks
Defender XDR alerts / incidents / hunting (Graph)https://graph.microsoft.comalerts_v2, incidents, runHuntingQuery
Defender for Endpoint response actionshttps://api.securitycenter.microsoft.comisolate, scan, collect package, machine inventory
Log Analytics direct query APIhttps://api.loganalytics.ioquerying a workspace from the data plane

🔬 Pick your identity by where the code runs: interactive at a workstation, a managed identity on Azure compute, and OIDC / workload identity federation in CI/CD. Avoid long-lived client secrets entirely where you can - the only one of these that creates a credential to leak is the SPN-with-secret path.

Azure CLI

Bash
# Interactive (workstation) - browser, or device code on a headless box
az login
az login --use-device-code
 
# Service principal - secret, then certificate
az login --service-principal -u "$APP_ID" -p "$CLIENT_SECRET" --tenant "$TENANT_ID"
az login --service-principal -u "$APP_ID" -p ./cert.pem --tenant "$TENANT_ID"
 
# Managed identity (on an Azure VM / Container App / Function)
az login --identity                              # system-assigned
az login --identity --username "$UAMI_CLIENT_ID" # user-assigned
 
# OIDC / workload identity federation (CI/CD) - exchange a federated token, no secret
az login --service-principal -u "$APP_ID" --tenant "$TENANT_ID" --federated-token "$ID_TOKEN"

Grab a token for each audience

Bash
az account get-access-token --resource https://management.azure.com           --query accessToken -o tsv
az account get-access-token --resource https://graph.microsoft.com            --query accessToken -o tsv
az account get-access-token --resource https://api.securitycenter.microsoft.com --query accessToken -o tsv
az account get-access-token --resource https://api.loganalytics.io            --query accessToken -o tsv

PowerShell (Az and Microsoft.Graph)

PowerShell
# Az - interactive, SPN (secret / cert), managed identity, OIDC
Connect-AzAccount
$cred = [pscredential]::new($appId, (ConvertTo-SecureString $secret -AsPlainText -Force))
Connect-AzAccount -ServicePrincipal -Credential $cred -Tenant $tenantId
Connect-AzAccount -ServicePrincipal -ApplicationId $appId -CertificateThumbprint $thumb -Tenant $tenantId
Connect-AzAccount -Identity                                   # system-assigned MI
Connect-AzAccount -Identity -AccountId $uamiClientId          # user-assigned MI
Connect-AzAccount -ServicePrincipal -ApplicationId $appId -Tenant $tenantId -FederatedToken $env:ID_TOKEN
 
# Microsoft.Graph - delegated scopes, app-only cert, or managed identity
Connect-MgGraph -Scopes 'SecurityAlert.Read.All', 'SecurityIncident.Read.All'
Connect-MgGraph -ClientId $appId -TenantId $tenantId -CertificateThumbprint $thumb
Connect-MgGraph -Identity

Get a raw token (note the SecureString change)

PowerShell
# Az.Accounts 5.x (Az 14+) returns the token as a SecureString by default.
$secure = (Get-AzAccessToken -ResourceUrl 'https://api.securitycenter.microsoft.com' -AsSecureString).Token
$token  = [System.Net.NetworkCredential]::new('', $secure).Password

Microsoft Graph / REST (client credentials)

Bash
# Secret-based client credentials - the .default scope grants all consented app roles
curl -s -X POST "https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/token" \
  -d "client_id=$APP_ID" \
  -d "client_secret=$CLIENT_SECRET" \
  -d "scope=https://graph.microsoft.com/.default" \
  -d "grant_type=client_credentials" | jq -r '.access_token'
 
# Swap the scope to target a different audience
#   https://api.securitycenter.microsoft.com/.default   -> Defender for Endpoint
#   https://management.azure.com/.default                -> ARM (Sentinel, Defender for Cloud)

Managed identity (from inside Azure)

Bash
# IMDS - works on any Azure VM/VMSS without a credential. Add &client_id=<uami> for user-assigned.
curl -s -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://graph.microsoft.com" |
  jq -r '.access_token'

A managed identity has no admin-consent UI, so its Graph and Defender app roles are granted by assignment. Do it once with the Graph PowerShell SDK:

PowerShell
Connect-MgGraph -Scopes 'AppRoleAssignment.ReadWrite.All', 'Application.Read.All'
 
$mi    = Get-MgServicePrincipal -Filter "displayName eq 'my-app-identity'"
 
# Microsoft Graph (well-known appId) - assign SecurityAlert.Read.All
$graph = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
$role  = $graph.AppRoles | Where-Object Value -eq 'SecurityAlert.Read.All'
New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $mi.Id `
  -PrincipalId $mi.Id -ResourceId $graph.Id -AppRoleId $role.Id
 
# Defender for Endpoint (WindowsDefenderATP appId) - assign Machine.Isolate
$mde   = Get-MgServicePrincipal -Filter "appId eq 'fc780465-2017-40d4-a0c5-307022471b92'"
$miso  = $mde.AppRoles | Where-Object Value -eq 'Machine.Isolate'
New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $mi.Id `
  -PrincipalId $mi.Id -ResourceId $mde.Id -AppRoleId $miso.Id

OIDC / workload identity federation in CI/CD

No secrets in the pipeline: the runner mints a short-lived OIDC token, and a federated credential on the app registration trusts it for a specific repo/branch/environment.

Register the federated credential (one-time)

Bash
az ad app federated-credential create --id "$APP_ID" --parameters '{
  "name": "github-main",
  "issuer": "https://token.actions.githubusercontent.com",
  "subject": "repo:libre-devops/defender-runbooks:ref:refs/heads/main",
  "audiences": ["api://AzureADTokenExchange"]
}'

GitHub Actions

YAML
permissions:
  id-token: write      # required for the runner to request an OIDC token
  contents: read
 
jobs:
  posture:
    runs-on: ubuntu-latest
    steps:
      - uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      # az is now authenticated with no secret; tokens for any audience follow
      - run: az security secure-scores show --name ascScore --query properties.score.percentage -o tsv

Azure DevOps

YAML
# A Workload Identity Federation service connection backs AzureCLI@2 - no secret stored.
steps:
  - task: AzureCLI@2
    inputs:
      azureSubscription: 'defender-wif-connection'   # WIF service connection name
      scriptType: bash
      scriptLocation: inlineScript
      inlineScript: az security assessment list --query "[?status.code=='Unhealthy']" -o table

Python (azure-identity)

Python
from azure.identity import (
    DefaultAzureCredential,        # env -> workload identity -> managed identity -> az cli
    ClientSecretCredential,
    ManagedIdentityCredential,
    WorkloadIdentityCredential,    # OIDC in AKS / federated CI
)
 
# One credential, many audiences - request the right scope per call.
credential = DefaultAzureCredential()
graph_token = credential.get_token("https://graph.microsoft.com/.default").token
mde_token   = credential.get_token("https://api.securitycenter.microsoft.com/.default").token
 
# Explicit forms when you are not relying on the default chain
ManagedIdentityCredential(client_id="<uami-client-id>")
ClientSecretCredential(tenant_id="<tid>", client_id="<app-id>", client_secret="<secret>")

DefaultAzureCredential is what every Python example below uses: it picks workload identity in CI (via the AZURE_* / federated-token-file env vars), a managed identity on Azure compute, and your az login session at a workstation - no code change between them.

See also: Permissions you will need below for the exact app roles each operation requires, and the AI Cheatsheet - Auth and Azure Cheatsheet for the same identity patterns applied to other services.


Defender for Cloud (az security)

Posture management for Azure subscriptions. Every command below requires a signed-in Azure CLI (az login) with at least Security Reader on the subscription; changing plans needs Security Admin.

Secure score

Show the overall subscription secure score

Bash
az security secure-scores show --name ascScore -o json

Secure score as a single percentage

Bash
az security secure-scores show --name ascScore \
  --query "properties.score.percentage" -o tsv

List per-control scores (which controls cost you the most)

Bash
az security secure-scores-controls list \
  --query "sort_by([].{control:displayName, current:score.current, max:score.max}, &max)[?max > \`0\`]" \
  -o table

Recommendations (assessments)

List all assessments

Bash
az security assessment list -o json

Only the unhealthy recommendations

Bash
az security assessment list \
  --query "[?status.code=='Unhealthy'].{name:displayName, resource:resourceDetails.id, severity:metadata.severity}" \
  -o table

Defender plans (pricing tiers)

List every Defender plan and its tier

Bash
az security pricing list \
  --query "value[].{plan:name, tier:pricingTier}" -o table

Show a single plan

Bash
az security pricing show --name StorageAccounts -o json

Enable a plan (Free -> Standard)

Bash
az security pricing create --name StorageAccounts --tier Standard

⚠️ Enabling a Standard plan starts billing immediately. Scope it deliberately and pair the change with a budget alert.

Security alerts (Azure CLI)

List active Defender for Cloud alerts

Bash
az security alert list \
  --query "[?status=='Active'].{name:alertDisplayName, severity:severity, time:timeGeneratedUtc}" \
  -o table

Show a single alert

Bash
az security alert show --name <alert-name> --location <region> -o json

Dismiss an alert

Bash
az security alert update --name <alert-name> --location <region> --status Dismiss

Defender for Cloud via Az PowerShell

The Az.Security module mirrors the CLI for engineers who live in PowerShell.

PowerShell
Connect-AzAccount
 
# Secure score and unhealthy assessments
Get-AzSecuritySecureScore
Get-AzSecurityAssessment | Where-Object { $_.StatusCode -eq 'Unhealthy' } |
  Select-Object DisplayName, ResourceDetailsId
 
# Plan tiers, and the active alerts
Get-AzSecurityPricing | Select-Object Name, PricingTier
Get-AzSecurityAlert | Where-Object { $_.State -eq 'Active' } |
  Select-Object AlertDisplayName, ReportedSeverity, TimeGeneratedUtc

See also: Azure - Auth & Context for service-principal creation and role assignment used by posture-as-code pipelines.


Windows Defender Antivirus

The built-in Defender module ships with Windows - no install required. Run an elevated PowerShell session. These are host-local; for fleet-wide control use Intune, Group Policy, or the Defender for Endpoint API further down.

Full engine and protection status

PowerShell
Get-MpComputerStatus

Just the bits that matter for a health check

PowerShell
Get-MpComputerStatus |
  Select-Object AMRunningMode, RealTimeProtectionEnabled,
                AntivirusSignatureLastUpdated, AntivirusSignatureVersion,
                IsTamperProtected, NISEnabled

Current preferences (exclusions, cloud level, sample submission)

PowerShell
Get-MpPreference |
  Select-Object MAPSReporting, SubmitSamplesConsent,
                ExclusionPath, ExclusionProcess, CloudBlockLevel

Run a scan

PowerShell
Start-MpScan -ScanType QuickScan    # or FullScan

Update signatures now

PowerShell
Update-MpSignature

Detection history (what was found and what was done)

PowerShell
Get-MpThreatDetection |
  Sort-Object InitialDetectionTime -Descending |
  Select-Object ThreatID, InitialDetectionTime, ActionSuccess,
                @{n='Resources';e={$_.Resources -join '; '}}

Map detection IDs to names and severity

PowerShell
Get-MpThreat |
  Select-Object ThreatID, ThreatName, SeverityID, DidThreatExecute

Add a path / process exclusion

PowerShell
Add-MpPreference -ExclusionPath 'C:\app\data', 'C:\cache'
Add-MpPreference -ExclusionProcess 'node.exe'

🔬 Exclusions are a common attacker persistence trick - they blind the engine to a folder. Treat the exclusion list as a security-sensitive config; review it in audits and alert on additions.

Raise the cloud protection level (aggressive)

PowerShell
Set-MpPreference -CloudBlockLevel HighPlus -MAPSReporting Advanced -SubmitSamplesConsent SendAllSamples

List Attack Surface Reduction (ASR) rule states

PowerShell
$ids = (Get-MpPreference).AttackSurfaceReductionRules_Ids
$acts = (Get-MpPreference).AttackSurfaceReductionRules_Actions
for ($i = 0; $i -lt $ids.Count; $i++) {
    [pscustomobject]@{ RuleId = $ids[$i]; Action = $acts[$i] }  # 0=Off 1=Block 2=Audit 6=Warn
}

Put an ASR rule into Block mode

PowerShell
# Block credential stealing from LSASS
Add-MpPreference -AttackSurfaceReductionRules_Ids 9e6c4e1f-7d60-472f-ba1a-a39ef669e4b2 `
                 -AttackSurfaceReductionRules_Actions Enabled

See also: Windows for event-log and firewall context, and Security - Defensive for broader host hardening.


Defender for Endpoint on Linux (mdatp)

The Linux agent exposes everything through the mdatp CLI. Most read commands work unprivileged; config changes need sudo. Output is human-readable by default; append nothing for the pretty form, or query single fields for scripts.

Agent health (one field, script-friendly)

Bash
mdatp health --field healthy             # true / false
mdatp health --field real_time_protection_enabled
mdatp health --field definitions_status  # up_to_date / ...

Full health dump as JSON

Bash
mdatp health --output json

Run a scan

Bash
mdatp scan quick
mdatp scan full
mdatp scan custom --path /var/www

Update definitions

Bash
sudo mdatp definitions update

Threat management

Bash
mdatp threat list                         # detections on this host
mdatp threat quarantine list              # what is quarantined
mdatp threat get --id <threat-id>

Real-time protection and EDR toggles

Bash
sudo mdatp config real-time-protection --value enabled
mdatp health --field edr_configuration_version

Folder / extension / process exclusions

Bash
sudo mdatp exclusion folder add --path /opt/app
sudo mdatp exclusion extension add --name .log
sudo mdatp exclusion process add --name ldconfig
mdatp exclusion list

Trigger an on-demand cloud connectivity test

Bash
mdatp connectivity test

Collect a diagnostic bundle for support

Bash
sudo mdatp diagnostic create

🔬 mdatp health --field healthy is the single best one-liner for a fleet health check - wire it into your config-management tool (Ansible/Salt) and alert on anything that is not true.

See also: Linux for the systemd and journald context to confirm the mdatp daemon is running and logging.


Client Analyzer (sensor health triage)

When a device shows as Inactive, No sensor data, or Impaired communications in the portal, the Microsoft Defender for Endpoint Client Analyzer (MDECA) is the first tool to reach for. It bundles onboarding state, cloud-connectivity results, configuration, and logs into one package you read locally or hand to Microsoft support. It runs on Windows, Linux, and macOS, before or after onboarding - so it doubles as a pre-flight prerequisites check.

🔬 Nothing is sent to Microsoft automatically. The output zip stays on the device and can contain PII (hostnames, usernames, IPs); share it with Microsoft CSS only through Secure File Exchange.

Windows

Download and run

PowerShell
# Download from https://aka.ms/mdatpanalyzer, extract MDEClientAnalyzer.zip, then from an
# elevated Command Prompt or PowerShell in the extracted folder:
.\MDEClientAnalyzer.cmd

On the modern unified solution the script calls MDEClientAnalyzer.exe for the cloud-connectivity tests and uses Sysinternals PsExec.exe to run them as Local System (emulating the SENSE service). Results land in MDEClientAnalyzerResult.zip.

What is in the result package

ItemWhy you care
MDEClientAnalyzer.htmThe main report - findings and remediation guidance, read this first
SystemInfoLogs/RegOnboardedInfoCurrent.JsonOnboarding state and org ID pulled from the registry
SystemInfoLogs/CertValidate.logCertificate revocation / TLS-inspection problems
EventLogs/sense.evtx, senseIR.evtx, utc.evtxEDR sensor, automated investigation, and DiagTrack logs
MdeConfigMgrLogs/*.jsonSecurity-management (Intune) policy and enforcement results

What to look out for (Windows)

  • ⚠️ ASR blocking the analyzer - the ASR rule Block process creations originating from PSExec and WMI commands blocks the connectivity test. Temporarily set it to Audit, add a folder exclusion, or disable it for the run.
  • ⚠️ PsExec must be allowed - WDAC / app-control or AV blocking PsExec.exe stops the cloud checks. Allow it at least while the analyzer runs.
  • 🚨 Signature errors mean tampering - every script in the package is Microsoft-signed. If it exits with a signature error, read issuerInfo.txt; do not “fix” it by unblocking a modified file - re-download from the official link.
  • 🔬 Sense stopped is normal pre-onboarding - on a device that is not onboarded yet the EDR sensor is stopped and the report reflects that. Run the analyzer anyway to validate connectivity before you onboard.

Linux

Since agent version 101.25082.0000 the analyzer ships inside the product, so on a modern install there is nothing to download.

Built-in (shipped with the agent)

Bash
# Self-contained binary - no Python required
cd /opt/microsoft/mdatp/tools/client_analyzer/binary
sudo ./MDESupportTool -d            # -d = full diagnostic bundle, written to /tmp/*.zip
 
# Or the Python build, same directory tree
cd /opt/microsoft/mdatp/tools/client_analyzer/python
sudo ./mde_support_tool.sh -d

Standalone (older agents, or running before install)

Bash
# Binary build - no Python dependency, prefer this on servers
wget --quiet -O XMDEClientAnalyzerBinary.zip https://aka.ms/XMDEClientAnalyzerBinary
unzip -q XMDEClientAnalyzerBinary.zip -d XMDEClientAnalyzerBinary
cd XMDEClientAnalyzerBinary
unzip -q SupportToolLinuxamd64Binary.zip      # or SupportToolLinuxarm64Binary.zip on ARM
sudo ./MDESupportTool -d
 
# Python build - needs Python 3 plus pip packages (decorator, sh, distro, lxml, psutil)
wget --quiet -O XMDEClientAnalyzer.zip https://aka.ms/XMDEClientAnalyzer
unzip -q XMDEClientAnalyzer.zip -d XMDEClientAnalyzer && cd XMDEClientAnalyzer
chmod a+x mde_support_tool.sh
./mde_support_tool.sh                # run once as a normal user to install deps
sudo ./mde_support_tool.sh -d        # then collect with root

🔬 The unzip package is required to install and acl to run. Behind a proxy, pass it through: https_proxy=https://proxy:8080 sudo ./mde_support_tool.sh -d.

Targeted checks (the useful subcommands)

Bash
# Are the MDE cloud URLs reachable? Pass the onboarding blob to test the real geo
sudo ./MDESupportTool connectivitytest -o ~/MicrosoftDefenderATPOnboardingLinuxServer.py
 
# Prerequisite / onboarding report -> installation_report.json
# (distro support, min requirements, connectivity, mde_health, folder_perm)
sudo ./MDESupportTool installation --all
 
# Reproduce and capture a performance problem -> perf_benchmark.tar.gz
sudo ./MDESupportTool performance --frequency 500
 
# auditd pegging the CPU? cap it to 2500 events/sec (affects every auditd consumer)
sudo ./mde_support_tool.sh ratelimit -e true

What to look out for (Linux)

  • 🚨 auditd CPU storms - on the auditd backend MDE adds rules that can spike CPU. Capture it with performance, then tame it with ratelimit or exclude - but remember ratelimit drops events for all auditd consumers, not just MDE.
  • ⚠️ eBPF vs auditd backend - the bundle records which provider is active (ebpf_* vs auditd_* files). Modern distros should be on eBPF; a silent fall back to auditd is a common root cause of performance tickets.
  • ⚠️ CRLF line endings - editing the wrapper scripts on Windows leaves CRLF endings that break them on Linux. Run dos2unix on anything you touched.
  • 🔬 Read installation_report.json first - support_status, distro, connectivitytest, and folder_perm tell you in one file whether the host is even a supported, reachable configuration.

Common triage across both

  • 🔬 Connectivity is the usual culprit - most Inactive / Impaired communications sensors are a proxy or firewall blocking the MDE service URLs. Run the analyzer’s connectivity test before any deeper digging, and allow the documented MDE service URLs through the proxy.
  • 🔬 Run it before onboarding too - as a prerequisites checker it catches unsupported distros / OS builds, missing dependencies, and blocked URLs before a rollout.
  • ⚠️ Treat the output as sensitive - the result zip contains PII; share it with Microsoft only via Secure File Exchange, and store it like any other host forensic artifact.

See also: the mdatp commands above for day-to-day Linux agent control, Windows Defender Antivirus for the on-device engine cmdlets, and Microsoft’s client analyzer overview  for the full file-by-file reference.


Defender XDR - Graph Security API

The unified https://graph.microsoft.com/v1.0/security surface returns alerts, incidents, and hunting results across every Defender product. Response actions on devices (isolate, scan, collect package) live on the older Defender for Endpoint API at https://api.securitycenter.microsoft.com.

Permissions you will need

OperationGraph application permissionDefender for Endpoint permission
Read alerts / incidentsSecurityAlert.Read.All, SecurityIncident.Read.All-
Run advanced huntingThreatHunting.Read.AllAdvancedQuery.Read.All
Isolate / release a device-Machine.Isolate
Run AV scan on a device-Machine.Scan
Collect investigation package-Machine.CollectForensics

Get a token

The examples below use $TOKEN for a Microsoft Graph token and $MDE_TOKEN for a Defender for Endpoint token - acquire each per the Authentication section (they are different audiences). The quickest form once you have an Azure CLI session:

Bash
TOKEN=$(az account get-access-token --resource https://graph.microsoft.com --query accessToken -o tsv)
MDE_TOKEN=$(az account get-access-token --resource https://api.securitycenter.microsoft.com --query accessToken -o tsv)

List high-severity new alerts

Bash
curl -s -G "https://graph.microsoft.com/v1.0/security/alerts_v2" \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode '$filter=severity eq '\''high'\'' and status eq '\''new'\''' \
  --data-urlencode '$top=50' | jq '.value[] | {id, title, severity, status}'

Get an incident with its alerts

Bash
curl -s -G "https://graph.microsoft.com/v1.0/security/incidents/<incident-id>" \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode '$expand=alerts' | jq

Run an advanced hunting (KQL) query over the API

Bash
curl -s -X POST "https://graph.microsoft.com/v1.0/security/runHuntingQuery" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query":"DeviceProcessEvents | where Timestamp > ago(1h) | take 10"}' |
  jq '.results'

Isolate a device (Defender for Endpoint API)

Bash
curl -s -X POST \
  "https://api.securitycenter.microsoft.com/api/machines/<machine-id>/isolate" \
  -H "Authorization: Bearer $MDE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"Comment":"IR-1234 containment","IsolationType":"Full"}'

List and act on devices (Defender for Endpoint API, Bash)

Bash
MDE="https://api.securitycenter.microsoft.com/api"
 
# Onboarded machines, highest risk first
curl -s -G "$MDE/machines" -H "Authorization: Bearer $MDE_TOKEN" \
  --data-urlencode '$top=100' |
  jq -r '.value | sort_by(.riskScore) | reverse[] | [.computerDnsName, .riskScore, .healthStatus] | @tsv'
 
# Resolve a hostname to its machine id
MID=$(curl -s -G "$MDE/machines" -H "Authorization: Bearer $MDE_TOKEN" \
  --data-urlencode "\$filter=computerDnsName eq 'web01'" | jq -r '.value[0].id')
 
# Run a full AV scan
curl -s -X POST "$MDE/machines/$MID/runAntiVirusScan" \
  -H "Authorization: Bearer $MDE_TOKEN" -H "Content-Type: application/json" \
  -d '{"Comment":"IR-1234","ScanType":"Full"}'
 
# Collect an investigation (forensics) package
curl -s -X POST "$MDE/machines/$MID/collectInvestigationPackage" \
  -H "Authorization: Bearer $MDE_TOKEN" -H "Content-Type: application/json" \
  -d '{"Comment":"IR-1234 forensics"}'
 
# Check the status of a submitted machine action
curl -s "$MDE/machineactions/<action-id>" \
  -H "Authorization: Bearer $MDE_TOKEN" | jq '{type, status, machineId, creationDateTimeUtc}'
Bash
# Graph and the Defender API cap page size; follow nextLink until it is gone.
url="$MDE/alerts?\$top=1000"
while [ -n "$url" ] && [ "$url" != "null" ]; do
  page=$(curl -s "$url" -H "Authorization: Bearer $MDE_TOKEN")
  echo "$page" | jq -c '.value[]'
  url=$(echo "$page" | jq -r '."@odata.nextLink" // ""')
done

The same calls in PowerShell (no module, just Invoke-AzRestMethod)

PowerShell
Connect-AzAccount
$mde = 'https://api.securitycenter.microsoft.com'
 
# Invoke-AzRestMethod handles the bearer token for the target resource for you
$machines = (Invoke-AzRestMethod -Method GET -Uri "$mde/api/machines?`$top=100").Content |
  ConvertFrom-Json
$machines.value |
  Sort-Object riskScore -Descending |
  Select-Object computerDnsName, riskScore, healthStatus -First 20
 
# Submit a response action
$id = ($machines.value | Where-Object computerDnsName -eq 'web01').id
Invoke-AzRestMethod -Method POST -Uri "$mde/api/machines/$id/isolate" `
  -Payload (@{ Comment = 'IR-1234 containment'; IsolationType = 'Full' } | ConvertTo-Json)

Or with the Microsoft Graph PowerShell SDK

PowerShell
Connect-MgGraph -Scopes 'SecurityAlert.Read.All', 'SecurityIncident.Read.All'
 
Get-MgSecurityIncident -Filter "status eq 'active'" -Top 20 |
  Select-Object Id, DisplayName, Severity, @{n='Alerts';e={$_.Alerts.Count}}
 
# Run an advanced hunting query through the SDK
$body = @{ query = 'DeviceProcessEvents | where Timestamp > ago(1h) | take 10' }
(Invoke-MgGraphRequest -Method POST `
  -Uri 'https://graph.microsoft.com/v1.0/security/runHuntingQuery' `
  -Body ($body | ConvertTo-Json)).results

🚨 Device isolation and AV scans are high-impact response actions. Gate them behind an approval step in any automation, log the Comment with a ticket reference, and make sure your runbook documents how to release isolation (/unisolate).

See also: KQL - Threat Hunting for the hunting queries you pass to runHuntingQuery, and the AI Cheatsheet - Security Copilot for natural-language incident triage over the same data.


Advanced Hunting (KQL)

Defender XDR advanced hunting runs KQL over the device, identity, email, and cloud-app tables. These are Defender-response-oriented snippets; the KQL cheatsheet holds the full table reference and the broader hunting library.

Devices that are candidates for isolation (active high-severity alerts)

KQL
AlertInfo
| where Timestamp > ago(24h)
| where Severity == "High"
| join kind=inner AlertEvidence on AlertId
| where EntityType == "Machine"
| summarize Alerts = dcount(AlertId), Titles = make_set(Title) by DeviceId, DeviceName
| sort by Alerts desc

LSASS credential access (Mimikatz-style)

KQL
DeviceProcessEvents
| where Timestamp > ago(7d)
| where FileName in~ ("rundll32.exe", "procdump.exe", "taskmgr.exe")
| where ProcessCommandLine has_any ("lsass", "MiniDump", "comsvcs.dll")
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine

New ASR exclusions or AV exclusions added on a device

KQL
DeviceRegistryEvents
| where Timestamp > ago(7d)
| where RegistryKey has @"Windows Defender\Exclusions"
| where ActionType == "RegistryValueSet"
| project Timestamp, DeviceName, RegistryKey, RegistryValueName, InitiatingProcessAccountName

Map an alert to the full device timeline (pivot)

KQL
let target = "<device-id>";
union DeviceProcessEvents, DeviceNetworkEvents, DeviceFileEvents, DeviceLogonEvents
| where Timestamp between (ago(2h) .. now())
| where DeviceId == target
| sort by Timestamp asc
| project Timestamp, $table, ActionType, FileName, RemoteIP, AccountName

See also: KQL - Threat Hunting for processes, network, identity, and email hunting plus multi-stage alert chaining.


Running KQL from the CLI and SDKs

The portal is fine for ad-hoc hunting, but runbooks, scheduled jobs, and CI need to run KQL headless against the Log Analytics / Sentinel workspace. The same query runs three ways.

Azure CLI - az monitor log-analytics query

Bash
# The query API wants the workspace GUID (customerId), not its resource name
WSID=$(az monitor log-analytics workspace show -g "$RG" -n "$WS" --query customerId -o tsv)
 
az monitor log-analytics query \
  --workspace "$WSID" \
  --analytics-query "SecurityAlert | where TimeGenerated > ago(24h) | summarize Count=count() by AlertSeverity" \
  -o table

PowerShell - Invoke-AzOperationalInsightsQuery

PowerShell
$wsid = (Get-AzOperationalInsightsWorkspace -ResourceGroupName $rg -Name $ws).CustomerId
$kql  = 'DeviceProcessEvents | where Timestamp > ago(1h) | summarize Count=count() by DeviceName'
 
$result = Invoke-AzOperationalInsightsQuery -WorkspaceId $wsid -Query $kql
$result.Results | Sort-Object Count -Descending | Format-Table

Python - azure-monitor-query

Python
# pip install azure-monitor-query azure-identity
from datetime import timedelta
 
from azure.identity import DefaultAzureCredential
from azure.monitor.query import LogsQueryClient, LogsQueryStatus
 
client = LogsQueryClient(DefaultAzureCredential())
 
response = client.query_workspace(
    workspace_id="<workspace-guid>",
    query="SigninLogs | where TimeGenerated > ago(1h) | summarize Count=count() by ResultType",
    timespan=timedelta(hours=1),
)
 
if response.status == LogsQueryStatus.SUCCESS:
    for table in response.tables:
        for row in table.rows:
            print(dict(zip(table.columns, row)))

🔬 Device tables (DeviceProcessEvents, etc.) are queryable through Log Analytics only when the workspace receives Defender XDR data via the connector. With raw Defender data only, hunt through Graph runHuntingQuery instead - the PowerShell and Python clients above both do this.

See also: KQL - Operational Monitoring for host-health, downtime, and request queries you can run the same way.


Microsoft Sentinel - Watchlists & Incidents

Watchlists are reference data (VIP users, terminated staff, approved IPs, asset inventories) you join against in detections. They are managed through the Sentinel REST API on Azure Resource Manager, the Microsoft.SecurityInsights provider.

Variables used below

Bash
SUB="<subscription-id>"
RG="<resource-group>"
WS="<log-analytics-workspace>"
API="2024-03-01"
BASE="https://management.azure.com/subscriptions/$SUB/resourceGroups/$RG/providers/Microsoft.OperationalInsights/workspaces/$WS/providers/Microsoft.SecurityInsights"
ARM=$(az account get-access-token --resource https://management.azure.com --query accessToken -o tsv)

List watchlists

Bash
curl -s -H "Authorization: Bearer $ARM" \
  "$BASE/watchlists?api-version=$API" | jq '.value[] | {alias:.name, items:.properties.numberOfLinesToSkip}'

Create a watchlist from inline CSV

Bash
curl -s -X PUT "$BASE/watchlists/HighValueAssets?api-version=$API" \
  -H "Authorization: Bearer $ARM" -H "Content-Type: application/json" \
  -d '{
    "properties": {
      "displayName": "High Value Assets",
      "provider": "LibreDevOps",
      "source": "Local file",
      "itemsSearchKey": "Hostname",
      "rawContent": "Hostname,Tier,Owner\nDC01,0,platform\nSQL01,1,data",
      "contentType": "text/csv"
    }
  }'

Add a single item to a watchlist

Bash
curl -s -X PUT "$BASE/watchlists/HighValueAssets/watchlistItems/$(uuidgen)?api-version=$API" \
  -H "Authorization: Bearer $ARM" -H "Content-Type: application/json" \
  -d '{"properties":{"itemsKeyValue":{"Hostname":"WEB01","Tier":"2","Owner":"web"}}}'

Delete a watchlist

Bash
curl -s -X DELETE "$BASE/watchlists/HighValueAssets?api-version=$API" \
  -H "Authorization: Bearer $ARM"

Join a watchlist inside a detection (KQL)

KQL
let HVA = _GetWatchlist('HighValueAssets');
DeviceLogonEvents
| where Timestamp > ago(1h)
| where LogonType == "RemoteInteractive"
| join kind=inner HVA on $left.DeviceName == $right.Hostname
| where Tier == "0"
| project Timestamp, DeviceName, AccountName, Tier, Owner

Incidents (Azure CLI via az rest)

Sentinel has no dedicated first-class CLI for most operations, so az rest against the management API is the portable path. It reuses the $BASE and $API variables from above.

List active incidents

Bash
az rest --method get \
  --url "$BASE/incidents?api-version=$API&\$filter=properties/status eq 'Active'" \
  --query "value[].{title:properties.title, severity:properties.severity, number:properties.incidentNumber}" \
  -o table

Close an incident as a true positive

Bash
az rest --method put \
  --url "$BASE/incidents/<incident-id>?api-version=$API" \
  --headers "Content-Type=application/json" \
  --body '{
    "properties": {
      "title": "Suspicious LSASS access on web01",
      "status": "Closed",
      "severity": "Medium",
      "classification": "TruePositive",
      "classificationReason": "SuspiciousActivity"
    }
  }'

Add an investigation comment

Bash
az rest --method put \
  --url "$BASE/incidents/<incident-id>/comments/$(uuidgen)?api-version=$API" \
  --headers "Content-Type=application/json" \
  --body '{"properties":{"message":"Triaged by automation - device isolated, escalated to tier 2."}}'

Incidents and watchlists (Az PowerShell, Az.SecurityInsights)

PowerShell
Install-Module Az.SecurityInsights -Scope CurrentUser
 
# Triage queue - active incidents, newest first
Get-AzSentinelIncident -ResourceGroupName $rg -WorkspaceName $ws |
  Where-Object Status -eq 'Active' |
  Sort-Object CreatedTimeUtc -Descending |
  Select-Object IncidentNumber, Title, Severity, Owner
 
# Close an incident
Update-AzSentinelIncident -ResourceGroupName $rg -WorkspaceName $ws -Id $incidentId `
  -Title 'Suspicious LSASS access on web01' -Status Closed -Severity Medium `
  -Classification TruePositive -ClassificationReason SuspiciousActivity
 
# Comment, then manage watchlists
New-AzSentinelIncidentComment -ResourceGroupName $rg -WorkspaceName $ws `
  -IncidentId $incidentId -Message 'Triaged by automation.'
 
Get-AzSentinelWatchlist -ResourceGroupName $rg -WorkspaceName $ws
New-AzSentinelWatchlist -ResourceGroupName $rg -WorkspaceName $ws -Alias HighValueAssets `
  -DisplayName 'High Value Assets' -Provider 'LibreDevOps' -Source 'Local file' `
  -ItemsSearchKey 'Hostname' -RawContent (Get-Content ./assets.csv -Raw)

See also: PowerShell - Microsoft Sentinel for watchlist export helpers, and Azure - Azure Monitor & Log Analytics for the workspace the watchlist lives in.


Python Reference Implementation

A small, typed client that authenticates once with azure-identity and reuses the token across Graph and Defender for Endpoint calls. Install: pip install azure-identity httpx tenacity.

Authenticated client with retry and Retry-After handling

Python
from __future__ import annotations
 
import httpx
from azure.identity import DefaultAzureCredential
from tenacity import (
    retry, retry_if_exception, stop_after_attempt,
    wait_exponential_jitter,
)
 
GRAPH = "https://graph.microsoft.com/v1.0"
MDE = "https://api.securitycenter.microsoft.com/api"
 
_RETRYABLE = {408, 429, 500, 502, 503, 504}
 
 
def _is_retryable(exc: BaseException) -> bool:
    # Retry transport errors and the transient HTTP statuses; 400/401/403 fail fast.
    if isinstance(exc, httpx.TransportError):
        return True
    if isinstance(exc, httpx.HTTPStatusError):
        return exc.response.status_code in _RETRYABLE
    return False
 
 
class DefenderClient:
    """Thin wrapper over the Graph Security API and Defender for Endpoint API."""
 
    def __init__(self, credential: DefaultAzureCredential | None = None) -> None:
        self._credential = credential or DefaultAzureCredential()
        self._http = httpx.Client(timeout=30.0)
 
    def _token(self, resource: str) -> str:
        # azure-identity scopes use the "/.default" suffix on the resource.
        return self._credential.get_token(f"{resource}/.default").token
 
    @retry(
        retry=retry_if_exception(_is_retryable),
        wait=wait_exponential_jitter(initial=2, max=60),
        stop=stop_after_attempt(5),
        reraise=True,
    )
    def _request(self, method: str, url: str, resource: str, **kwargs) -> httpx.Response:
        headers = {"Authorization": f"Bearer {self._token(resource)}"}
        headers.update(kwargs.pop("headers", {}))
        resp = self._http.request(method, url, headers=headers, **kwargs)
        resp.raise_for_status()
        return resp
 
    def list_alerts(self, severity: str = "high", status: str = "new", top: int = 50) -> list[dict]:
        params = {
            "$filter": f"severity eq '{severity}' and status eq '{status}'",
            "$top": top,
        }
        resp = self._request("GET", f"{GRAPH}/security/alerts_v2",
                             "https://graph.microsoft.com", params=params)
        return resp.json().get("value", [])
 
    def run_hunting_query(self, query: str) -> list[dict]:
        resp = self._request("POST", f"{GRAPH}/security/runHuntingQuery",
                             "https://graph.microsoft.com", json={"query": query})
        return resp.json().get("results", [])
 
    def list_incidents(self, top: int = 50) -> list[dict]:
        resp = self._request("GET", f"{GRAPH}/security/incidents",
                             "https://graph.microsoft.com", params={"$top": top})
        return resp.json().get("value", [])
 
    def list_machines(self, odata_filter: str | None = None) -> list[dict]:
        params = {"$filter": odata_filter} if odata_filter else None
        resp = self._request("GET", f"{MDE}/machines",
                             "https://api.securitycenter.microsoft.com", params=params)
        return resp.json().get("value", [])
 
    def run_av_scan(self, machine_id: str, comment: str, scan_type: str = "Full") -> dict:
        body = {"Comment": comment, "ScanType": scan_type}
        resp = self._request("POST", f"{MDE}/machines/{machine_id}/runAntiVirusScan",
                             "https://api.securitycenter.microsoft.com", json=body)
        return resp.json()
 
    def isolate_device(self, machine_id: str, comment: str, full: bool = True) -> dict:
        body = {"Comment": comment, "IsolationType": "Full" if full else "Selective"}
        resp = self._request("POST", f"{MDE}/machines/{machine_id}/isolate",
                             "https://api.securitycenter.microsoft.com", json=body)
        return resp.json()

Use it

Python
client = DefenderClient()
 
for alert in client.list_alerts(severity="high"):
    print(alert["id"], alert["title"])
 
rows = client.run_hunting_query(
    "DeviceProcessEvents | where Timestamp > ago(1h) "
    "| where FileName == 'powershell.exe' | take 20"
)
print(f"{len(rows)} matching process events")
 
# Find a high-risk host and kick off a full scan
for machine in client.list_machines(odata_filter="riskScore eq 'High'"):
    print("scanning", machine["computerDnsName"])
    client.run_av_scan(machine["id"], comment="auto-triage", scan_type="Full")

Sentinel watchlist via the management SDK

Python
# pip install azure-mgmt-securityinsight azure-identity
from azure.identity import DefaultAzureCredential
from azure.mgmt.securityinsight import SecurityInsights
 
client = SecurityInsights(DefaultAzureCredential(), subscription_id="<sub-id>")
 
for wl in client.watchlists.list(resource_group_name="<rg>", workspace_name="<ws>"):
    print(wl.name, wl.display_name, wl.items_search_key)

Incidents with the Microsoft Graph SDK (async)

The official msgraph-sdk is the typed alternative to hand-rolled HTTP - it pages, deserialises, and refreshes tokens for you.

Python
# pip install msgraph-sdk azure-identity
import asyncio
 
from azure.identity.aio import DefaultAzureCredential
from msgraph import GraphServiceClient
 
 
async def main() -> None:
    credential = DefaultAzureCredential()
    graph = GraphServiceClient(credential, scopes=["https://graph.microsoft.com/.default"])
 
    incidents = await graph.security.incidents.get()
    for inc in incidents.value or []:
        print(inc.id, inc.display_name, inc.severity, inc.status)
 
 
asyncio.run(main())

See also: Python for DefaultAzureCredential setup and async patterns, and Logging Standards for emitting these calls as structured JSON with trace correlation.


Workbooks

Sentinel and Azure Monitor workbooks are KQL-backed dashboards stored as ARM resources (Microsoft.Insights/workbooks). Treat them as code: author in the portal, export the JSON, and deploy through your pipeline so every environment renders the same SOC view.

Deploy a workbook from a template (Bicep)

BICEP
param workbookDisplayName string = 'Defender XDR - Response Overview'
param workspaceResourceId string
 
resource workbook 'Microsoft.Insights/workbooks@2023-06-01' = {
  name: guid(resourceGroup().id, workbookDisplayName)
  location: resourceGroup().location
  kind: 'shared'
  properties: {
    displayName: workbookDisplayName
    category: 'sentinel'
    sourceId: workspaceResourceId
    serializedData: loadTextContent('./workbook-content.json')
  }
}

Deploy with the Azure CLI

Bash
az deployment group create \
  --resource-group "$RG" \
  --template-file workbook.bicep \
  --parameters workspaceResourceId="/subscriptions/$SUB/resourceGroups/$RG/providers/Microsoft.OperationalInsights/workspaces/$WS"

Example workbook tile query (alert volume by severity)

KQL
AlertInfo
| where Timestamp > ago(30d)
| summarize Alerts = count() by bin(Timestamp, 1d), Severity
| render timechart

🔬 Export the workbook JSON straight from the portal (Edit -> Advanced Editor -> Gallery Template) and check it into source control. Parameterise the workspaceResourceId so the same template lands in dev, test, and prod.

See also: KQL for the queries that power workbook tiles, and Bicep for the deployment-as-code patterns above.


PowerShell Reference - LibreDevOpsHelpers

The LibreDevOpsHelpers module wraps every surface on this page behind consistent, logged cmdlets. It handles token caching and refresh, exponential backoff with Retry-After, and a single 401-refresh retry through Invoke-LdoGraphRequest, so the Defender cmdlets stay thin.

PowerShell
Install-Module LibreDevOpsHelpers -Scope CurrentUser
Connect-AzAccount   # or a managed identity in CI

Defender for Cloud posture

PowerShell
(Get-LdoDefenderSecureScore).properties.score.percentage
Get-LdoDefenderRecommendation -UnhealthyOnly
Set-LdoDefenderPlan -Name StorageAccounts -Tier Standard

Defender XDR alerts and hunting

PowerShell
Get-LdoDefenderAlert -Severity high -Status new
Invoke-LdoDefenderHuntingQuery -Query 'DeviceProcessEvents | take 10'

Endpoint response actions

PowerShell
Invoke-LdoDefenderDeviceIsolation -DeviceId $id -Comment 'IR-1234 containment'
Invoke-LdoDefenderDeviceIsolation -DeviceId $id -Release        # release isolation
Invoke-LdoDefenderAvScan -DeviceId $id -ScanType Full

Windows Defender Antivirus (Windows only)

PowerShell
(Get-LdoDefenderAvStatus).RealTimeProtectionEnabled
Start-LdoDefenderAvScan -ScanType Quick
Update-LdoDefenderAvSignature
Add-LdoDefenderAvExclusion -Path 'C:\app', 'C:\cache'

Defender for Endpoint on Linux (Linux only)

PowerShell
Get-LdoMdatpHealth -Field healthy
Start-LdoMdatpScan -ScanType Full
Update-LdoMdatpDefinition
Add-LdoMdatpExclusion -Path /opt/app

The module’s request layer is reusable on its own: Invoke-LdoGraphRequest gives you the same retry, backoff, and 401-refresh behaviour against any Graph endpoint.

PowerShell
# Read with automatic paging-friendly retries
Invoke-LdoGraphRequest -Uri 'https://graph.microsoft.com/v1.0/security/incidents?$top=10'
 
# Write, body is JSON-serialised for you
Invoke-LdoGraphRequest -Method Post `
  -Uri 'https://graph.microsoft.com/v1.0/security/runHuntingQuery' `
  -Body @{ query = 'AlertInfo | where Severity == "High" | take 5' }

See also: PowerShell for the broader Azure automation helpers, and PowerShell Standards for the strict-mode, structured-error, and logging conventions these cmdlets follow.


Anti-patterns

  • 🚨 Automating isolation with no human gate - device isolation cuts a host off the network. A false positive that auto-isolates a domain controller is a self-inflicted outage. Require approval, log a ticket reference, and rehearse the release path.
  • 🚨 Client secrets in scripts - never embed an Entra app secret in a runbook. Use a managed identity (DefaultAzureCredential) in CI and on Azure-hosted runners, or a workload identity federation, so there is no secret to leak.
  • ⚠️ Over-scoped Graph permissions - an app with Machine.Isolate and Machine.Scan is a response weapon. Grant the narrowest set per automation, and split read-only hunting apps from response apps.
  • ⚠️ AV exclusions as a fix - excluding a folder to “stop the AV noise” blinds the engine and is a known persistence technique. Investigate the detection instead; if an exclusion is genuinely needed, make it as narrow as possible and review it regularly.
  • 🔬 Polling alerts instead of streaming - hammering alerts_v2 on a tight loop wastes throttling budget. Stream alerts and incidents to Sentinel or an event hub and react to them, rather than polling the API.
  • 🔬 Hunting queries with no time bound - runHuntingQuery over an unscoped table is slow and can time out. Always lead with | where Timestamp > ago(...) exactly as you would in the portal.
  • ⚠️ Ignoring Retry-After - the Graph and Defender APIs throttle aggressively (HTTP 429). Honour the Retry-After header and back off; a fixed-interval retry just prolongs the throttling.

See Also

Last updated on