Skip to content

Query library

The library is a catalog of packaged Advanced Hunting (KQL) queries that ship inside the CLI. Each entry is one .kql file under src/xdr_cli/queries/ with a short -- key: value front-matter header that declares its name, description, tier, parameters, and the reference lists it consumes. The CLI ships 67 files: 66 hunting queries plus 1 utility (sys_schema_probe). You can add your own (see Custom queries).

TaskCommand
Browse the catalogxdr library list
Filter by keywordxdr library list --search powershell
Filter by tierxdr library list --tier r1
Inspect one entryxdr library show NAME
Render the resolved KQL without running itxdr hunt library-show NAME -p key=value
Run an entryxdr library run NAME -p key=value [-p key=value ...]
Raise the per-call API timeoutxdr library run NAME --timeout 240
Keep JSON-string columns unexpandedxdr library run NAME --raw (ad-hoc KQL: xdr hunt run --raw)
Print the KQL a run actually sentxdr results query <run-id>

Notes:

  • --search is a case-insensitive substring match against the entry name and description. --tier must be one of the tiers present in the catalog; an unknown value is a usage error (exit 6, CLI_USAGE_ERROR) whose allowed field lists the valid tiers.
  • -p/--param is repeatable. Pass one key=value per flag; values are not comma-separated. A parameter name the query does not declare is rejected rather than silently dropped, so a typo such as accont_upn= cannot turn a scoped hunt into a tenant-wide sweep.
  • A required parameter has no default; omitting it fails before any API call. Omit an optional parameter to use its declared default. An explicit empty value still undergoes validation (-p mode= is invalid).
  • hunt library-show validates parameters exactly as library run does, so the KQL it renders is the KQL a run would send.
  • Parameter problems fail with exit 5 and one of these codes: LIBRARY_UNKNOWN_ENTRY (no such query; suggestions lists near matches), LIBRARY_UNKNOWN_PARAM (undeclared name, or an item without =), LIBRARY_MISSING_PARAM (a required parameter was omitted), or LIBRARY_INVALID_PARAM (the value fails its type or format check, including dates, durations, and integers). LIBRARY_UNKNOWN_ENTRY carries a help_command of xdr library list --search NAME; the other three point at xdr library show NAME. Problems in a query file’s own KQL, such as a placeholder placed inside a comment or a verbatim literal, are authoring errors rather than parameter errors and are reported as QUERY_ERROR. Empty scope defaults only mean “match all” when the query implements that condition; declaring a default alone does not change how the KQL filters rows.
  • library list and library run both print a JSON receipt first; the full rows live in the JSONL file named by the receipt’s data_path, and xdr results rows <run-id> pages through them.
  • library show NAME returns the descriptor only: parameters with their type, format, allowed values and defaults, the tables and declared output fields, consumed lists, required permissions, a cost hint, and example invocations. It does not dump the KQL body; xdr hunt library-show NAME -p key=value renders the substituted query, and xdr results query after a run for that.

Every .kql file must declare -- tier: with one of these eight values, enforced by the loader (_VALID_TIERS in src/xdr_cli/queries/__init__.py):

r1, r2, r3, n, beta, pivot, utility, deprecated

What is documented about them comes from the loader and from the methodology contract tests in tests/test_queries_methodology.py:

TierDocumented contract
r1, r2, n, beta“Finding” tiers. Each query emits a Severity = case(...) band and projects SchemaVersion. r1, r2 and n must also expose a time anchor (hours=N or start=/end=).
r3Projects SchemaVersion; no Severity band required. In the shipped catalog the r3 entries are entity-scoped queries, most anchored with start=/end= and defaulting to mode=detail (the two identity_signin_* summaries are the exceptions).
pivotProjects SchemaVersion; exempt from the Severity requirement. In the shipped catalog these are entity-scoped lookups (by message ID, hash, app ID, device, remote, …) that default to mode=detail.
utilityExempt from all methodology checks. Currently only sys_schema_probe.
deprecatedAn alias shim (no shipped query currently uses it). The file must also declare -- alias_of: <target>; library run loads the target’s body instead, accepts and validates the target’s parameters, and prints a stderr warning. library show and library list keep the alias’s name and description but report the target’s parameters, tables, and cost hint.
betaTreated as a finding tier by the tests; the shipped entries prefix their description with (beta).

There is no documented definition of what distinguishes r1 from r2 from r3, or what n stands for, anywhere in docs/, AGENTS.md or the README. The table above records only the behaviour the tests enforce; do not read more into the labels than that.

