Changelog
All notable changes to xdr-cli are documented here. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
This repository begins with a fresh source snapshot of 0.9.0. Earlier
changelog entries are retained as release notes; their commits and referenced
issues or pull requests are not part of this repository. Historical CI entries
may describe tooling that has since been replaced; see CI security
for the current checks.
[0.15.1] - 2026-10-04
Section titled “[0.15.1] - 2026-10-04”- xdr-cli.com landing page and searchable documentation built from the existing README, guides, and playbooks, with responsive light/dark themes and a user-controlled investigation demo.
- GitHub Pages build and deployment workflow, with content publication checks, internal-link validation, browser accessibility checks, and website dependency updates. Publication follows a successful main-branch CI run.
[0.15.0] - 2026-10-04
Section titled “[0.15.0] - 2026-10-04”Changed
Section titled “Changed”- Replace Typer with Click, using public command callbacks for dispatch recording and backend checks. Preserve parameter bindings, JSON errors, exit codes, and session recording.
- Render help with Click’s plain formatter and positional-argument descriptions. Preserve tables, examples, accepted enum defaults, and full group summaries. Rich remains a direct dependency for styled stderr messages.
- Remove duplicate typo-hint wording formerly added by Typer; structured suggestions and error codes remain unchanged.
Removed
Section titled “Removed”- Root
--install-completionand--show-completionflags. Generate shell completion through Click’s_XDR_COMPLETEenvironment variable instead. - Typer, Shellingham, and annotated-doc dependencies, eliminating the deprecated stream-helper imports during startup and tests.
[0.14.0] - 2026-10-04
Section titled “[0.14.0] - 2026-10-04”Second documentation pass for a reader arriving fresh at the public repository, and removal of the compatibility aliases, stubs and removed-option hints carried over from before 0.9.0.
Removed
Section titled “Removed”xdr hunt libraryandxdr hunt library-run, the aliases forxdr library listandxdr library run.xdr hunt library-showstays as the resolved-KQL renderer.xdr schema candidates, the alias forxdr schema discoveries.xdr auth portal-loginand the portal OAuth token cache (portal_token_cache.json) behind it. Portal access is cookie-based andauth statusno longer reportsmsal_cached.auth portal-logout(and cookie-modeauth logout) still delete aportal_token_cache.jsonleft by an earlier release, so one logout cleans up an upgraded installation.- The
CLI_REMOVED_OPTIONshims for--fields,--jq, hunt--limit, andschema collect --resume/--exhaustive. These are ordinary unknown options now. - The hidden, always-rejected
schema collect --max-targets. - The deprecated query aliases
qry_inbox_rule_auditandqry_inbox_rule_triggers; useqry_inbox_rule_activity. Thedeprecatedtier andalias_ofmechanism remain for future deprecations.
Changed
Section titled “Changed”xdr history statscounts onlylibrary runrecords in itshunt_library_runratio; the output key name is unchanged.- README leads with the two sign-in paths and presents the portal cookie as the quick start (no app registration or admin consent); the Entra app registration follows as the supported path. The cookie backend’s undocumented-interface and credential caveats now precede its setup steps.
- README states that the schema graph and sessions are optional, what an
explicit
session endcosts, and how to keep it offline. docs/portal_cookie.mdrewritten newcomer-first: capture and import steps, what works in cookie mode and its input limits, backend selection, known gaps, and error codes.- Corrected against source:
unrestrictrequires--comment; exit 2 in cookie mode is fixed by re-importing a cookie, notauth login; theauth statusshape differs by backend;results pruneretires automatic graph evidence;observeacceptsTimeGenerated; the auto-session command set; the audit-log scope; missing run IDs reportRESULT_NOT_FOUND; unknown--tieris a usage error; declined confirmations exit 13. - Documented every
xdr schemaflag that had no mention,--backend,api_backend,action_associations/, and the cookie-backend error codes in the troubleshooting reference. docs/schema_probe.mdfolded intodocs/schema_pivots.md; the browser cookie renewal proposal moved todocs/proposals/.
[0.13.0] - 2026-10-04
Section titled “[0.13.0] - 2026-10-04”Experimental portal-cookie authentication, a shared API backend interface, and the reliability fixes identified during implementation and adversarial review.
- Experimental
portal-cookiebackend using the undocumented Defender portalapiproxyinterface. This is not an officially supported public integration path. Tenant-bound browser cookies support hunting, incident/alert workflows, domains, device reads/actions, and timelines without an app registration. - Automatic backend selection: prefer matching MSAL credentials or configured
app credentials; select cookies when they are the only usable local credentials.
Explicit
--backendand configured preferences override automatic selection. - Combined Entra and MDI-observed Active Directory domain inventory through
domains list, with source labels, source selection, and coverage metadata. device unrestrictfor official and cookie authentication; cookie-based investigation-package archive downloads with private atomic output and no extraction or disclosure of signed download URLs.- Tenant-bound local action/device associations and device enrichment with native source provenance and explicit unavailable-field reporting.
Changed
Section titled “Changed”- Both API backends implement a shared named-operation interface. API helpers delegate through that contract; capability profiles supply backend differences. Official domain and portal Graph reads share continuation URL validation.
domains listsaves artifact receipts and reports partial success when a requested source is unavailable. Use--source entrafor Entra-only reads.auth portal-loginis retired and directs users to cookie import. Browser renewal is documented as a separate follow-up, not included in this release.
- Overlapping portal pages no longer discard incident/alert listings or hostname searches; duplicate IDs are skipped while continuation and page bounds remain.
- Invalid backend configuration preserves diagnostics and reads, but blocks tenant writes without an explicit backend choice. Auth recovery does not silently change the invalid preference into a valid automatic selection.
- Session attribution honors an explicit official-backend override. Auth status diagnoses rejected cookie stores, and portal logout clears legacy portal tokens.
- Optional device lookup timeouts/network failures preserve the primary response; device detail is fetched once, and timeline verification skips enrichment.
- AD source failures distinguish unavailable data from retained partial results.
- Scan/isolation modes accept any letter case and reject invalid values before confirmation. Unknown-outcome portal mutation timeouts are nonretryable.
Full device-field and historical action parity remains limited by observed portal contracts. See portal-cookie support and limits.
[0.12.0] - 2026-10-04
Section titled “[0.12.0] - 2026-10-04”Documentation rewrite for the public launch, plus the fixes found while verifying it against the code and a live tenant.
Changed
Section titled “Changed”- README rewritten as a landing page: what the tool is, who it is for, a
recorded demo, install, Entra app registration, a six-command quick start,
and a complete command reference. Long-form material moved to
docs/. - Entra permissions table verified against a live tenant:
Domain.Read.Alladded forxdr domains list;SecurityAlert.Read.Allinstead ofReadWrite.All(xdrnever modifies alerts); delegatedMachine.Read(sufficient fordevice show, hostname lookups anddevice action-status) instead ofMachine.Read.All/Machine.ReadWrite;AdvancedQuery.Readfor the hunting fallback. Service-principal setup names the differing application permissions. - 403 errors and the
library showdescriptor name the delegated Defender for Endpoint permission and its application equivalent (Machine.Read (delegated) or Machine.Read.All (application)), nameSecurityAlert.Read.Allfor alerts, and nameDomain.Read.Allfordomains list. The descriptor’srequired_permissionsentries gain anapplication_permissionfield. incidents show --expandaccepts onlyalertsand rejects anything else withCLI_INVALID_ENUM(exit 6) before calling Graph. Graph incidents have noevidenceexpansion;--expand evidencepreviously returned HTTP 400. Evidence is already included in each alert.hunt library-showand the deprecatedhunt library-runvalidate parameters exactly aslibrary rundoes. Unknown names, missing required parameters, and invalid values now fail with theLIBRARY_*codes (exit 5) instead of being silently dropped or coerced. Unknown entries reportLIBRARY_UNKNOWN_ENTRYinstead ofQUERY_ERROR.- Deprecated library aliases accept and validate their target’s parameters.
- The audit log is written from the command Click actually dispatches,
instead of a token search over raw argv. Every accepted syntax (
--, clustered short flags,--opt=value) is covered; an argument equal to a command name (xdr hunt run update) is no longer logged;--helpand argument errors, which dispatch nothing, are no longer logged; and--refresh-tokenvalues are redacted as in the session recorder. - The hunting fallback calls Defender for Endpoint’s documented
api/advancedqueries/run, whose delegated permission is theAdvancedQuery.Readthe README asks for, instead of the Defender XDRapi/advancedhunting/run, which Microsoft documents withAdvancedHunting.Readunder a different API. The request and response are unchanged. - Library parameter validation reuses the renderer’s type checks, so invalid
dates, durations, and integers fail with
LIBRARY_INVALID_PARAMand ahelp_commandinstead of a genericQUERY_ERROR. library showandlibrary listdescribe a deprecated alias with its target’s parameters, tables, and cost hint, matching what it runs.- README gains a “Built to improve with use” section describing the local
improvement loop that already exists: automatic session recording,
--rationale, learning mode withxdr annotate, append-only agent and analyst feedback at session end, andxdr history statsfailure and library-coverage metrics. - README now describes
xdr auth loginaccurately (WAM on Windows, browser elsewhere, device code only when neither can start), adds the Windows broker redirect URI to setup, documentsxdr lists init, the--rationaleglobal flag, and the real scope of~/.xdr-cli/audit.log, and lists every exit code. - Troubleshooting references the error codes the CLI actually emits
(
PERMISSION_MISSING_SCOPE, notFORBIDDEN;AUTH_LOGIN_REQUIRED, notTOKEN_EXPIRED) and describesPERMISSION_DENIED,CLI_COMMAND_FAILED, and non-interactive confirmation behaviour correctly. docs/schema_graph.mdgained the bundle guarantees,repair-overlay/migrate-cachebehaviour, and thevalidate-corecommand-map row that previously lived only in the README.- Help text and comments that pointed at removed README sections now point
at
docs/device_timeline.md(xdr auth portal-login --help,xdr device timeline --help, AGENTS.md). - Launch documentation reconciled with 0.11.0: local-first discovery, automatic session-end upkeep, output contracts, and evidence retention.
investigateno longer treats an IP address reported as a device name (Defender for Identity does this for hosts it cannot resolve) as a device: the address is recorded as an IP, and no process-tree hunt or isolation command is suggested for it.- Isolation suggestions for a device without an MDE ID carry a
resolve_command(xdr device show <host>) and print it, instead of a bare<device-id>placeholder. - In learning mode, a command refused by the gate is no longer the target of
the next
xdr annotate. It was recorded as a new unannotated invocation, so annotating cleared the refusal and left the gate closed. Refused attempts are now recorded withlearning_gate_refused: trueand are never pending. - The README’s admin-consent step named Security Administrator, which cannot grant tenant-wide consent. It now names Cloud Application Administrator, Application Administrator, or Privileged Role Administrator.
default_limitinconfig.tomlis now the default forincidents listandalerts list --limit; it was previously ignored.
docs/device_timeline.md: the unofficial device-timeline feature and portal authentication, moved out of the README and corrected.docs/library.md: every packaged KQL query with tier and parameters, the parameter contract and its error codes, and custom-query format.docs/troubleshooting.md: long-form troubleshooting and the exit-code table.docs/media/investigate.gif(with its asciinema source) recorded against a demo tenant, with tenant, object, and local-path identifiers replaced for publication.
Removed
Section titled “Removed”- References in the documentation and library descriptions to releases that predate this repository.
[0.11.0] - 2026-10-04
Section titled “[0.11.0] - 2026-10-04”- Explicit session end refreshes missing/stale physical schema, validates saved
overlaps, and explores new locations in the foreground. Exploration defaults
to
schema_explore_max_queries = 5per end; refresh and exploration have individual switches.schema_collect_on_session_end = falsedisables all stages. Automatic retirement never collects, and maintenance failures leave the session safely closed. - Local-first schema discovery recovers field origins from saved KQL and indexes identifier overlap across differently named fields in private tenant artifacts.
schema collect --local-onlypublishes local evidence without tenant queries; focused validation reuses saved identifiers and recent results.- Saved result metadata includes recoverable per-column lineage.
- Identifier-led exploration discovers new fields and nested JSON paths using typed query results, exact value matches, and reusable private evidence.
Changed
Section titled “Changed”- Removed
session end --prompt-feedback; use the closure receipt’s feedback command.schema collect --sourcenow requires--explore, and--max-queries-per-pageis a total per-invocation query budget. - Session-end cancellation returns 130 after durable closure. Expected scope exclusions are reported separately from incomplete or failed eligible work.
- Session end flushes closure and feedback instructions before maintenance, then
emits a separate terminal maintenance record; incomplete upkeep returns exit
14. Added
--no-maintenanceand a configurable 90-second overall deadline capped at 3,600 seconds. Progress respects automatic quiet mode for pipes. - Automatic evidence retirement now follows explicit result-pruning cutoffs and cannot be recreated by mining the same old artifacts. Explicit observation and proposal evidence remains protected.
- Untested local overlaps have low confidence. Generic vocabulary and malformed seeds are excluded, while strong empirical evidence retains ranking priority.
- Pending discovery work runs before stale refreshes across daily budget windows; KQL verbatim strings preserve projection lineage and malformed literals fail closed. Oversized seed batches split instead of aborting all discovery.
schema collectnow mines local artifacts before querying the tenant. Broad identifier-led exploration requires--explore. Local validation saves progress after each bounded query batch.- Removed
schema collect --exhaustive; use--explorefor identifier-led discovery, replacing the six-field starter matrix and target-field sweep. Query budgets pause remaining work; result caps and depth limits are explicit coverage gaps. - Removed
schema collect --resume, the legacy collection executor, checkpoint status, and checkpoint bundle support. Rerunning current collection commands continues remaining work using their saved query evidence. - Navigation prioritizes empirically validated and observed pivots over authored catalog status. Overlap remains correlation evidence, not a join assertion.
- Expected timeless tables and unsupported paths no longer force every session end to return partial success. Validation can reverse a pair when only its other endpoint supports a time bound.
- Single coincidental overlaps remain candidates; tenant-wide and built-in constants are excluded from discovery. Overlapping cohorts cannot count twice toward validation, and future-dated artifacts cannot establish freshness.
- Bounded JSON and KQL lineage handling preserve useful hunt results when optional discovery parsing cannot process a cell or expression. Free-text JSON no longer manufactures dynamic fields.
- Retention indexes metadata once per publication and contains malformed automatic references without weakening explicit observation/proposal pins.
- Opposite-direction observations now aggregate into a single bidirectional pivot rather than preventing tenant graph composition.
- Preserve reproducible column lineage through JSON expressions and skip malformed identifiers without aborting local discovery.
- Revalidate changed time windows or seed cohorts, retire invalid local evidence, and support projected evidence in candidate review.
- Reuse verified evidence within each discovery report and serialize tenant collection to avoid redundant reads and concurrent duplicate queries.
- Preserve underlying authentication recovery details in session-end receipts.
[0.10.0] - 2026-09-30
Section titled “[0.10.0] - 2026-09-30”BloodHound readability update for the OpenGraph schema export.
schema export-opengraphnow links tables directly withJoin,NormalizeJoin,SameEntity,Correlate, andBridgeedges, one per table pair and pivot kind, soMATCH p=(:XDR_Table)-[:`Join`|NormalizeJoin|SameEntity|Correlate|Bridge]->(:XDR_Table) RETURN pdraws a table-level pivot map. Each edge lists the field pairs and relationship IDs it summarizes. Queries that match every relationship from a table, such asMATCH (:XDR_Table)-[r]->(), now also return these edges.schema export-opengraph --custom-nodes PATHwrites the request body for BloodHound’sPOST /api/v2/custom-nodes, giving tables, fields, entity kinds, and namespaces their own icon and color. xdr-cli does not contact BloodHound.- Every exported node has a short
nameproperty (table name, column, entity kind, or namespace), which BloodHound uses as the canvas label ahead of the longdisplayname.
- Updated locked PyJWT from 2.14.0 to 2.15.0 to address GHSA-42vr-xj54-vc7v, an unhandled exception when parsing certain JWT payloads.
- Both OpenGraph output files are staged before either is published. A later styling-publication failure reports the saved graph and failed path with partial-success exit 14.
- Table summaries use the weakest member evidence and expose mixed statuses, confidence levels, candidate counts, and directions. Candidate keys cannot inherit reviewed status or traversability from another member.
- Overlapping direction groups merge into one edge per start/end/kind instead of emitting duplicate table edges.
- Table-map examples quote
Join, a reserved word in BloodHound CE’s Cypher parser.
[0.9.0] - 2026-09-03
Section titled “[0.9.0] - 2026-09-03”Autonomous semantic-crawl and empirical-pivot update following live-tenant validation of the initial graph workflow.
schema discoveriesreports automatically usableobservedand machine-validatedinvestigation pivots;schema candidatesremains a compatibility alias.schema collect --resumepersists a tenant-bound crawl checkpoint, whileschema observeexposes pinned--from-run,--start-query,--max-queries, and--schema-generationcontinuation controls.- Repeatable
--target-tableand--exclude-tablefilters support narrow retries and explicit coverage exclusions. - Pivot, path, and OpenGraph output expose evidence level and join safety, with BloodHound examples for direct and multi-hop schema traversal.
Changed
Section titled “Changed”- Compatible scalar targets on the same table share one bounded table scan, eliminating the prior field-by-field rescans that made exhaustive crawls impractical on wide tables.
- Target-local API, query, and timeout failures are quarantined with exact task, table, locator, and retry details while collection continues across the remaining tables and source locators. Authentication and rate-limit failures still stop globally.
- A completed positive observation is immediately available as a
correlation-only investigation pivot. Two independently sampled,
artifact-verified positive runs can advance it to
validatedunder the versioned tenant search-pivot policy. - Automatic empirical evidence never claims raw equality-join safety, cardinality, a bridge, or repository-reviewed semantics. Optional private context review and contributor proposals remain available for ambiguous investigations or independently contracted core changes.
- Schema help, README guidance, and the detailed graph guide now distinguish routine target caps from exhaustive all-table coverage and document paging, resume, quarantine, evidence levels, and current BloodHound behavior.
- Deprecated
hunt libraryandhunt library-runaliases remain available through the 0.x line for compatibility and now advertise removal in 1.0. - Release CI now validates the latest three stable Python feature releases (3.12–3.14), with Windows state-portability coverage running on Python 3.14. The declared Python 3.11 minimum remains unchanged.
- Generated semantic-probe KQL now uses Defender-compatible
letidentifiers, preventing live crawls from quarantining otherwise valid target-table pages. - Library parameters are now rendered through typed, quote-aware KQL literal encoding, preventing tenant-controlled entity values from changing a built-in or user-library query’s syntax.
config.toml, token caches, and stored portal cookies use private atomic persistence; MSAL cache acquisition is serialized across processes so concurrent commands cannot lose refresh-token or audience-cache updates.- Portal cookies are bound to the configured tenant, and cookie-authenticated MachineIds are verified through that tenant’s official MDE API before portal events are accepted. Cookie-source cleanup refuses symlinks and rechecks the validated file identity before overwrite.
- Schema collection children now run with isolated module search paths, finite process deadlines, and concurrently drained 1 MiB control-output caps; timeouts remain resumable from the existing checkpoint.
- Explicit device-timeline exports publish atomically with owner-only
descriptor-backed staging, reject unsafe shared directories, refuse existing
paths by default, and replace symlink entries rather than following them when
--forceis intentional. - Incident update/comment split failures report non-retryable partial success with an inspection-and-comment-only recovery path, preventing accidental replay of an already-applied field update.
- OData list filters encode apostrophes and validate closed severity/status vocabularies before sending requests.
- OpenGraph exports use unique owner-only temporary files, avoiding stale-file and concurrent-export collisions.
- Removed the nonexistent Typer
allextra from package metadata while keeping the tested Typer 0.24 compatibility range. - Completed observations retain their actual matched/no-match outcomes when a different target table fails instead of being downgraded wholesale to partial evidence.
- Observe continuations reuse the exact sampled or explicit source artifact and every post-refresh page is pinned to one physical-schema generation, preventing resampling drift and mixed-generation evidence.
- Resumed and imported collection checkpoints enforce their complete plan contract and may invoke only the expected read-only schema child commands.
- Effective graph composition reopens tenant-bound source and target artifacts
before granting
validated; imported or edited metadata cannot self-assert machine validation. - Corrected stale command references and examples throughout schema docstrings,
--help, diagnostics capabilities, README, and the detailed graph guide.
[0.8.1] - 2026-08-12
Section titled “[0.8.1] - 2026-08-12”Semantic schema graph release-hardening update following live-state and
adversarial bundle validation of 0.8.0.
Changed
Section titled “Changed”- Semantic probes match normalized identifier values across tables while using
Defender
Timestampor Sentinel/workspaceTimeGeneratedonly as independent lookback bounds. Tables without either remain excluded rather than producing unbounded or misleading evidence. - Schema collection and OpenGraph documentation now distinguish the six default reviewed source locators from the full eligible target catalog, and document BloodHound Quick Upload, source replacement, and Cypher visualization for public, tenant, and candidate layers.
- Bundle help and documentation now describe SHA-256 and tenant fingerprints as integrity/routing checks rather than archive-authenticity proof.
- Portable bundle inspection/import now enforce exact schema, result, proposal, and inactive-session namespaces plus sensitivity bindings; unsigned archives cannot inject configuration, query-library, auth-adjacent, or other live xdr-cli state.
- Tenant overlays cannot self-assert repository-reviewed relationships through import or effective graph composition; tenant-only relationships remain candidates until code review.
- Bundle export validates size and content before publication, leaves no rejected output, creates owner-only POSIX archives, and preserves collision refusal and rollback on filesystems without hard-link support.
- Read-only bundle inspection streams bounded headers and payloads without retaining archive contents, and imported session history must satisfy exact filename, JSONL, and counter-sidecar contracts.
- Maintenance advisories honor automatic pipe quieting and use fixed-cost current markers instead of scanning growing result/observation history on every unrelated command.
- Corrected attachment-to-message and image-load-event-to-device-inventory
cardinalities to
many-to-onein their stored direction. - OpenGraph node IDs now preserve readable table, field, entity-kind, and namespace names because BloodHound uses the root ID as the graph’s visible object label; sanitization-only collision suffixes replace always-opaque SHA-256 IDs.
- Added a dedicated Windows CI job for bundle, cache, overlay, command, and session portability contracts.
[0.8.0] - 2026-08-11
Section titled “[0.8.0] - 2026-08-11”Semantic schema graph and correlation release.
schema diagnosticsexposes package/source identity, registered schema capabilities, passive-ingestion outcomes, evidence eligibility, and maintenance state without tenant access.schema migrate-cache --yescontent-binds a structurally valid pre-digest physical cache while preserving its original metadata in quarantine.schema bundle export,schema bundle inspect, andschema bundle importprovide content-bound schema-state relocation with same-tenant activation, evidence/proposal path rewriting, optional inactive session history, collision refusal, and rollback on interrupted publication.- Packaged semantic graph and an explicitly scoped Microsoft E5-oriented reviewed starter profile with typed pivots, direct joins, transforms, bridge-table paths, correlation-only relationships, deterministic path traversal, and OpenGraph export.
schema observewith explicit exhaustive tenant-locator planning, safe default bounds, counts-only batched probes, private provisional interpretations, partial-success publication, and candidate-only learning.- Cache-only
schema candidatesevidence triage with automated sufficiency gates, explicit human-context requirements, and no bulk promotion path. schema statusplus boundedschema collectmaintenance, with a nonblocking periodic stderr advisory and explicit exhaustive mode.- Native
schema export-opengraphfor public or value-free tenant overlays; repository Python scripts remain contributor/UAT tooling. - Bounded
schema candidate-review, privateresults head, and reference-aware schema/result pruning complete the candidate evidence lifecycle without adding a bulk approval path. - Lineage-validated offline artifact correlation with relationship-path safety metadata, lossless reviewed nested JSON locators, privacy-filtered passive paths, private tenant overlays, and purpose-built live-validation tooling.
Changed
Section titled “Changed”schema statusnow reports physical integrity, semantic-overlay integrity, packaged-contract compatibility, collection state, passive ingestion, and observations inside versus outside the 90-day readiness horizon separately.schema repair-overlay --yesnow quarantines exact prior artifacts and publishes a loss-aware compatible generation when packaged contracts change; obsolete provisional interpretations and dependent observations are inactivated rather than silently reinterpreted.- Result sidecars can passively register nested string paths when physical lineage is deterministic; ambiguous projections, joins, and legacy paths fail closed.
- Result sidecars now carry tenant fingerprints and data digests; offline correlation verifies both plus exact registered paths and row counts.
- Physical schema availability, semantic pivot compatibility, and reviewed join safety are now represented and documented as separate claims.
- Legacy
sha-lowerprovisional interpretations no longer conflict with the currentsha256-lowerpackaged source during collection planning. - Legacy passive nested fields that fail the current reviewed key policy are excluded from availability, probing, traversal, and correlation.
- Result and candidate-proposal registrations are safely relocated during portable import instead of requiring manual absolute-path edits.
- Bundle inspection now bounds aggregate uncompressed bytes before extraction, bundle publication refuses concurrent destination creation atomically, and optional session-history imports coordinate with live session counters.
- Semantic source sampling uses Advanced Hunting-compatible deterministic ordering; batched probes avoid cross-union scalar scope assumptions, malformed sampled identifiers fail soft, and the live-validation collector bounds validation fan-out while prioritizing reviewed mappings. Collection progress, sanitized diagnostics, and an explicit sensitive-debug mode make live probe failures attributable without weakening the default value-free bundle.
- Observation publication accepts the numeric-leading generation IDs emitted by
schema refresh, and source sampling over-fetches candidates so malformed rare UPN placeholders do not crowd out valid identifiers. - Schema path rows now distinguish direct joins, multi-hop join chains, and
sequential pivots with explicit route kind, hop count, and join-compatibility
fields while retaining the existing
DirectJoincontract. - Passive nested-field learning now uses a reviewed property allowlist and rejects every projection variant, preventing tenant values or renamed fields from entering value-free lineage. Tenant overlays round-trip relationship records instead of publishing generations their loader cannot read.
- Candidate readiness now requires distinct evidence bundles and non-empty context, mixed-invalid retained samples fail soft, and a failed later batch marks the whole observation run partial. Empty or unavailable routine sources no longer prevent otherwise complete maintenance.
- Private result previews verify bundle identity, digest, row type, and count; result pruning aborts before deletion when any current schema overlay cannot be validated.
- Candidate readiness now rebinds each claim to the exact source artifact,
selected-seed count, target interpretation, probe batch, and independently
distinct source/target bundles. Collection preserves empty-source outcomes
and relaunches portably under both console-script and
python -minvocation. - Tenant overlays now bind generations to content digests.
schema statusdiagnoses integrity state andschema repair-overlay --yesatomically selects the newest valid retained generation or migrates legacy metadata. schema validate-coregives the human-reviewed core-graph update lifecycle a native graph/profile and generated-document validation command; it can update only the generated reference block when explicitly requested.- Overlay recovery now detects missing manifests, repairs all local tenant overlays, and offers an explicit quarantined empty reset when no generation is usable. Result pruning and overlay publication share an evidence lock.
- Core-document validation rejects unmatched, duplicate, or reversed generated markers with a structured corrective error instead of mutating the file.
- Probe plans now carry canonical field dependencies for provisional interpretations, and passive nested learning reuses exact packaged records while correctly marking tenant-observed paths available.
- Result pruning validates exact bundle identity, evidence pruning rejects stale generation replacement, and normal evidence-lock contention is retryable and actionable.
- Physical schema cache generations now validate exact bytes, digest, identity,
and row count in both consumers and maintenance status. Result discovery
exposes copyable physical
Table=RUN_IDcorrelation inputs. - Candidate evidence validation preserves each observation’s source/target direction, and persisted provisional interpretations can no longer masquerade as reviewed probe targets.
- Windows schema refreshes now write the exact LF bytes recorded by cache integrity metadata, and a best-effort old-generation cleanup cannot turn an already committed tenant overlay into a reported publication failure.
- Collection planning preflights its cache once and returns the exact bounded collection command to run. Aggregate collection artifacts retain both a partial child’s saved run ID and sanitized actionable failure details.
- Candidate readiness is computed from independently verified evidence only; incomplete, missing, or provenance-mismatched historical observations remain explicit concerns without poisoning later sufficient evidence.
- Source sampling and candidate review preserve the KQL’s rare-first ordering
when normalizing identifiers. Probes reject or exclude tables without
Timestamp, so an unbounded query cannot claim a bounded evidence window. results rows --type ... --offset ...makes late correlation match records inspectable through the CLI, and correlation receipts link to that view.- Evidence-ready candidates can produce a deterministic, value-free
candidate-proposalpacket only after the analyst supplies explicit relationship semantics and provenance; it never edits the core graph. The proposal cannot shadow reviewed/deprecated core semantics, requires an actual contract/documentation citation for join or bridge claims, and atomically binds its exact evidence-generation snapshot and proposal-file bytes. Extant proposal drafts protect their verified receipt and referenced evidence from result pruning; digest, snapshot, path, row, or binding mismatches stop pruning instead of trusting altered references. - Core-graph upgrades reconcile retained tenant fields/interpretations when their stable semantic contracts are identical and only private/provisional annotations differ, both during effective composition and subsequent overlay publication. Real type, locator, namespace, role, normalizer, constraint, or relationship conflicts still fail closed.
- Artifact receipts now state preview
shown,total, andhas_moreand provide an exactresults rowsreader whenever the two stdout previews are incomplete. Nestedschema pivotinputs canonicalize documented dotted shorthand before traversal. - The immediately preceding four-key candidate-proposal sidecar remains upgrade-safe: intact live drafts are row-verified and conservatively retain evidence, while deleting a retired draft releases it without a permanent prune integrity error.
- Live proposals now cause result pruning to open and fully validate every referenced source, target, and review bundle, including tenant, stage, and cross-reference bindings. Missing or corrupt evidence stops deletion.
- Explicit file/stdin seeds now receive zero-preview private source artifacts, participate in deterministic readiness validation, and remain clearly marked for origin review. Candidate review falls back past invalid newer evidence.
- Effective graph conflicts now return an actionable schema conflict instead of a generic internal error, and tenant observation extensions survive overlay rewrites for forward compatibility.
- Incomplete receipts expose
results_commandseparately from workflownext_command, so collection guidance cannot hide the complete local result. - Missing
Timestamppreconditions now use one actionable conflict exit/code across source sampling and candidate review instead of masquerading as an artifact-write or invalid-argument failure.
[0.7.4] - 2026-07-31
Section titled “[0.7.4] - 2026-07-31”Changed
Section titled “Changed”- Operational, contributor, security, schema, and foundation-design Markdown now matches the shipped artifact/session/discovery contracts and avoids treating schema availability as proof of table population or licensing.
- Examples now consume complete compact output safely, distinguish pre-artifact timeouts from successful result provenance, and use the validated portal cookie path for device-timeline quick starts.
[0.7.3] - 2026-07-31
Section titled “[0.7.3] - 2026-07-31”Windows session-concurrency remediation release.
- File-backed locks now combine the existing cross-process sidecar lock with a per-path in-process mutex, preventing concurrent Windows threads from entering the same marker-update critical section.
- Atomic session-marker publication now retries brief Windows sharing violations with a bounded backoff while continuing to surface persistent access failures.
[0.7.2] - 2026-07-31
Section titled “[0.7.2] - 2026-07-31”Windows UAT remediation release for the artifact-first foundation.
- Built-in and user KQL files now decode explicitly as UTF-8, preventing Windows ANSI code pages from corrupting query descriptions and content before they reach library artifacts or execution.
- Cross-platform credential and audit-log tests retain their functional
coverage on Windows while limiting Unix mode-bit assertions to POSIX,
where the
0600contract is meaningful.
[0.7.1] - 2026-07-31
Section titled “[0.7.1] - 2026-07-31”Focused live-tenant remediation release for the artifact-first foundation.
xdr device timeline --hours Nfor narrow pulls on high-volume devices;--hoursand--daysare mutually exclusive and the default remains seven days when neither is specified.
- Result storage now enforces private POSIX modes on the config home and every
results directory (
0700) as well as generated and explicit timeline files (0600). - Portal-cookie verification now checks HTTP success without requiring a
dict-shaped response, and portal login-timeout responses (HTTP 440) map to
the authentication recovery path (exit 2) with valid
xdr auth ...commands. - Main authentication errors no longer suggest the nonexistent
xdr logincommand.
[0.7.0] - 2026-07-31
Section titled “[0.7.0] - 2026-07-31”Breaking pre-1.0 foundation release based on sanitized Copilot-workflow analysis.
- Private artifact-first result bundles under
~/.xdr-cli/results/YYYY-MM-DD/: data-only JSONL, provenance/observed-shape sidecars, compact receipts, bounded previews, atomic writes, and explicitresults list/show/query/shape/prunelifecycle commands. - One-line versioned corrective errors with stable codes and distinct recovery exits for usage, permission, not-found, throttling, timeout, network, artifact, conflict, and partial-success failures.
- Automatic 30-minute session attachment, incident/alert anchors, nonblocking ambiguity, explicit concurrent sessions, deterministic end summaries, and immutable append-only agent/analyst feedback.
- Native
library list/show/runand cachedschema refresh/tables/showdiscovery backed by the existing KQL catalog andsys_schema_probe. - Typed library parameter, table, output-field, permission, and cost hints; bounded/searchable result-shape discovery; and visibly stale schema receipts.
Changed
Section titled “Changed”- Hunt and library execution, library listing, schema refresh/browsing, alert/incident list/show, investigate, and default device timeline output now save the complete returned dataset and write at most one receipt plus two previews to stdout.
- Expanded incidents, alerts, and investigations are normalized into granular JSONL records; partial enrichment exits 14 while retaining its artifact.
- Automatic alert sessions attach only after Graph supplies authoritative incident identity, and schema cache generations publish atomically.
- Sessions are optional telemetry and never gate read-only investigation work.
The old
hunt libraryandhunt library-runpaths remain deprecated aliases for one release;hunt library-showremains the compatibility executable-KQL renderer while nativelibrary showreturns a typed descriptor. - Versioned exit taxonomy preserves codes 0–5 where accurate and adds 6–14; 403, 404, 429, timeouts, and network failures no longer collapse into code 3.
Removed
Section titled “Removed”- Global
--fieldsand native--jq/JMESPath projection. - Hunt display
--limit, which discarded locally displayed rows without controlling Defender API execution. - The arbitrary synthetic
XDR_SESSION=adhoc-Nescape hatch; explicit attachment must name an existing session.
[0.6.1] - 2026-07-25
Section titled “[0.6.1] - 2026-07-25”Documentation-correctness release. The instructional docs (README.md,
AGENTS.md, CONTRIBUTING.md, docs/, playbooks/) are read directly by AI
agents and treated as contract, so each drift from the implemented CLI, query
library, and Defender schema is tracked below. Corrections were verified
against the code, and — where marked (tenant-verified) — against a live
Defender XDR tenant (read-only, 30 recorded invocations, 2026-07-25).
Changed
Section titled “Changed”--rationalehelp text (src/xdr_cli/main.py) now reads “Captured on any invocation record when a session is active.” The prior “silently ignored on session/history/annotate paths” was wrong — the flag is recorded onto any command’s invocation record whenever a session is active.
AI-agent output contract (AGENTS.md, README.md):
- Corrected “all output is JSON”:
session start/end/resumeandannotateprint a plain-text line;device timeline/session showemit raw JSONL, not the{status,data,metadata}envelope. Agents branching on the envelope were told a contract the CLI does not emit for those commands. - Removed the nonexistent
metadata.cpu_usagefield (quota surfaces only as HTTP 429); renamedexecution_time→execution_time_ms;metadata.session_id/session_labelare present only on success envelopes (error envelopes carry nometadata) — was documented as “always present.” xdr auth logindocumented as interactive-first (WAM/browser) with device-code fallback, matchingauth.py(was “device code flow”).investigateclarified as read-only (audit-logged for traceability, mutates nothing) — it was listed among “state-changing commands,” contradicting the read-only-command list two sections away.
Device-timeline auth accuracy (README.md, AGENTS.md, auth_cmd.py help, and the [0.6.0] entry above):
- Reframed the portal-auth story to match what is actually validated: cookie
auth (
xdr auth portal-cookie, a Microsoft Edge DevTools “Copy as cURL (bash)”) is the validated, recommended method; the MSAL/FOCIxdr auth portal-loginpath is now marked experimental and unverified (it had been documented as the default in “Three auth paths”). Sign-in-log attribution (“Microsoft Azure CLI”) is stated as intended, not verified fact, with a “confirm in your own tenant” caveat.
CLI command/flag examples (README.md, playbooks/):
xdr hunt runtakes a positional KQL argument; removed the nonexistent--kqlflag.xdr device isolate/unisolate/restrictrequire--comment; added to every runnable playbook example and the README reference table (scan/collect-packagedo not require it). The bare form exits 2 before running.api_timeoutdefault corrected to 120s (was documented as 30).- Removed the nonexistent
xdr threats listcommand from examples. - README custom-query example now includes the required
-- tier:frontmatter — the prior example raisedQueryErroron load.
KQL authoring & schema (docs/schema_pivots.md, docs/schema_probe.md, docs/investigation.md, playbooks/):
--lines are repo frontmatter, not KQL comments (KQL line comments are//).RiskLevelDuringSignInis an integer, not a nullable string — the!inguidance was wrong; the failure is a type mismatch, not nullability. (tenant-verified)CloudAppEventsacting user isRawEventData.UserId(noAccountUpncolumn);GraphAPIAuditEventsusesIpAddress(camelCase); removed nonexistent columns (AlertEvidence.Url→RemoteUrl,EmailPostDeliveryEvents.DeliveryAction, others).- Exchange
Sendaudit records carry noSubject/RecipientsinRawEventData; the email-forwarding sent-mail check now usesqry_email_outbound_detail(EmailEvents-based) and the custom rule query gains the missingRedirectToforwarding vector. (tenant-verified) BehaviorIdis not unique acrossBehaviorInfo/BehaviorEntities. (tenant-verified)- Corrected the curated-table count (63 → 80), initiating-process bundle count
(25 → 27), and probe row-count/runtime figures;
schema_probe.mdQuick Start now documents the requiredxdr session startprerequisite and the JSON→CSV step.
Library query references (playbooks/):
qry_inbox_rule_audit→qry_inbox_rule_activity(former is a deprecated alias).ttp_oauth_app_signin_anomaly/ttp_oauth_consent_anomalydescriptions aligned to the queries’ actual scoring signals.- Sign-in baseline window is 30 days (
hours=720default), not 7; corrected param names and the password-sprayFailCount/UniqueAccountsdescription.
Contributor & security docs (CONTRIBUTING.md, docs/contributing-walkthrough.md, SECURITY.md):
- Branch-protection description corrected against the live rulesets:
mainhas two rulesets; the review-requiring one exempts the repository Admin role via a bypass actor (was “no bypass actors” / “self-merge not possible”), and required status checks (lint-and-test 3.11/3.12/3.13, version-bump/CHANGELOG, GuardDog) are enforced. (verified viagh api) - A new flag is a MINOR bump, not PATCH (walkthrough examples 0.4.1 → 0.5.0).
- Fixed
SECURITY.mdsource path (src/xdr_cli/commands/auth_cmd.py) and the private-vulnerability-reporting steps; corrected a nonexistent test path (tests/test_domains_cmd.py) and the Keep-a-Changelog category list.
CLI help strings & docstrings (src/xdr_cli/):
- Audited every
--helpstring and command docstring against the code and fixed 15 drifts (the review had only covered the markdown docs, not the CLI’s own self-description). Examples: theincidentsoutput-schema status enum was missinginProgressand “list active” actually lists all incidents;--determinationhelp now enumerates the accepted values (was a circular “see--help”);investigate’s containment-gate docstring omitted theinformationalExpectedActivityexclusion;hunt --timeoutreferenced a nonexistent “summary tier” (summaryis a mode); a stalexdr history --anchor-incidentreference (the real flag is--incident); andhistory --by-actorhelp now matches the actual JSON shape (thecpu_usage_total_by_actorsibling key).
Cross-file consistency & housekeeping:
- Removed a dead cross-reference to a nonexistent
usb-worm-malware.mdplaybook; fixed ordered-list numbering (playbooks/README.md,email-malicious-delivery.md); standardized “Microsoft Entra” branding; documented the--ttlweeks unit.
[0.6.0] - 2026-07-24
Section titled “[0.6.0] - 2026-07-24”xdr device timeline <hostname-or-id>: paginated download of Defender for Endpoint device timeline events via the unofficialsecurity.microsoft.com/apiproxy/mtp/mdeTimelineExperienceendpoint. Extends visibility to ~180 days, beyond Advanced Hunting’s 30-day retention.<device>is resolved to a MachineId via the official MDEmachinesAPI first; only the timeline fetch itself uses the unofficial portal apiproxy. The validated auth path is cookie auth (xdr auth portal-cookie): paste the browserCookieheader from a logged-in Defender portal session — a Microsoft Edge DevTools “Copy as cURL (bash)” of the timeline apiproxy request — which reuses your existing portal session. A separate MSAL/FOCI path (xdr auth portal-login, Azure CLI public client, isolated from xdr-cli’s main app registration) also exists but is experimental and unverified: it may not succeed in every tenant, and its Entra sign-in-log attribution has not been confirmed.--refresh-token(envMDE_REFRESH_TOKEN) is supported for CI. Output is raw JSONL — one event per line, NOT the standard{status, data, metadata}envelope other commands use — to stdout or--output PATH(optional--gzip); a one-line event-count summary prints to stderr.xdr auth portal-login/xdr auth portal-cookie/xdr auth portal-logout: manage the separate portal auth.portal-cookie COOKIE_SOURCE— the validated method — takes the whole browserCookieheader from a file (or-for stdin) — a saved Microsoft Edge DevTools “Copy as cURL (bash)” of the timeline apiproxy request, or a raw Cookie header — and stores it in~/.xdr-cli/portal_cookies.json(0600), with an optional--verify/--no-verifylive apiproxy check; cookie values never reach argv. It forwards every cookie verbatim — including the routing (X-PortalEndpoint-RouteKey) and session (s.SessID) cookies the apiproxy backend requires (whose absence the backend answers with an opaque HTTP 500) and a chunkedsccauth(chunks:N+sccauthC1…sccauthCN) — and auto-extracts the XSRF token (prompting only if the header carries noXSRF-TOKENcookie). A file/stdin source is required rather than a hidden prompt because the full header runs past the ~4 KB a terminal accepts on one line. On a successful import the source file is securely shredded by default (it holds a live bearer credential);--keep-sourceopts out. A source with nosccauthcookie is rejected and never shredded, so a mistyped path can’t be turned into a file wipe. The portal client now sends browser-parityAccept/Accept-Languageheaders and anxdr-cliUser-Agent (not the defaultpython-httpx), and surfaces the failing request path (with itsfromDate/toDate/skipTokenquery) plus the apiproxy error response body — or routing/correlation headers on an empty-body 5xx — inAPIError.detailso failures are diagnosable.portal-loginattempts an interactive FOCI/MSAL sign-in (Azure CLI public client) as an experimental, unverified alternative to cookie auth.portal-logoutclears both the portal MSAL token cache and the cookie store, independent ofxdr auth logout.xdr auth status: now reports both main and portal auth state, including the active portalmethod(msalorcookie) and itsaudit_app_namelabel (“Microsoft Azure CLI” for MSAL, “Microsoft Defender portal (browser session)” for cookie). The label reflects the client each method authenticates as; how portal activity actually surfaces in Entra sign-in logs has not been independently verified.
xdr device timeline: pagination silently truncated to the first page (~1000 events) on any device with more history, ending with a spurious 404. The apiproxy’sNextlink is a path relative to themdeTimelineExperienceendpoint (e.g./machines/<id>/events?…&ReportIdForScrolling=…) and does not repeat that segment; the follow-up request merged it against the base URL alone, hitting/apiproxy/mtp/machines/<id>/events(segment dropped) → 404. The endpoint segment is now re-added when followingNext, mirroring the reference downloader, so the full requested range is retrieved.Nextis also now followed with its query string preserved byte-for-byte (a decode/ re-encode round-trip would have corrupted a base64skipToken’s literal+). The initial request now also sendsIsScrollingForward=trueso the apiproxy pages forward fromfromDatethrough the whole window; without it the apiproxy returned only the newest page and a forward-to-now cursor that terminated immediately, truncating history to the first ~1000 events.xdr device timeline --from/--to: now accept the RFC3339 forms the help documents — a trailingZ(UTC) or a numeric offset (e.g.2026-07-24T21:00:00Z,…-05:00). Previously only naive forms parsed, so the documentedZsuffix was rejected client-side.xdr device timeline --to: is now an exact upper bound. Forward-scroll pagination overshootstoDatewithin the final fetched page (the apiproxy bounds pagination, not individual events), so events after--toare now filtered out; previously the last page could return events well past the requested cutoff. (No effect on the common--dayscase, where--tois now.)- The audit log (
~/.xdr-cli/audit.log) is now created mode0600instead of theFileHandlerdefault (~0644); it records command history and should not be world-readable, matching the cookie/token stores.
Documentation
Section titled “Documentation”device timelinehelp + README now document the identifier mapping: thedeviceargument is aDeviceName(hostname) or a 40-hexDeviceId, the portal timeline’sMachineIdis Advanced Hunting’sDeviceId(and the hostname isDeviceName, verified against live data), and each event carries the pair underMachine.MachineId/Machine.Name— so a timeline pivots into theDevice*Advanced Hunting tables onDeviceId, with the full table list and a KQL pivot predicate in README §“Device Timeline”.
- The portal API surface used by
device timelineand theportal-*auth commands is undocumented and unstable. Microsoft may break this at any time. SeeREADME.md§“Device Timeline” for the trade-offs, including audit-attribution and compliance considerations.
[0.5.2] - 2026-07-23
Section titled “[0.5.2] - 2026-07-23”- Silent supply-chain coverage hole:
scripts/filter_guarddog.pyread only GuardDog’sresultsand ignored per-packageerrors, so a scan whose rules failed to run (e.g. semgrep missing) reported “clean” and exited 0 while having scanned nothing. The filter now fails closed on any rule-run error. - “Clean” now requires proof that a scan happened.
filter_guarddog.pyadditionally fails closed on an empty payload (what GuardDog emits, exit 0, for an empty requirements file), on a package whoseresult/results/errorsis missing or not a mapping, on a package whoseresultsmapping is empty, and — via the new--closureargument, passed by both workflows — on any package listed in the scanned closure that never appears in the output. GuardDog silently drops requirements it cannot resolve and still exits 0, so a partial scan previously read as clean. - Allowlist matching could bless more than it was given. One entry now
forgives exactly one finding (matching consumes it), so a second identical
finding injected at a new line is no longer accepted; metadata findings
(
bundled_binary,single_python_file, …) key on their message, so one entry no longer blesses any future set of bundled binaries; duplicate YAML keys are rejected instead of silently discarding the entries under the first; a leftoverTODOreason is rejected;file/code/reasonare type-checked (YAML coercescode: 12345to an int) instead of raisingAttributeError; and snippet normalization keeps line boundaries, so a multi-line span re-flowed onto fewer lines is no longer the same key.
Changed
Section titled “Changed”- GuardDog now scans a pinned closure (
.github/guarddog-scan-closure.txt) instead of a livepip freeze, so the required check only moves when a PR moves it — not when PyPI drifts. Same 35-package population as before (audited: zero added, zero removed);pyproject.tomlkeeps its>=ranges, so end-user resolution is unchanged. - The GuardDog allowlist is now structured
(package, rule, file, code)entries instead offile:linestrings: line-agnostic, so upstream line moves no longer break CI, but still file-anchored, so a blessed line relocated into a new path is re-reviewed. Entries are emitted/parsed viayaml.safe_dump, making snippets with quotes, backslashes and unicode safe. Several justifications were corrected against the code actually flagged. - The GuardDog scan output is no longer cached at all. A required security gate must not be satisfiable by a stored artifact: a cache hit skipped the scan, so a planted JSON blob could pass the check, and with the closure hash now deliberately stable the scan could have gone unrun for months while GuardDog’s live-state rules (typosquatting, maintainer domains) never re-evaluated.
uvin the supply-chain watch workflow is now hash-pinned (.github/uv-requirements.txt); pinning the resolver does not stale the resolution, sinceuv pip compilestill queries the live index. Both GuardDog jobs carrytimeout-minutes: 20, andci.ymldeclares a top-levelpermissions: contents: read.
.github/workflows/supply-chain-watch.yml— a weekly, off-gate scan that compiles the closure fresh frompyproject.tomland scans that live resolution. The pinned scan cannot see newer versions developers actually install, so this restores that zero-day coverage as early warning without blocking merges.
[0.5.1] - 2026-07-23
Section titled “[0.5.1] - 2026-07-23”Changed
Section titled “Changed”version-and-changelogCI job skips the version-bump and CHANGELOG checks for PRs whose entire diff is under.github/— unblocks dependabot PRs. Nothing under.github/ships; a diff touching any file outside it still gets the full policy. Detection fails closed (bad base SHA fails the job; renames out of the shipped tree still count as shipped).- CONTRIBUTING §1/§4/§5 updated to match;
.github/workflows and guarddog config added to §4’s higher-risk always-wait-for-review list as the compensating audit control. - Aligned the PR template,
docs/contributing-walkthrough.md, and CONTRIBUTING §4/§7 with the.github/-only exemption; corrected the branch-protection description to reflect the activemainruleset.
[0.5.0] - 2026-07-20
Section titled “[0.5.0] - 2026-07-20”sys_schema_probenow covers 17 confirmed Microsoft Sentinel and Entra workspace tables, including Entra sign-in/audit logs, Sentinel incidents and alerts, and workspace health data.
Changed
Section titled “Changed”- The schema pivot renderer now automatically includes every shared field from the probe, keeping connected-workspace tables represented without adding manual field categories.
- Schema-probe documentation now describes its 80-table curated catalog and the explicit limitation for custom or future workspace tables.
- Generated schema-pivot refresh guidance now uses the supported CLI command and conversion workflow.
[0.4.4] - 2026-05-11
Section titled “[0.4.4] - 2026-05-11”identity_signin_baseline(#20): query returnedAPI 400 — Operator source expression should be table or columnbecausemake_list(pack(... 'LastSeen', max(Timestamp)), 10)is a nested aggregate. Pre-aggregate per(IPAddress, Country)in alet, then buildTopIPsfrom the materialized rows via a left-outer join.identity_signin_baseline:IsKnownEgressreturnednullfor IPv6 source IPs becauseipv4_is_in_any_range()returnsnullfor IPv6 inputs and null bubbled through the OR-chain. Wrapped the call withcoalesce(..., false)so IPv6 sign-ins now reportIsKnownEgress=false(matching the literalin (_xdr_KnownEgressIPs)path) instead of leaking null into downstream consumers. Surfaced during the live test ofidentity_signin_baseline.ttp_ldap_process_attribution(#19): DC-to-DC LDAP and many client-to-DC connections record onlyRemoteIPinDeviceNetworkEvents(RemoteUrlis empty), so joining onRemoteUrlalone dropped real telemetry. Build both an FQDN target list and an IP target list fromIdentityQueryEvents, allow the join to match either, and back-fillRemoteFqdnvia an IP->FQDN map so downstream groupings still key on a meaningful value. NewIsIpFallbackflag surfaces when correlation was IP-only.playbooks/email-forwarding-rule.md(#24): theSend-activity confirmation snippet referencedClientIP=ClientIP(no-op alias).CloudAppEventsexposes the column asIPAddress; aligned with the library queries’ClientIP = IPAddressconvention.
ttp_ldap_process_attributionlookbackparam (default0d): widens only the MDI/IdentityQueryEvents discovery window backward fromstart; the MDE attribution window stays bounded bystart/end. Useful when MDI LDAP events are sparse and a tight start/end leavesLdapTargetRowsempty even though MDE has plenty of LDAP-port traffic. Example:--param start=<incident T-1h> --param end=<incident T+1h> --param lookback=7d.ttp_ldap_process_attribution: detail mode now joinsSuppressedPerDeviceand surfacesSuppressedCountalongside attributed rows (previously only the summary row exposed it). Analysts in detail mode can now see how many connections were dropped byKnownServiceAccounts/ the MDI-sensor hard-exclusion without re-running in summary mode.ttp_ldap_process_attribution: hard-excludesmicrosoft.tri.sensor.exe(the MDI sensor’s own LDAP health-check process, running asNT AUTHORITY\Local Service) inside the query body. Suppressed connections are still counted inSuppressedPerDevicefor transparency. Fresh tenants without a populatedKnownServiceAccountslist now get clean output instead of seeing the sensor itself surfaced as low-severity LDAP attribution.
Changed
Section titled “Changed”-
ttp_ldap_process_attribution: the IP-fallback target list now unions TWO sources — MDI-provided (IdentityQueryEvents.DestinationIPAddress) AND MDE-learned (RemoteIPs observed in DeviceNetworkEvents rows whose RemoteUrl already matched a known FQDN target in the same window). Surfaced by the post-fix live test: the tenant’s MDI never populatedDestinationIPAddressacross the full 30-day retention window, so the MDI-only target list was always empty and the IsIpFallback code path was dormant — DC-to-DC empty-RemoteUrlconnections (8 in the tester’s synthetic validation) silently dropped. The MDE-learned source closes that gap independently of whether MDI ever records the IP.New
IpSourcefield on each attributed row (and rolled up asIpSourcessets +IpFallbackCounttotals on SummaryRow and DetailRows) makes the correlation provenance explicit:fqdn— RemoteUrl matched a known FQDN target directly (strongest).mdi— RemoteUrl empty; RemoteIP matched an MDI-provided IP.mde-learned— RemoteUrl empty; RemoteIP matched an IP we learned from another MDE row whose RemoteUrl was populated and matched an MDI FQDN target in the same window (weakest of the three; still sound).
LdapIpToFqdnback-fill prefers MDI source when both are present for the same IP; the resolved FQDN is the same either way.
ttp_ldap_process_attribution: theIsIpFallback=truecode path is the primary behavioral differentiator versus a naive port-389 filter. Four structural ratchet tests (test_ttp_ldap_process_attribution_has_ip_fallback_path,..._has_mde_learned_ip_source,..._summary_carries_fallback_signal,..._detail_carries_fallback_signal) assert every element of the fallback derivation (MDI + MDE-learned source tables, union, three-value IpSource case,IpFallbackCount/IpSourcespropagation through summary and detail) stays in the rendered KQL so future refactors cannot silently regress. End-to-end runtime behavior of the fallback would require a KQL emulator and is tracked separately; live testing in the report confirmed the MDE-learned path captures the 8 empty-RemoteUrlconnections it was designed for.
[0.4.3] - 2026-05-08
Section titled “[0.4.3] - 2026-05-08”Changed
Section titled “Changed”- Restructured
AGENTS.mdinto mission briefing + CLI reference (~170 lines, down from ~352). Expandeddocs/investigation.mdto be self-contained for investigations (absorbed KQL authoring rules,xdr investigateguidance,--rationale/--learning-mode). Extracted session mechanics todocs/sessions.md. Moved CloudAppEvents inbox-rule KQL recipe to the email-forwarding playbook. - Fixed seven doc-accuracy defects found during code review (obsolete
session-pointer description, phantom command name, mission overclaim, no-op
flag promotion, redundant text, orphaned term, imprecise bypass description).
Added code-fidelity verification rule to
CONTRIBUTING.md§7.
[0.4.2] - 2026-05-05
Section titled “[0.4.2] - 2026-05-05”Changed
Section titled “Changed”.github/guarddog-requirements.txt: CI - bumped python-multipart from 0.26 to 0.27.
[0.4.1] - 2026-05-05
Section titled “[0.4.1] - 2026-05-05”Changed
Section titled “Changed”endpoint-malware-genericplaybook: added archive-extraction delivery vector guidance (container pivot, broad/narrow hunt patterns,FileOriginReferrerUrlinterpretation).
[0.4.0] - 2026-05-03
Section titled “[0.4.0] - 2026-05-03”xdr lists initcommand — seeds~/.xdr-cli/lists/from in-repo public defaults (KnownGoodSigners,InternalSubnets,KnownRemoteSupportTools, etc.) with atomic writes and idempotent re-runs. Audit-logged.- Lists infrastructure: queries declare
-- lists: <Name>, <Name>in frontmatter; loader prependslet _xdr_<Name> = dynamic([...])blocks from per-tenant text files at~/.xdr-cli/lists/<Name>.txt. List files support# fetched:,# ttl:,# source:headers; loader emits stderr staleness warnings whennow() > fetched + ttl. - 12 new library queries:
qry_mailbox_delegation,ttp_oauth_app_signin_anomaly,ttp_oauth_consent_anomaly,ttp_discovery_recon,ttp_defender_av_tampering,ttp_ransomware_precursors,ttp_conditional_access_tamper,ttp_credential_dumping,ttp_rmm_first_seen(production tier);ttp_adcs_abuse,ttp_kerberos_delegation_abuse,ttp_device_code_flow_abuse(beta tier). qry_inbox_rule_activity— replacesqry_inbox_rule_auditandqry_inbox_rule_triggers(which remain as deprecated aliases for one release; removal planned for0.5.0).- Methodology surface across the library: every non-utility query now
declares
mode=summary|detail, projectsSchemaVersion, and (for finding-tier queries) carries aSeverity = case(...)band. - Spec-mandated JSON envelope completeness fields on every R1/R2/N/beta
summary row:
EvidenceTotal,EvidenceOmitted,SuppressedCount,FirstSeen,LastSeen,ListHealth. Playbooks consuming summary rows can now see what was omitted before closing on summary alone. tier:andalias_of:frontmatter, validated at load time. Deprecated-alias resolution: invoking atier: deprecatedquery forwards to itsalias_of:target with a one-release stderr warning.- Static methodology contract test (
tests/test_queries_methodology.py) enforcing: tier frontmatter,modeparameter,SchemaVersion, Severity bands, public-list negative-weight contract, deny-list positive operators, deprecated-alias resolution, lists reference real seed files, completeness-field projections, no inline tenant strings in DNS queries, no remainingAADSignInEventsBetareferences,arg_max()not wrapped in scalar conversions,-- name:matches filename stem. scripts/extract_tenant_domains.py— one-shot migration from inline hardcoded tenant FQDNs to~/.xdr-cli/lists/TenantDomains.txt(with CDN allowlist filtering and atomic write).docs/lists.mdanalyst documentation for the lists model.
Changed
Section titled “Changed”- 12 R1 full rewrites:
ttp_token_theft_replay(multi-signal session/token scoring + AiTM + refresh-token replay),ttp_impossible_travel(geo_distance + velocity scoring),ttp_dns_beaconing(jitter scoring with TenantDomains list extraction; +10 weight on_xdr_MaliciousDomains),dns_subdomain_diversity(burstiness scoring with split lists),ttp_ransomware_mass-rename(multi-signal late-stage correlation),ttp_lateral_movement_rdp(DeviceLogonEvents anchor + novelty/fan-out/tunnel detection),ttp_new_service_creation(3-leg union + parent attribution),ttp_encoded_powershell(decode pipeline + AMSI/ETW + obfuscation heuristics),qry_entra_role_changes,qry_exchange_role_changes,qry_privilege_grant_revoke,ttp_lateral_psexec_wmi. - 12 R2 methodology-only retrofits and 29 R3 mode-only retrofits across the existing pivot/finding queries.
- Schema migration:
AADSignInEventsBetaandAADSpnSignInEventsBetareferences inttp_impossible_travelandttp_token_theft_replaymigrated toEntraIdSignInEvents/EntraIdSpnSignInEvents. Onlysys_schema_probe(utility tier, intentionally documents the legacy table) still references the deprecated names. playbooks/email-forwarding-rule.md— full rewrite to consume the mergedqry_inbox_rule_activityend-to-end.- 10 other playbooks — param-name corrections and orphan-query reference
sweep after the merge of
qry_inbox_rule_audit/qry_inbox_rule_triggers. - Public allowlists (
KnownGoodSigners,KnownRemoteSupportTools,KnownGoodParentDomains) wired as negative-weight scoring signals (-3or-5), not row-droppingwherefilters, in the queries that consume them — per the spec’s “Public-list semantics: enrichment vs hard-suppression” contract. Tenant-curated allowlists (KnownServiceAccounts,TenantDomains, etc.) retain row-drop semantics where the operator opts in. - Loader:
parse_frontmatterrecognizeslists:,tier:,alias_of:, multi-lineagent_hint:(continuation via 2-space-indent-- ...). Unknown query parameters now raiseQueryError(a typo’d--param accont_upn=...no longer silently widens a scoped hunt to a tenant-wide sweep). An empty resolved value coerces to the declared default when the default is non-empty (preserves theaccount_upn=scope-pass-through idiom). _WRITE_COMMANDSmatcher inmain.pynow handles multi-token entries (soxdr lists initis correctly audit-logged).xdr hunt libraryrenders empty-default params (account_upn=()) distinctly from required params (account_upn=required) and valued-default params (hours=(168)).kql_parse.extract_tablesnow scans the RHS ofletbindings for table references — previously the loader’s table-introspection pass underreported tables on R1/R2/R3-shaped queries that bind their detail rows to aletvariable.
arg_max()row-selector aggregation no longer wrapped intostring()/substring()in three queries (ttp_dns_beaconing,dns_subdomain_diversity,ttp_encoded_powershell) — the wrapping was broken at runtime per the Kusto docs.- 8 new query files had
-- name:frontmatter stripped of theqry_/ttp_prefix; aligned to filename stem. xdr hunt libraryno longer hard-fails on a single malformed.kql. Builtin and user-installed queries with missing/invalid frontmatter are skipped with a stderr warning; the rest of the library still loads. This previously bricked the entire library command when a stale pre-rename file lingered in~/.xdr-cli/queries/or in a stale pipx package install.- Query timeouts now surface as
error: query timed out (...)on stderr with exit code 3 instead of exiting silently with no rows on stdout — previously a 30s timeout looked indistinguishable from “empty results” to the operator.
Added (post-bump, slated for 0.4.0 retag)
Section titled “Added (post-bump, slated for 0.4.0 retag)”xdr hunt library-show <name> [-p key=value]— render a library query with substitutions applied without executing it. Useful for diffing CLI behaviour against a query pasted into the Defender Advanced Hunting GUI.--timeout <seconds>flag onhunt runandhunt library-run— per-call override ofconfig.api_timeout. Summary-tier hunts with joins / aggregations may need 180-300s on busy tenants.
Changed (post-bump, slated for 0.4.0 retag)
Section titled “Changed (post-bump, slated for 0.4.0 retag)”- Default
api_timeoutraised from 30s to 120s. Library queries with joins / aggregations routinely run 30-90s+ against Defender; the prior default reflected single-event lookups. - Hunt response envelope cleanup.
metadata.execution_timerenamed tometadata.execution_time_msand now always populated — falls back to client-measured wall-clock when the API doesn’t return execution time (Graph’srunHuntingQuerydocuments onlyschema+results, no stats).metadata.cpu_usageremoved: it’s a tenant-wide quota concept (“10 min/hour, 3 hours/day”), not a per-call value, and Defender’s successful responses don’t expose it. Quota exhaustion still surfaces as a 429 via the existing HTTPStatusError handler. - Hunt rows now strip Microsoft Graph’s
@odata.typeannotations (Score@odata.type: "#Int64",IsExternal@odata.type: "#Int64", etc.). Graph emits these next to int64 columns to hint precision back to clients — pure protocol noise that doubled column count for AI-agent consumers when the same precision info is already inresult.schema. - List-block consolidation.
InternalDomainscollapsed intoTenantDomains— same data (“domains owned by my tenant”), different name with no operational distinction.qry_inbox_rule_activityandqry_mailbox_delegationnow referenceTenantDomains. Operators who hand-populated~/.xdr-cli/lists/InternalDomains.txtshould rename it toTenantDomains.txt(or copy the contents over;xdr hunt library-show qry_inbox_rule_activitywill render the values into_xdr_TenantDomainsonce moved). - Dropped four zero-reference seed blocks from the lists catalog:
InternalSubnetsIPv6,KnownGoodDllSha1,KnownGoodSoftwareSha1,MaliciousHashes. None were consumed by any query; they appeared inxdr lists initoutput and_known_list_blocks()validation without doing anything. Re-add when a real consumer ships.
[0.3.0] - 2026-04-27
Section titled “[0.3.0] - 2026-04-27”- Missing playbooks and library queries from prior PR.
Changed
Section titled “Changed”- Playbooks moved to
playbooks/top-level directory.
[0.2.0] - 2026-04-26
Section titled “[0.2.0] - 2026-04-26”CONTRIBUTING.mdat repo root — reference doc covering branching, commits, pull requests, versioning, syncing main, AI-agent rules, the don’t-commit list, recipes for common git mishaps, open-source readiness, and security disclosure.docs/contributing-walkthrough.md— three narrative walkthroughs (first human-only contribution, first AI-assisted contribution, rebase walkthrough).CHANGELOG.md— this file.SECURITY.md— private vulnerability disclosure policy..github/pull_request_template.md— auto-populated PR description with Bump, Test plan (automated + live smoke), Risk areas, References, and pre-merge checklist.- Per-PR versioning policy (SemVer with pre-1.0 stance, project-specific bump table treating the JSON envelope, AGENTS.md contract, and session JSONL schema as public API).
- Test that catches version drift between
pyproject.tomlandxdr_cli.__version__(tests/test_version.py). - CI enforcement of the per-PR policy: a
version-and-changelogjob in.github/workflows/ci.ymlfails the PR check ifpyproject.tomlversion is unchanged orCHANGELOG.mdis unmodified vs the base branch. - GuardDog supply-chain scan in CI:
guarddog pypi verifyscans the resolved dependency closure (frompip freeze --exclude-editable) for known-malicious PyPI packages, andguarddog github_action verifyscans the workflow file for malicious GitHub Actions patterns. GuardDog and its full transitive dependency closure are pinned with SHA-256 hashes in.github/guarddog-requirements.txt(generated bypip-compile --generate-hashes); CI installs viapip install --require-hashes -r ...so any tampered artifact in the closure fails the install.
Changed
Section titled “Changed”src/xdr_cli/__init__.pyreads__version__dynamically from package metadata viaimportlib.metadata.version("xdr-cli"), with aPackageNotFoundErrorfallback to"0.0.0+unknown"for source-tree-only execution.pyproject.tomlis now the single source of truth.README.mdgained a “Contributing” section linking to the new docs and toSECURITY.md.AGENTS.md§13 (Links) gained a cross-link toCONTRIBUTING.md§7 (the AI-agent contribution rules)..gitignoreextended to cover.claude/,.cursor/,.windsurf/alongside the existing.codexentry.