xdr-cli handbook
Microsoft Defender XDR investigation CLI — for humans and AI agents.
Website · Get started · Search the docs
xdr lets a SOC analyst — or an AI agent working on their behalf — triage
incidents, run advanced hunting, and take response actions in Microsoft
Defender XDR from a terminal. Every large read is saved as a private JSONL
artifact and summarised by a one-line receipt, so results are greppable,
pipeable, and small enough for an agent’s context window.
Watch the guided investigation demo
Demo tenant; tenant and object identifiers replaced for publication.
What it does
Section titled “What it does”- Incidents and alerts — list, filter, show (with alerts and their evidence), and update status, classification, determination and comments.
- Guided investigation —
xdr investigate <id>pulls the incident, extracts devices/users/IPs/hashes, runs the relevant hunting queries, and prints suggested next steps with ready-to-run commands. - Advanced hunting — run ad-hoc KQL, or pick from a library of 66 hunting queries (process trees, Kerberos delegation abuse, token replay, inbox rules, OAuth consent anomalies, lateral movement, ransomware precursors, …) that take typed parameters and are escaped before they reach the API. Three are marked beta; see the library reference.
- Response actions — isolate/release, scan, restrict/unrestrict execution,
collect and download investigation packages, with confirmation,
--dry-run, and a local audit log. - Two ways to sign in — reuse your logged-in Defender portal session (minutes to set up, no app registration), or register an Entra app and use Microsoft’s supported APIs. See Two ways to sign in.
- Built for agents — compact JSON receipts on stdout, progress on stderr,
one-line structured errors with a
corrected_argvhint, and anAGENTS.mdthat tells Claude Code, Copilot CLI, or Codex exactly how to drive it. - Playbooks — alert-title → playbook mapping for common Defender alerts (BEC, forwarding rules, ransomware, AiTM, C2, …).
- Optional extras — a schema graph that learns identifier pivots from your saved hunts (exportable to BloodHound), local investigation sessions with an improvement loop, and an unofficial device-timeline download that reaches back ~180 days. None of these are needed to work an incident.
Is it for you?
Section titled “Is it for you?”Use xdr if you work incidents in Defender XDR and want to do it from a
shell — because you script, because you pair with an AI agent, or because
clicking through the portal is slower than typing. Nothing leaves your
machine except the API calls to Microsoft, made either with your own browser
session or with your own Entra app registration and delegated permissions.
It is not a SIEM, a scheduler, or a replacement for the portal’s action center. Response actions require an explicit command, a comment, and a confirmation.
Install
Section titled “Install”Requires Python 3.11+. pipx gives xdr its own isolated environment and
puts the command on your PATH, which matters when an agent spawns it as a
subprocess.
Linux / macOS
Section titled “Linux / macOS”pipx install "git+https://github.com/NerfBlasters/m365-xdr-cli.git"xdr --versionWindows (PowerShell)
Section titled “Windows (PowerShell)”python -m pip install --user pipxpython -m pipx ensurepath# refresh PATH in this session so pipx and xdr are visible without reopening$env:Path = [Environment]::GetEnvironmentVariable("Path","User") + ";" + [Environment]::GetEnvironmentVariable("Path","Machine")
pipx install "git+https://github.com/NerfBlasters/m365-xdr-cli.git"xdr --versionUpdating
Section titled “Updating”pipx upgrade xdr-cli # or: pipx reinstall xdr-cliDevelopers: see Development for an editable install.
Shell completion
Section titled “Shell completion”xdr uses Click’s shell completion. To enable it for the current Bash session:
eval "$(_XDR_COMPLETE=bash_source xdr)"For Zsh use eval "$(_XDR_COMPLETE=zsh_source xdr)"; for Fish use
_XDR_COMPLETE=fish_source xdr | source. To avoid generating the script on
every shell startup, save that shell’s generated output and source the saved
file from your shell configuration. PowerShell users can generate its script
with $env:_XDR_COMPLETE = 'powershell_source'; xdr, then clear the variable
with Remove-Item Env:_XDR_COMPLETE and load the saved script.
The former --install-completion and --show-completion options were removed
in 0.15.0. Help now uses plain Click formatting; command examples and parameter
help remain available through --help.
Two ways to sign in
Section titled “Two ways to sign in”| Portal cookie (quick start) | Entra app registration | |
|---|---|---|
| Setup | Minutes: copy one request from your browser’s DevTools | Register an app, add permissions, get admin consent |
| Talks to | The Defender portal’s own apiproxy interface — undocumented and unsupported by Microsoft; can break without notice | Microsoft Graph security and Defender for Endpoint APIs — documented and supported |
| Credential | Your browser session cookie: a bearer credential for you, subject to the same expiry, Conditional Access and sign-in-frequency rules as the portal. Renewal is manual (re-import). | MSAL tokens with refresh; or a service principal for automation |
| Covers | Hunting, incidents, alerts, investigate, domains, device show/timeline/action-status, every response action, package download | Everything except package download and the AD domain inventory |
| Permissions | Whatever your user already has in the portal (Defender RBAC applies) | Delegated permissions you grant, plus the user’s Defender roles |
Start with the portal cookie if you want to try the tool today. Set up the app
registration when you need a supported path, CI/automation, or a team-wide
credential. Both can coexist; xdr picks the official backend when its
credentials are present and the cookie otherwise (--backend overrides).
Details, limits and error codes: docs/portal_cookie.md.
Quick start (portal cookie)
Section titled “Quick start (portal cookie)”-
Tell
xdrwhich tenant you are in. Create~/.xdr-cli/config.toml(or add the line to an existing one). The Directory (tenant) ID is on the Entra admin center’s Overview page, and in Defender portal URLs astid=.tenant_id = "<TENANT_ID>" -
Capture your session, import it, and start investigating:
# 2. Capture your portal session (tested with Microsoft Edge):# - sign in to https://security.microsoft.com and open any device's# Timeline tab# - open DevTools (F12) > Network, filter for "apiproxy"# - right-click the timeline request > Copy > "Copy as cURL (bash)"# - save the clipboard to a file, e.g. ~/mde-curl.txtxdr auth portal-cookie ~/mde-curl.txt # stores the cookie (0600) and # shreds the source filexdr auth status # backend: portal-cookie
# 3. Look around.xdr incidents list --since 7d --severity highxdr incidents show <ID> --expand alerts # <ID> is .id from the list
# 4. Investigate one incident end to end.xdr investigate --auto-enrich <ID>
# 5. Hunt.xdr hunt run "DeviceProcessEvents | where Timestamp > ago(1d) | where FileName == 'powershell.exe' | take 10"xdr library list --search kerberosxdr library run qry_process_tree --param device_name=WS-01Optional: xdr lists init copies the reference-list templates the hunting
library reads (internal subnets, known-good signers, tenant domains, …) into
~/.xdr-cli/lists/. Generic lists ship with defaults; tenant-specific ones
such as TenantDomains start empty. Not needed for incidents or
investigate.
When a command answers with exit 2 in cookie mode, the session has expired:
capture and import a fresh cookie. xdr auth logout deletes the local cookie
only; it does not sign you out of the portal.
Global flags go before the subcommand: xdr --quiet incidents list,
xdr --no-interactive investigate 42, xdr --backend official hunt run ….
Setting up the Entra app registration
Section titled “Setting up the Entra app registration”xdr can instead authenticate as you through an app registration in your
tenant, over Microsoft’s supported APIs. One registration can be shared by
everyone on the team.
-
Azure Portal → Microsoft Entra ID → App registrations → New registration. Name it (
xdr-cli), choose Single tenant, and add a Public client/native redirect URI ofhttp://localhost. If anyone will sign in on Windows, also addms-appx-web://Microsoft.AAD.BrokerPlugin/<client-id>(your application ID from step 2), which the Windows sign-in broker (WAM) requires. -
From Overview, copy the Application (client) ID and Directory (tenant) ID.
-
API permissions → Add a permission. Add these delegated permissions:
API Permission Used by Microsoft Graph SecurityIncident.ReadWrite.Allincidents list/show/update,investigateMicrosoft Graph SecurityAlert.Read.Allalerts list/showMicrosoft Graph ThreatHunting.Read.Allhunt run,library run,investigate,schemaMicrosoft Graph Domain.Read.AllEntra portion of domains listWindowsDefenderATP¹ Machine.Readdevice show,device action-status, hostname lookupsWindowsDefenderATP Machine.Isolatedevice isolate/unisolateWindowsDefenderATP Machine.Scandevice scanWindowsDefenderATP Machine.CollectForensicsdevice collect-packageWindowsDefenderATP Machine.RestrictExecutiondevice restrict,device unrestrictWindowsDefenderATP AdvancedQuery.ReadHunting fallback only (see below) ¹ Under APIs my organization uses, search for WindowsDefenderATP. Defender for Endpoint tokens are still issued for the
api.securitycenter.microsoft.comaudience even though requests go toapi.security.microsoft.com; that is why these live under WindowsDefenderATP rather than Microsoft Graph.The minimum for the quick start above is the first three Graph rows plus
Machine.Read(used byinvestigateto enrich devices). Grant only what the commands you intend to use need.device download-packageand the Active Directory part ofdomains listhave no official API and stay cookie-only.Hunting goes through Microsoft Graph. If Graph hunting returns 403 or 404,
xdrretries the query once against the Defender for Endpoint hunting API (api.security.microsoft.com/api/advancedqueries/run), which needsAdvancedQuery.Readand only sees Defender for Endpoint tables. Microsoft began retiring that API in January 2026, so configure Graph hunting and treat the fallback as temporary. -
Grant admin consent for your tenant. Every permission above is delegated, so a Cloud Application Administrator, Application Administrator, or Privileged Role Administrator can grant it (Microsoft’s requirements). Security Administrator alone cannot. The service-principal setup described under Configuration uses Microsoft Graph application permissions, which need Privileged Role Administrator.
-
Authentication → Advanced settings → Allow public client flows → Yes.
-
Sign in. This opens an interactive sign-in (the WAM broker on Windows, your browser elsewhere); if neither can start, e.g. on a headless host, it prints a device code instead.
tenant_idandclient_idare saved to~/.xdr-cli/config.toml.Terminal window xdr auth login --tenant-id <TENANT_ID> --client-id <CLIENT_ID>xdr auth status # main.backend: official
The signed-in user also needs the Defender roles that match what they run (Security Reader for triage; Active remediation actions for device actions). The app registration cannot grant more than the user has.
If you add a permission after signing in, run xdr auth logout && xdr auth login afterwards. Until then xdr keeps using the cached access token
issued before the change, and the new permission fails with
PERMISSION_MISSING_SCOPE until that token expires (up to about 90 minutes).
Reading results
Section titled “Reading results”Any command that can return a lot of data prints a receipt followed by at
most two preview rows, and saves the complete result as JSONL under
~/.xdr-cli/results/. When there are more rows than previews, the receipt’s
context.results_command is the exact command to page through them:
{"status":"success","schema_version":1,"run_id":"20261004T015710757111Z-41a96c5113e3","data_path":"/home/me/.xdr-cli/results/2026-10-04/20261004T015710757111Z-41a96c5113e3.jsonl","meta_path":"/home/me/.xdr-cli/results/2026-10-04/20261004T015710757111Z-41a96c5113e3.meta.json","rows":2,"server_truncation_state":"unknown","execution_time_ms":1537,"session_id":null,"session_label":null,"session_attachment":"unattached","incident_id":null,"alert_id":null,"context":{"shown":2,"total":2,"has_more":false}}{"id":"2","severity":"high","status":"active","displayName":"Multi-stage incident involving Credential access & Lateral movement on multiple endpoints", …}{"id":"1","severity":"high","status":"active","displayName":"'Ceprolad' detected on one endpoint", …}The preview rows above are abridged; real rows are the full API objects
(previews larger than 4 KB are replaced by a preview_omitted marker).
Work with the artifact using whatever you already use — jq, rg, Python —
or the built-in xdr results commands. incidents show, alerts show, and
investigate split their output into typed rows (record_type of incident,
alert, evidence, entity, …), so one kind can be selected directly:
xdr incidents show 42 --expand alerts # note the receipt's data_pathjq -r 'select(.record_type=="alert") | "\(.severity)\t\(.title)"' "$DATA_PATH"xdr results shape <run-id> # which fields exist, with types and countsxdr results rows <run-id> --type evidencexdr results query <run-id> # the KQL behind a hunt or library runArtifacts are never deleted automatically. xdr results prune --older-than 30 --yes
removes eligible old results and retires automatic discovery evidence for them;
explicit observation and proposal evidence remains protected. See
evidence retention.
Response actions
Section titled “Response actions”xdr device show <device-id>xdr device isolate <device-id> --comment "Incident 42" --dry-run # previewxdr device isolate <device-id> --comment "Incident 42" --yes # do itxdr device unisolate <device-id> --comment "Remediated" --yesxdr device scan <device-id> --scan-type Fullxdr device restrict <device-id> --comment "Suspicious activity" --yesxdr device unrestrict <device-id> --comment "Recovery approved" --yesxdr device collect-package <device-id>xdr device action-status <action-id>xdr device download-package <action-id> --device <device-id> --output pkg.zip # cookie backendisolate, unisolate, restrict, and unrestrict require --comment.
Every action asks for confirmation unless you pass --yes, and refuses to run
non-interactively without it; declining the prompt exits 13. --dry-run is
available on all actions except unisolate. In cookie mode, device IDs must
be the 40-hex MachineId (or a hostname with exactly one match) and action IDs
must be GUIDs; action-status needs --device the first time it sees an
action on this machine.
For AI agents
Section titled “For AI agents”xdr is designed to be driven by an agent. Point the agent at
AGENTS.md — it covers invocation rules, output shapes, exit
codes, and the investigation methodology in
docs/investigation.md and playbooks/.
The short version:
- Large reads emit JSON receipts and previews; progress and warnings go to
stderr. Lifecycle and raw-render commands have their own output shapes
documented in AGENTS.md.
Never merge streams with
2>&1. - Progress is auto-silenced when stdout is piped;
--quietsuppresses progress,--no-quietforces it on. Configuration warnings can still appear on stderr. xdrprompts only when both stdin and stdout are TTYs; otherwise (or with--no-interactive) it never prompts. Commands that would ask for confirmation (response actions,incidents update,results prune) refuse with exit 6 unless--yesis passed.auth loginandauth portal-cookieare interactive flows for a human; agents should ask rather than treat--no-interactiveas unattended sign-in.- Failures before durable output are one JSON error line with a stable
codeandexit_code; usage errors may include acorrected_argvhint. Operations the selected backend cannot perform returnBACKEND_CAPABILITY_UNAVAILABLEand never fall back to the other backend. - Explicit
session endemits a closure record with feedback instructions, then a maintenance record. Upkeep can return exit 14 after the session is safely closed; cancellation returns 130. Do not retry the session end. - Exit codes: 0 ok · 1 internal · 2 auth · 3 upstream API · 4 config · 5 query · 6 usage · 7 permission · 8 not found · 9 rate-limited · 10 timeout · 11 network · 12 artifact I/O · 13 conflict · 14 partial success · 130 cancelled.
# In a Claude Code / Copilot CLI / Codex prompt:"Use xdr-cli to investigate incident 4421: run `xdr investigate --auto-enrich 4421`,read the receipt's data_path, and summarise the alerts, entities, and recommendedactions. Do not run any `xdr device` command without asking me."Optional: sessions and the improvement loop
Section titled “Optional: sessions and the improvement loop”Every investigation can leave a structured record of how the tool performed,
so gaps show up as data rather than anecdotes. The record stays on your
machine under ~/.xdr-cli/sessions/; nothing is sent anywhere.
- Sessions record every command.
hunt run,library run,investigate,incidents show,alerts show,schema observeandschema candidate-reviewstart a session automatically when none is live (or start one yourself withxdr session start --label incident-42). Each invocation becomes one JSONL record with the command and redacted arguments, the KQL and tables it touched, the library query and parameters, the exit and error code, duration, and row count. Automatic sessions expire after 30 minutes of inactivity; you never have to end one. --rationalecaptures intent before the result.xdr --rationale "expect RDP from WS-01 to the DC" hunt run "…"stores the hypothesis on that command’s record, so a review can compare what was expected with what came back.- Learning mode captures the lesson after. In a session started with
xdr session start --learning-mode, each command must be followed byxdr annotate "<what this showed>", orxdr annotate --skip "<why it wasn't useful>", before the next one runs. A skip is recorded as signal too. - Feedback closes each session.
xdr session endreturns anext_actionasking for an assessment, whichxdr session feedbackappends with an outcome (completed-smoothly,completed-with-friction,incomplete-blocked) and friction categories (output-handling,query-or-schema,library-discovery,auth-or-permission,latency-or-timeout, …). The agent’s assessment and the analyst’s are separate entries (--source agent/--source analyst). Entries are append-only and never overwritten, and agents are told never to invent analyst feedback. xdr history statsturns the record into metrics: failure rate and top error codes, hand-written versus library hunts, and table coverage gaps (tables queried with ad-hoc KQL that a library query already covers). Scope it to one--sessionor to all of an--operator’s sessions, narrow with--incident,--command, or--since, and use--by-actorto separate parallel agents (XDR_ACTOR).
An explicit xdr session end also runs bounded schema upkeep against the
tenant (see the next section) — up to 90 seconds by default. Pass
--no-maintenance to skip it once, or set schema_collect_on_session_end = false to keep session end offline. Automatic expiry never runs upkeep.
xdr session start --learning-mode --label incident-42xdr --rationale "token replay from a new ASN" library run ttp_token_theft_replayxdr annotate "two sign-ins from one ASN; both were the user's VPN"xdr session endxdr session feedback <session-id> --source agent --outcome completed-with-friction \ --category library-discovery --comment "needed three searches to find the replay query"xdr history stats --operator <initials> --since 30d # initials prefix your session IDsDetails: docs/sessions.md.
Optional: schema graph
Section titled “Optional: schema graph”You can work incidents without ever running xdr schema. The schema graph is
for the moment you hold an identifier — a DeviceId, an account, a SHA-256 —
and want to know which other tables and differently named fields carry it:
xdr schema pivot DeviceNetworkEvents.DeviceIdxdr schema path DeviceNetworkEvents DeviceProcessEventsIt starts from a reviewed graph shipped with the tool and grows from your own
hunts: xdr schema collect mines the results you have already saved, recovers
field origins from their KQL, and validates promising pairs with a few
targeted queries; --explore searches for saved identifiers in other tables.
Shared values establish correlation evidence, never join safety.
xdr schema status # cache-only; prints the exact next_commandxdr schema collect --local-only # mine saved results without tenant queriesxdr schema collect --plan-only # preview focused validationxdr schema collect # validate promising field pairsxdr schema collect --explore # discover additional identifier locationsxdr schema export-opengraph graph.json --include-tenant # BloodHound importSee the full guide for evidence thresholds,
query budgets, what session end does, BloodHound export, and portable bundles
(xdr schema bundle export / xdr schema bundle inspect /
xdr schema bundle import).
Command reference
Section titled “Command reference”| Command | Description |
|---|---|
xdr auth login / status / logout | Interactive sign-in for the official backend (device-code fallback), status for both backends, clear the selected backend’s credentials |
xdr auth portal-cookie SOURCE / portal-logout | Import (--verify, --keep-source) or remove a portal-session cookie — see docs/portal_cookie.md |
xdr incidents list | --since, --severity, --status, --assigned-to, --limit |
xdr incidents show ID [--expand alerts] / update ID | View (alerts include their evidence); update --status, --classification, --determination, --comment (--dry-run, --yes) |
xdr alerts list / show ID | --since, --severity, --service, --limit |
xdr investigate ID [--auto-enrich] | Guided investigation; without --auto-enrich it asks which queries to run (all, when non-interactive) |
xdr hunt run KQL | Ad-hoc advanced hunting (--from-file, --from-stdin, --timeout, --raw) |
xdr hunt library-show NAME -p k=v | Render a library query’s resolved KQL without running it |
xdr library list / show NAME / run NAME | Browse (--search, --tier) and run library queries (-p key=value, repeatable; --timeout, --raw) |
xdr lists init | Seed ~/.xdr-cli/lists/ reference data used by library queries (--force --yes to overwrite) |
xdr device show / isolate / unisolate / scan / restrict / unrestrict / collect-package / action-status | Device details and response actions |
xdr device download-package ACTION --device ID --output PATH | Download a completed investigation ZIP (cookie backend; --force, --max-bytes) |
xdr device timeline DEVICE | Download a device’s portal timeline (unofficial API) — docs/device_timeline.md |
xdr domains list [--source all|entra|active-directory] | Source-labelled Entra and observed AD domains (AD is cookie-only) |
xdr results list / show / head / rows / query / shape / prune | Browse and manage local result artifacts (rows --type/--offset/--limit) |
xdr schema status / xdr schema diagnostics | Cache-only state of the schema cache and semantic graph; status prints the exact next_command |
xdr schema collect [--local-only | --plan-only | --explore] | Mine saved results, validate focused pairs, or discover additional identifier locations; rerun to continue |
xdr schema pivot FIELD / xdr schema path A B / xdr schema discoveries | Explain reviewed and empirical identifier routes |
xdr schema refresh / tables / show TABLE | Refresh and query the physical table/column cache (--search) |
xdr schema observe / candidate-review / candidate-proposal | Explicit identifier probes, private evidence review, non-promoting core proposals |
xdr schema export-opengraph PATH | Export the graph for BloodHound (--include-tenant, --include-candidates, --custom-nodes) |
xdr schema bundle export / xdr schema bundle inspect / xdr schema bundle import | Move schema state between machines as a content-bound archive |
xdr schema repair-overlay / xdr schema migrate-cache / prune-evidence / correlate / validate-core | Local repair, evidence retirement, offline correlation, packaged-graph validation |
xdr session start / end / resume / list / show / feedback | Sessions, learning mode, append-only feedback — see docs/sessions.md |
xdr history [stats], xdr annotate | Browse recorded invocations, aggregate failure and coverage metrics, record a lesson |
Global options: --backend auto|official|portal-cookie, --quiet/-q,
--no-quiet, --no-interactive, --debug, --rationale TEXT (record intent
on the session log), --version/-v. Every command has --help.
Configuration
Section titled “Configuration”~/.xdr-cli/config.toml (created by xdr auth login, or by hand for the
cookie quick start; set XDR_CLI_HOME to relocate the whole directory, e.g.
one per tenant):
tenant_id = "…" # required for both backendsclient_id = "…" # official backend onlyauth_mode = "device_code" # or "client_credentials" (see below)client_secret = ""api_backend = "auto" # or "official" / "portal-cookie" to pin oneapi_timeout = 120 # seconds; raise for heavy huntsdefault_limit = 25 # incidents/alerts list when --limit is omittedsession_timeout_seconds = 1800 # automatic-session inactivity timeoutschema_stale_seconds = 86400 # cached schema is visibly stale after this ageschema_collection_stale_seconds = 604800 # nonblocking semantic-collection reminderschema_collect_on_session_end = true # master switch for session-end schema upkeepschema_refresh_on_session_end = true # refresh only if physical cache is missing/staleschema_explore_on_session_end = true # discover new locations after focused validationschema_explore_max_queries = 5 # exploration queries per explicit session end (1-1000)schema_maintenance_timeout_seconds = 90 # overall foreground upkeep deadline; maximum 3600Unknown keys produce a warning on stderr.
| Path | Purpose |
|---|---|
~/.xdr-cli/ | Config directory (0700 on POSIX) |
~/.xdr-cli/config.toml | Configuration (0600) |
~/.xdr-cli/token_cache.json | MSAL token cache for the official backend (0600) |
~/.xdr-cli/portal_cookies.json | Imported portal session cookie (0600) |
~/.xdr-cli/action_associations/ | Action ID → device ID pairs (IDs only) learned in cookie mode |
~/.xdr-cli/audit.log | Local log of attempted state-changing commands (response actions, incident updates, auth changes, lists init, schema repair/import) and investigate runs. Written with redacted argv when the command is dispatched, before it runs, so --dry-run and declined attempts appear too; --help and argument errors do not, and outcomes are not recorded (0600) |
~/.xdr-cli/lists/*.txt | Reference lists for library queries |
~/.xdr-cli/queries/*.kql | Your own library queries (see docs/library.md) |
~/.xdr-cli/results/YYYY-MM-DD/ | Result artifacts and metadata |
~/.xdr-cli/schema/, sessions/ | Schema cache; session histories |
Environment: XDR_CLI_HOME (config directory), XDR_SESSION (attach to a
session), XDR_ACTOR (name parallel actors in one session),
MDE_REFRESH_TOKEN (device timeline on the official backend, CI use).
Service-principal auth. Set auth_mode = "client_credentials" and
client_secret in config.toml, and give the app registration
application permissions with admin consent. The application names match the
table above except Machine.Read.All (for Machine.Read) and
AdvancedQuery.Read.All (for AdvancedQuery.Read). Tokens are acquired
automatically; xdr auth login is not needed. xdr auth status reports
main.authenticated from the cached delegated account, not from the
service-principal credentials, so it can be false while app credentials work.
Troubleshooting
Section titled “Troubleshooting”xdr auth status shows main.configured: false — on the official
backend, run xdr auth login --tenant-id <ID> --client-id <ID> once; the
values are saved. If you meant to use a cookie, check tenant_id is in
config.toml and import the cookie with xdr auth portal-cookie.
Exit 2: NOT_AUTHENTICATED / AUTH_LOGIN_REQUIRED — official backend:
xdr auth login again. Cookie backend: the session expired; capture and
import a fresh cookie.
PERMISSION_MISSING_SCOPE (exit 7) or AADSTS65001 — a permission in
the table above is missing or not consented. In API permissions, every row
must show Granted for <tenant>. Remember there are two token audiences
(Microsoft Graph and WindowsDefenderATP); consent covers both only if both
sets of permissions are present.
BACKEND_CAPABILITY_UNAVAILABLE (exit 3) — the selected backend cannot
do this (for example domains list --source active-directory on the official
backend). Nothing was sent; switch with --backend or import a cookie.
download-package on the official backend is the one exception: it is a
usage error (CLI_USAGE_ERROR, exit 6) with the same remedy.
AADSTS50011 / redirect URI mismatch — add http://localhost under
Authentication → Public client/native.
Sign-in never completes — enable Allow public client flows on the
registration. On Windows, also check the broker redirect URI from setup
step 1. If no browser or broker can start (for example over SSH), xdr
prints a device code instead; enter it in any browser.
API_TIMEOUT (exit 10) on a hunt that works in the portal — raise the
per-call timeout: xdr library run <name> --timeout 240, or set
api_timeout = 240 in config.toml.
--param only kept the first value — -p is repeatable, not
comma-separated: -p account_upn=alice@corp.com -p mode=detail.
More in docs/troubleshooting.md.
Documentation
Section titled “Documentation”| AGENTS.md | How an AI agent should drive xdr |
| docs/portal_cookie.md | The portal-cookie backend: capture, limits, selection rules, errors |
| docs/investigation.md | Investigation methodology and KQL-writing guidance |
| playbooks/ | Alert-specific investigation playbooks |
| docs/library.md | The KQL query library: tiers, every query, parameters, custom queries |
| docs/lists.md | Reference lists (tenant domains, subnets, IOC feeds) used by library queries |
| docs/sessions.md | Investigation sessions and feedback |
| docs/schema_graph.md | Schema graph: discovery, session-end upkeep, BloodHound export, bundles |
| docs/schema_pivots.md | Generated reference of tables, shared fields and reviewed pivots |
| docs/device_timeline.md | Unofficial device-timeline download |
| docs/troubleshooting.md | Error and exit codes, longer troubleshooting reference |
| docs/backend_development.md | Adding an operation to both API backends (contributors) |
| docs/ci.md | What CI checks on every PR |
| CHANGELOG.md | Release notes |
Development
Section titled “Development”git clone https://github.com/NerfBlasters/m365-xdr-cli.gitcd m365-xdr-cliuv sync --locked --extra devuv run --frozen --extra dev pytest tests/ -quv run --frozen --extra dev ruff check src/ tests/pip install -e ".[dev]" in a virtualenv works too.
Contributing and security
Section titled “Contributing and security”Contributions are welcome — read CONTRIBUTING.md
(branching, commits, versioning, AI-agent guidelines, the don’t-commit list)
and the contributing walkthrough first.
Report vulnerabilities privately as described in SECURITY.md,
not in public issues.