Every non-utility, non-deprecated query also declares a mode parameter with allowed values summary and detail. Finding-tier queries default to summary (one scored row per entity with Severity, Score, TopEvidence and ListHealth); pivot queries and most r3 queries default to detail (raw matching rows). Check the Parameters column below for each entry’s actual default.

Generated from the library list artifact. * marks a required parameter; =value shows the declared default. Descriptions are shown as the query declares them.

QueryTierDescriptionParameters
dns_subdomain_diversityr1High unique-subdomain count per parent domain — DNS tunneling / data exfil indicator with burst-hour concentration scoring and tenant-data-as-listhours =24, device_name, mode =summary

identity_ — Entra ID and on-prem identity

Section titled “identity_ — Entra ID and on-prem identity”
QueryTierDescriptionParameters
identity_brute_force_summaryr2On-prem brute-force summary by source IP — Severity-banded, attempt/success/target countsip, start, end*, account_upn, mode =summary
identity_cloud_spray_summaryr2Cloud password-spray scope by attacker IP — Severity-banded, error-code distribution, suppression list awareip, start, end*, mode =summary
identity_ip_blast_radiusr3Find all users who successfully signed in from a specific IP — identifies lateral targeting or shared attacker infrastructureip, start, end*, mode =detail
identity_onprem_logon_activityr3On-prem Kerberos/NTLM logon detail for an account or source IP — used for Golden Ticket, pass-the-ticket, and lateral movement investigationaccount_oid, start, end*, mode =detail
identity_signin_baseliner3Per-user sign-in baseline — top IPs / UAs / Countries / ASNs by frequency, distinct-counts, last-seenaccount_oid*, hours =720, mode =summary
identity_signin_contextr3Per-user sign-in history with risk / CA / token / device fieldsaccount_oid, start, end*, mode =detail
identity_signin_ip_summaryr3Per-user sign-ins by IP/Country/City with external-source flaggingaccount_oid, start, end*, mode =summary
QueryTierDescriptionParameters
qry_app_data_accesspivotFile downloads, access, and uploads by an OAuth app — data exfiltration detectionapp_name, start, end*, mode =detail
qry_connection_scopepivotAll devices connecting to a specific remote IP or domain — scopes C2 infrastructure or phishing campaign blast radiusremote, start, end*, mode =detail
qry_delivery_vectorpivotHow a file arrived on a device — download URL, origin IP, parent process. Traces the delivery vector for malware.device_id, sha256, start, end, mode =detail
qry_device_connectionspivotProcess on a device connecting to a specific remote IP or domain — identifies C2 communication, phishing proxy connections, or post-click endpoint activitydevice_id, remote, start, end, mode =detail
qry_device_logonspivotLogon events on a specific device or from a specific remote IP — identifies who accessed a compromised machinedevice_name, start, end*, mode =detail
qry_email_attachmentspivotAttachment details (name, size, hash) for a specific email by NetworkMessageIdnetwork_message_id*, mode =detail
qry_email_blast_radiuspivotOther recipients of the same email by sender and subject — assesses campaign scope and ZAP success ratesender, subject, start, end, mode =detail
qry_email_deliverypivotDelivery status of a specific email by NetworkMessageId — determines if ZAP succeeded or the threat reached the inboxnetwork_message_id*, mode =detail
qry_email_outbound_detailpivotIndividual outbound emails for a sender — examines content patterns, recipients, and delivery statussender, start, end*, mode =detail
qry_email_outbound_spiker2Hourly outbound email volume for a sender — rate-of-change Severity band against 7-day baselinesender, start, end*, mode =summary
qry_email_url_blast_radiuspivotRecipients who received email containing a specific URL or domain — scopes phishing campaign reachurl_domain, start, end*, mode =detail
qry_email_urlspivotEmbedded URLs in a specific email by NetworkMessageIdnetwork_message_id*, mode =detail
qry_entra_role_changesr1Entra role and group membership changes — actor/target scope, privileged-role weighting, new-IP-for-actor (30d), self-add and rapid add+revoke detectionhours =72, actor_upn, target_upn, mode =summary
qry_exchange_role_changesr1Exchange Online role assignments — actor scope, privileged-role weighting, new-IP-for-actor (30d), external-source detection, add+revoke pairshours =72, actor_upn, mode =summary
qry_external_sharingpivotExternal sharing recipients and anonymous links created by an account — data exfiltration via sharingaccount_oid, start, end*, mode =detail
qry_file_access_detailpivotSpecific files downloaded or synced by an account — identifies targeted access to sensitive contentaccount_oid, start, end*, mode =detail
qry_file_hash_scopepivotFind all devices with a specific file hashsha256*, hours =720, mode =detail
qry_inbox_rule_activityr1Exchange rule changes (inbox + transport + mailbox forwarding) with extracted predicates, forwarding-destination classification, and BEC-specific scoringhours =168, account_upn, mode =summary
qry_mailbox_delegationnExchange mailbox delegation grants — FullAccess / SendAs / SendOnBehalf, with internal/external delegate classificationhours =168, account_upn, mode =summary
qry_oauth_app_infopivotOAuth app registration details — app name, service principal ID, and owner tenant (first-party vs third-party)app_id*, mode =detail
qry_oauth_consentpivotWho consented to an OAuth app and from where — identifies suspicious or coerced consent eventsapp_id, start, end*, mode =detail
qry_oauth_credentialspivotCredential additions (secrets, certificates) to an OAuth app — persistence mechanism detectionapp_id, start, end*, mode =detail
qry_post_compromise_activityr2Cloud actions taken by an account from a given IP after compromise — ActionType-taxonomy scoredaccount_oid, ip, start, end, mode =summary
qry_privilege_grant_revoker1Add-then-revoke role correlation across Entra ID and Exchange Online — short-gap pairs, privileged-role weighting, new-IP-for-actor, self-grant detectionhours =72, actor_upn, target_upn, mode =summary
qry_process_treepivotFull process ancestry for a device in a time windowdevice_name*, hours =1, mode =detail
qry_spn_activityr2Per-SPN sign-in activity — per-day rollup with multi-country Severity annotationspn_id, start, end*, mode =summary
qry_url_clickspivotSafe Links click activity — by NetworkMessageId, user UPN, or URL domain. Shows whether users clicked through warnings.account_upn, start, end*, mode =detail
QueryTierDescriptionParameters
sys_schema_probeutilityEnumerate tables/columns available in this tenant (run first in a new environment)—
QueryTierDescriptionParameters
ttp_ad_directory_changesr3On-prem AD group membership changes, account modifications, and computer account creation by a specific accountaccount_oid, start, end*, mode =detail
ttp_ad_recon_queriesr3LDAP/SAMR reconnaissance activity — AD queries by a specific account or source IPaccount_oid, start, end*, mode =detail
ttp_adcs_abusebeta(beta) ADCS abuse signals — template enrollment by non-admin, SAN/requester mismatch, ESC8 NTLM-relay artefactshours =72, account_upn, mode =summary
ttp_conditional_access_tampernConditional Access policy add/update/disable/delete (CloudAppEvents) correlated with actor sign-in context (new IP vs 30d baseline, off-hours, RiskLevel/anonymizing IP within 30m) — Storm-1167 / Tycoon2FA admin-token tamper patternhours =72, actor_upn, mode =summary
ttp_credential_dumpingnT1003 LSASS credential dumping — comsvcs.dll MiniDump, procdump -ma lsass, rundll32 MiniDumpW, mimikatz / sekurlsa / lsadump cmdline literals, nanodump / dumpert / pypykatz / SilentTrinity, taskmgr -d, sqldumper LOLBin, plus DeviceEvents OpenProcess/ReadProcessMemory against lsass.exe with signer-allowlist and KnownGoodSigners suppressionhours =24, device_id, account_upn, mode =summary
ttp_defender_av_tamperingnDefender AV / Sense tampering — disable, exclusion, definition removal, service stop, tamper attempts, registry-policy pokehours =24, device_name, mode =summary
ttp_device_code_flow_abusebeta(beta) Device-code flow abuse — net-new location, off-hours completion, rapid multi-completionhours =24, account_upn, mode =summary
ttp_discovery_reconnAD / share enumeration — BloodHound, SharpHound, PowerView, net/nltest, LDAP-burst — correlated by devicehours =24, device_name, account_upn, mode =summary
ttp_dll_sideloadingr2DLL loaded from the same user-writable directory as the loading process — sideloading / hijack indicator with hijack-target awareness and signer suppressionhours =24, device_name, account_upn, mode =summary
ttp_dns_beaconingr1Periodic DNS queries with low inter-arrival jitter (CV) — C2 beaconing detector with tenant-data-as-list, 30-day novelty, off-hours biashours =24, device_name, mode =summary
ttp_encoded_powershellr1Base64-encoded PowerShell with decoded payload (single + double-decode), AMSI/ETW bypass detection, format-string + char-array obfuscation heuristics, parent attributionhours =24, device_name, account_upn, mode =summary
ttp_facedancer_webview2r2FaceDancer / DLL proxy sideloading — rename + replace correlation in user-writable paths, WebView2 target awarenesshours =24, device_name, mode =summary
ttp_file_operation_volumer3File operation volume by type for an account — quantifies download, upload, share, and access activityaccount_oid, start, end*, mode =detail
ttp_impossible_travelr1Geographically improbable consecutive sign-ins via geo_distance + time delta + implied velocity, additive scoring with MFA / internal / known-egress suppressionhours =24, account_upn, mode =summary
ttp_internal_lateral_connectionsr3Connections from a device to private IPs on non-standard ports — lateral movement indicatordevice_id, start, end*, mode =detail
ttp_kerberos_delegation_abusebeta(beta) Kerberos delegation abuse — RBCD self-grant, S4U2Self/S4U2Proxy chains, sensitive-SPN TGS by low-priv accountshours =72, account_upn, mode =summary
ttp_lateral_movement_rdpr1Successful RDP logons (LogonType=RemoteInteractive) with novelty, fan-out, failed-then-success scoring, tunnel detection, off-hours, and KnownRemoteSupportTools suppressionhours =6, source_account, target_device, source_ip, mode =summary
ttp_lateral_psexec_wmir1Lateral movement via PsExec / WMI / PSRemoting / remote schtasks — cross-device fan-out, unsigned tooling, suspicious cmdline, suppression-list awarehours =24, device_id, account_upn, mode =summary
ttp_lateral_targetsr3Summarize successful on-prem authentication destinations for an account — identifies lateral movement targetsaccount_oid, start, end*, mode =detail
ttp_ldap_process_attributionr2Correlate MDI LDAP recon with initiating process on source device — Severity-banded for triagedevice_name, start, end*, lookback =0d, mode =summary
ttp_malware_executionr3Check if a specific file (by hash or name) executed on a device — determines prevented vs. executed verdictdevice_id, sha256, start, end, mode =detail
ttp_new_service_creationr1New service registrations (ImagePath / ServiceDll / FailureCommand) scored by parent process, binary path, signer, and 30-day rarityhours =24, device_name, account_upn, mode =summary
ttp_oauth_app_signin_anomalynSPN sign-in anomalies — net-new IPs/countries, token reuse across IPs, public-client workload sign-inshours =24, service_principal_id, mode =summary
ttp_oauth_consent_anomalynBroad-scan OAuth consent grants scored by requested scope, app age, consent-burst per user, per-IP cross-user burst, same-session register-then-consent, admin-consenthours =168, account_upn, application_id, mode =summary
ttp_ransomware_mass-renamer1Late-stage ransomware activity — mass rename + extension-entropy + shadow copy deletion + backup deletion + ransom note creation. For early-warning, see ttp_ransomware_precursors.hours =6, threshold =50, device_name, mode =summary
ttp_ransomware_precursorsnPre-deployment ransomware kill-chain — recon, AV tamper, backup kill, operator tooling, DA acquisitionhours =72, device_name, mode =summary
ttp_registry_persistencer3Run/RunOnce/Services registry modifications on a device — persistence mechanism detectiondevice_id, start, end*, mode =detail
ttp_rmm_first_seennFirst-seen RMM tool (AnyDesk, ScreenConnect, ConnectWise Control, NinjaRMM, Atera, Splashtop, TeamViewer, etc.) on a device — process or file-create with no occurrence in the prior 30d. Highest-fidelity initial-access signal in the SMB threat model (Akira, BlackBasta, Storm-0867, ScatteredSpider).hours =24, device_id, mode =summary
ttp_shadow_copy_deletionr3Detect shadow copy deletion and recovery mode tampering — a hallmark of ransomware pre-encryption stagingdevice_id, start, end*, mode =detail
ttp_suspicious_downloads_execr2Executables / scripts run from user-writable paths with at least one behavioural indicator — multi-signal scoredhours =24, device_name, account_upn, mode =summary
ttp_token_theft_replayr1Per-session sign-in fingerprint anomalies (SessionId / UniqueTokenId reuse from different IPs / UAs / Countries / JA4) + AiTM infra hits + refresh-token replay (>24h, no-MFA)hours =24, account_upn, session_id, mode =summary

Parameters are not pasted into the KQL as raw text. The loader assigns a catalog-wide type to each parameter by name (_parameter_contract), then validates the value and renders the {name} placeholder according to its KQL context (_render_parameters).

TypeParameter namesAccepted valuesPlacement in KQL
enummodesummary or detailinside a quoted literal
integerhours, thresholdpositive integer (1, 24, 720)outside quotes
datetimestart, endISO-8601 date or timestamp (2026-10-01, 2026-10-01T08:00:00Z)outside quotes
durationlookbackKQL duration literal (30m, 6h, 7d)outside quotes
stringeverything else (account_upn, device_name, sha256, ip, *_id, …)any text without control characters; sha256 must be exactly 64 hex characters and ip/source_ip a valid IPv4 or IPv6 addressinside a quoted literal

Rules the renderer enforces:

  • A string or enum placeholder must sit inside an ordinary single- or double-quoted KQL string literal, for example DeviceName == "{device_name}". The value is escaped as literal content: backslashes are doubled and the enclosing quote character is backslash-escaped. A value containing quotes, |, or other operator characters therefore stays data and cannot change the query’s shape.
  • A typed placeholder ({hours}, {start}, {end}, {lookback}, {threshold}) must stay outside quotes, for example ago({hours}h) or between (datetime({start}) .. datetime({end})). The value is validated against its literal format before substitution, so -p hours=24h or -p start=yesterday is rejected.
  • Placeholders are not substituted inside // or /* */ comments, are refused inside verbatim (@"...") literals, and are refused inside multiline (```) literals.
  • A placeholder that appears in the KQL but is never substituted (for example, one that occurs only inside a comment) fails the run. A declared parameter whose placeholder does not appear in the body at all is accepted and has no effect.

Drop .kql files into ~/.xdr-cli/queries/ (or $XDR_CLI_HOME/queries/ when you isolate configuration per tenant). The file stem becomes the query name and a user file with the same stem as a builtin overrides it. Front-matter is a run of -- key: value lines at the top of the file; the first non--- line starts the KQL body.

KeyRequiredMeaning
-- tier:yesOne of the eight tier values above.
-- description:recommendedOne line shown by library list and library show.
-- params:if the body has placeholdersComma-separated names; name is required, name= declares an empty default, name=value declares a default.
-- lists:noComma-separated list-block names the body references as _xdr_<Name>.
-- alias_of:only with tier: deprecatedName of the live query to forward to.
-- agent_hint:noMulti-line hint (continuation lines start with -- and two spaces).
-- name:noAccepted but ignored; the query name always comes from the file stem.

Complete example, ~/.xdr-cli/queries/my_device_processes.kql:

-- description: My custom query
-- tier: r3
-- params: device_name, hours=24
DeviceProcessEvents
| where Timestamp > ago({hours}h)
| where DeviceName == "{device_name}"
| take 100

Run it with xdr library run my_device_processes -p device_name=WS-01. Because device_name has no default it is required. To make it optional, use -- params: device_name=, hours=24 and change its filter to where isempty("{device_name}") or DeviceName == "{device_name}" if an empty value should mean “match all”. The hours parameter controls the explicit Timestamp filter above; declaring it alone would not bound the query.

A malformed file (missing -- tier:, an unknown tier, or tier: deprecated without alias_of) is skipped with a stderr warning rather than aborting the whole command, so one bad file never hides the rest of the library. A skipped file with a new name does not appear in library list; a skipped file that shares a builtin’s name leaves the builtin in effect, so check stderr if your override seems to be ignored.

Many queries consume tenant and IOC reference data through list blocks: let _xdr_<ListName> = dynamic([...]); declarations that the loader synthesises from ~/.xdr-cli/lists/<ListName>.txt and prepends to the body before parameter substitution. Seed the directory once with xdr lists init. A list that is missing, empty, over its size cap, or not a known block name resolves to an empty dynamic([]) and the query still runs, so an unseeded allowlist admits every row and an unseeded denylist matches nothing: results silently lose coverage rather than failing. Summary rows carry a ListHealth column that reports each block’s value count, staleness, and source so you can spot this. See lists.md for the file format, seeded blocks, and TTL headers.

Every library run stores the fully rendered KQL (lists prepended, parameters substituted and escaped) alongside its result. Print it with:

Terminal window
xdr results query <run-id>

The run ID is in the receipt that library run prints. The output is the exact text sent to the Advanced Hunting API, suitable for pasting into the Defender portal or attaching to a case.