Skip to content

Releases

ad-doctor 0.2.6

Choose a tag to compare

@brkozer brkozer released this 09 Nov 22:58

Forest scanning.

  • collect --forest -o DIR and scan --forest -o DIR find the forest's domains from the crossRef objects and scan them one at a time, writing one ordinary snapshot per domain and a forest.json manifest.
  • rollup --forest DIR shows the per-domain results, the domains that could not be read, and who holds Enterprise Admins and Schema Admins across domains.
  • A missing domain is never a clean result. A domain that cannot be read is listed as not evaluated with the reason, and the run exits 3, even when --fail-on also matched.
  • Without --forest, nothing changes.

Where your credentials go. A forest scan binds to domain controllers named in the Configuration partition, which is more than the one host you named. Before any bind, a name must be a plain DNS name inside its own domain; --forest-allow-dns SUFFIX replaces that rule for disjoint namespaces. Forest connections do not follow LDAP referrals, and a refused bind stops at the first DC of that domain to avoid lockouts. Without --forest-ca-file PEM, --ldaps does not authenticate the servers it finds. See docs/forest.md.

What is not joined. Only those two groups are followed across domains. Control paths, findings and scores stay per domain, so a path that crosses a domain boundary is not shown.

How far this is tested. The lab has one domain: there, scan --forest gives the same findings as a normal scan. Child domains, several trees, choosing and binding another domain's DC, and following members across domains have run only against fake forests and scripted LDAP servers, not against a real multi-domain forest. Reports from real forests are especially welcome.

Install:

uv tool install ad-doctor

Full list of changes: CHANGELOG.md

ad-doctor 0.2.5

Choose a tag to compare

@brkozer brkozer released this 09 Nov 22:02

Every check now says whether it could judge what it covers, you can declare your own tier 0, and 27 new checks (125 to 152).

Check status. Each check is judged, not applicable, or not evaluated with the reason (data unreadable, older snapshot, switched off). It is in the JSON report, text, Markdown, SARIF, JUnit and the viewer. A check that was not evaluated never counts as passed, and the score is labelled a minimum when checks were held back.

The JSON report schema goes from 2 to 3, with new check_status and coverage keys.

Declared tier 0. --tier0-file and --tier0 add your own assets on analyze, scan, paths, view, collect, whatif and plan. Built-in tier 0 is never altered, and paths to a declared asset are marked. An asset whose owner and DACL are not in the snapshot is reported as not evaluated.

New checks

  • Tier-0 logon restrictions per OU, authentication policy silos, and recent tier-0 changes from replication metadata (members added, AdminSDHolder's ACL, sIDHistory)
  • Configuration Manager, AD FS DKM, Entra Cloud Sync and connector accounts
  • Schema, Configuration and OU extended-right ACLs
  • Scripts and tasks run by Group Policy
  • SMB signing on tier-0 member servers
  • RID 500 use, smart-card account hash age, mail on tier-0 accounts, group loops
  • Weak CA certificates, display specifiers, dynamic objects
  • Patch level of DCs and CAs against a dated table (patches.toml), a version match only, with --no-patch-level to turn it off

New options and commands

  • --dc-probes (off by default): WebClient, SMBv1 and LDAPS TLS versions on domain controllers
  • --preflight FILE: a read-only script to run before the first 10 fixes
  • ad-doctor rollup: one view over the JSON reports of several domains
  • ad-doctor service-accounts and ad-doctor tier-scorecard

Also changed: on a snapshot whose control graph is incomplete, such as one from an older version, plan writes no [expect], and whatif --check fails any limit written by hand.

How far this is tested. Most positive cases of the new checks are unit tests, because the lab domain does not contain them and was not changed to. Reports from real domains are welcome. The patch table is maintained by hand and says when it is old.

Install:

uv tool install ad-doctor

Full list of changes: CHANGELOG.md

ad-doctor 0.2.4

Choose a tag to compare

@brkozer brkozer released this 09 Nov 20:52

What-if in the browser, and seven checks on Active Directory-integrated DNS (118 to 125 checks).

Interactive what-if. With the full graph embedded, the What-if screen of ad-doctor view lets you turn each change on and off. Exposure, holders, routes and the paths closed, rerouted, open and new follow live, computed in the page on the same model as whatif and checked against it. Add changes from the steps and chokepoints on Control paths, compare with the plan in the file, and download the selection as a plan.toml that whatif --plan accepts.

  • Changes that need a rebuilt graph are unavailable, with the reason, and get no number.
  • Findings are not rechecked in the browser, and the page says so.
  • Above the full-graph limit (50,000 directory objects) there is no interactive what-if.

New DNS checks

Check Finds
AD-DNS-001 Zones that accept nonsecure dynamic updates
AD-DNS-002 Write control over a zone outside tier 0 and DnsAdmins
AD-DNS-003 Broad groups that can add records (the default, info only)
AD-DNS-004 Names carrying marshaled target information, used for DNS-based coercion
AD-DNS-005 Wildcard records
AD-DNS-006 wpad, isatap or autodiscover owned by an ordinary account
AD-DNS-007 SPN hosts with no DNS record, in zones where a low-privileged user can add one

No zone transfer is attempted. An unreadable zone or partition is reported as not evaluated, not as passing, and older snapshots say the same.

How far this is tested. On the lab only AD-DNS-003 fires, which is the default state. The positive cases of the other six checks are unit tests and have not fired on a live domain. Reports from real domains are welcome.

Install:

uv tool install ad-doctor

Full list of changes: CHANGELOG.md

ad-doctor 0.2.3

Choose a tag to compare

@brkozer brkozer released this 09 Nov 13:06

ad-doctor now reads what Group Policy sets in the registry, with Group Policy precedence. Nine new checks, 109 to 118.

Reader. Machine\Registry.pol, audit.csv and Preferences\Registry\Registry.xml from SYSVOL, resolved per computer by link order, Enforced, Block Inheritance and security filtering. Where the winner cannot be decided, for example a WMI filter, the value stays undecided with the reason instead of being assumed.

New checks

Check Finds
AD-GPO-009 LLMNR not turned off by policy
AD-GPO-010 WSUS over HTTP
AD-GPO-011 PowerShell script block or module logging not set
AD-GPO-012 UNC hardened paths missing or weak
AD-GPO-013 Domain controller audit policy against Microsoft's baseline
AD-GPO-014 DefaultPassword set through Group Policy
AD-GPO-015 BlockNtlmv1SSO turned off
AD-GPO-016 Weak DefaultDomainSupportedEncTypes on domain controllers
AD-ADCS-020 ESC10: weak certificate mapping as far as Group Policy shows it

What these checks can and cannot see. Group Policy not setting a value says nothing about the host's own registry, so most "not set" results are low or info and worded that way. ESC10 is reported as unknown when no GPO sets the values, because they are commonly set locally. Secret values never leave the parser: only the fact and the location are kept.

How far this is tested. The lab's GPOs carry no Registry.pol data, so the parser has run only on bytes built from Microsoft's format description, and the positive cases are unit tests. Reports from real domains are welcome. Microsoft's documents disagree on the order of two links in the same OU; the code follows New-GPLink and the Group Policy scope documentation, and this matters only when two such links set different values.

Older snapshots still load; these checks report as not evaluated.

Install:

uv tool install ad-doctor

Full list of changes: CHANGELOG.md

ad-doctor 0.2.2

Choose a tag to compare

@brkozer brkozer released this 09 Nov 00:15

Ten new checks and new kinds of edge in the control graph. Control-path results can change: a domain with delegation or SID history paths may show more paths than it did on 0.2.1.

New edges in the control graph (paths, blast radius, plan, whatif, the viewer and the OpenGraph export)

  • Constrained delegation with protocol transition
  • Resource-based constrained delegation
  • SID history
  • ESC13, and ESC6 together with ESC16

An edge is drawn only when the attack works on the data the snapshot holds. ESC9, ESC15 and ESC6 without ESC16 stay findings rather than edges, because they depend on patch state the snapshot does not have. With several enterprise CAs no ESC6+ESC16 edge is drawn. A plan says "proven minimum" only when the model is exact; where flags shared between gates make it inexact, it gives a lower bound instead.

New checks (99 to 109)

Check Finds
AD-ACC-014 Stale enabled computers
AD-ACC-015 gMSAs with a long password interval
AD-ACC-016 Identity collisions: UPN and sAMAccountName, invisible Unicode in SPNs, dNSHostName mismatch
AD-ACL-009 Control over dMSAs and their superseded accounts from outside tier 0
AD-INF-018 Old domain controller machine passwords
AD-INF-019 SYSVOL still replicated by FRS
AD-INF-020 Site and subnet problems
AD-INF-021 Conflict objects, shared SIDs and names
AD-KRB-007 Age of the Entra Kerberos key
AD-POL-003 Content of fine-grained password policies

Also changed

  • JUnit output reports checks that were not judged as skipped, and SARIF carries the notes that say why.
  • Older snapshots still load; the new checks and RBCD edges report as not evaluated.

How far this is tested: the positive cases of the new checks and the edges that reach tier 0 are covered by unit tests. The lab domain does not contain them, so they have not fired on a live domain. Reports from real domains are welcome.

Install:

uv tool install ad-doctor

Full list of changes: CHANGELOG.md

ad-doctor 0.2.1

Choose a tag to compare

@brkozer brkozer released this 08 Nov 21:18

Fixes from a review of the what-if work in 0.2.0. Upgrade if you use [expect] closes_all in CI or view --whatif.

[expect] closes_all is now judged on the graph after the changes. In 0.2.0 it could pass when a change took a user out of tier 0 who still had a path, and fail when a route ended at that user.

What-if screen in the viewer

  • A result that failed --check now says so, with its problems and each change's plan status. In 0.2.0 it looked like a healthy result.
  • Rerouted paths no longer show as blank rows.
  • With --fixes, each change's label and fix lines are built from the snapshot for its ID and never read from the JSON. An ID the snapshot does not have is refused.
  • A malformed --whatif file is refused with a message instead of a traceback or a blank screen.
  • The screen states the plan's goal, says when findings were not rechecked, marks absent changes and shows the blast radius of --account. The About screen lists the embedded plan. Long names no longer widen the page on a phone.

plan

  • --from NAME counts the routes through the account's implicit groups, such as Domain Users.
  • --keep and --allow KIND no longer write a closes_all that their own plan fails.

Install:

uv tool install ad-doctor

Full list of changes: CHANGELOG.md

ad-doctor 0.2.0

Choose a tag to compare

@brkozer brkozer released this 08 Nov 01:40

What-if: see which changes close the control paths to tier 0 before touching AD.

  • ad-doctor plan computes the cheapest set of removals that closes every control path ad-doctor sees (a minimum cut), says "proven minimum" only when it can prove it, lists the sources no removal can close, and orders the changes in steps. --max-changes gives the best it can do within a budget (heuristic, labelled as such).
  • ad-doctor whatif simulates removals named by change ID, finding fingerprint, path step, chokepoint or spec: paths closed, rerouted or still open, exposure before and after, blast radius, and the findings that would clear or appear, rechecked on the changed snapshot.
  • Plan file (TOML) tracks each change as done, open, overdue, stale or ineffective against a newer snapshot. whatif --plan plan.toml --check exits 2 when its [expect] block fails and 3 when the plan has gone stale, for CI.
  • Fix lines as PowerShell with -WhatIf on every writing line, plus a rollback line per change.
  • Viewer: view --whatif plan.json adds a What-if screen with the steps, change IDs, fix lines and before/after numbers.
  • Snapshots keep the INHERITED_ACE flag. 0.1.5 still reads a snapshot with no inherited ACE.

Nothing is applied to AD: every result is a simulation on the snapshot. On the lab, the plan takes exposed users from 25 of 26 to 1 of 26 with 15 changes (proven minimum). analyze and paths output is byte for byte that of 0.1.5.

Install:

uv tool install ad-doctor

Docs: https://brkozer.github.io/ad-doctor/whatif/

Full list of changes: CHANGELOG.md

ad-doctor 0.1.5

Choose a tag to compare

@brkozer brkozer released this 07 Nov 00:06

Changed

  • Less memory. Four new Account fields in 0.1.0 took it to 30 attributes, where
    CPython stops sharing dict keys, so every account got a full dict of its own: 1.3 KB more
    each on Python 3.12. Account, DirObject and Ace now use slots. view lets go of
    the parsed file once the snapshot is built, loading shares equal ACEs and member DNs, the
    control graph shares node strings, to_json no longer makes a deep copy first, OpenGraph
    edges share repeated dicts, and export-graph writes the file in pieces. On the
    250,000-user bench domain on Python 3.12, view peaks at 1.43 GB instead of 2.35 GB,
    scripts/bench.py at 2.1 GB instead of about 4.9 GB, and export-graph at 1.8 GB where
    it did not fit in 5 GB before. Findings, fingerprints and every report and export are byte
    for byte the same.
  • redact no longer slows down with the square of the domain. Each word is looked up
    instead of one pattern of every name running over every string: 1.4 s instead of 40 s at
    5,000 users, 4 s at 20,000, and 49 s with a 3.0 GB peak at 250,000 users, where it could
    not finish before.
  • GPO reach. A disabled side stays in with a caveat, because the writer can flip it. A
    filter stops a writer of values, but an owner or DACL writer gets a path through a node
    GPO ... with its filter changed. User-side reach needs some computer able to read the
    GPO, as Windows has required since MS16-072. A filter on a group the snapshot does not
    hold, such as another domain's, is assumed to apply, with a caveat and a note.
  • AD-ACC-013. Any key on a DC or RODC is high, and so is a second key on a computer. CA
    servers and Entra Connect sync accounts and servers are read too. A short hash now tells
    keys and mappings apart, so the object text and identities of this check differ from 0.1.2.
  • AD-DEL-005. Graded by the delegating account rather than the host: medium, or high with
    protocol transition. sPNMappings changes only the service class, so ports and instance
    names are kept. Entries for another domain get their own wording.
  • AD-ADCS-019. A URL the directory lists that refuses or answers with anything but 401
    gets a note that EPA and NTLM were not judged.

Fixed

  • Redaction. A comma in a name defeated it: Doe\, John stayed readable in every DN, and
    so did any name with a dotted capital I. Also covered now: enrollment URLs that exist only
    in a note, user names and passwords in such URLs, service classes outside a default
    sPNMappings, delegation targets with no /, and text around the tags of a malformed
    altSecurityIdentities value. An IP address in an enrollment URL no longer makes the output
    impossible to load, a host label such as group no longer changes findings, and the Entra
    Kerberos trust stays skipped after redaction.
  • --no-probes also skips the LDAP signing bind, as the docs said. AD-INF-005 then says in
    a note that it did not judge. The SYSVOL read has its own --no-sysvol; the docs no longer
    put it under --no-probes.
  • A control path now says in its text what an edge assumes, such as a WMI filter or a
    disabled side, instead of only in the OpenGraph export. Fingerprints are unchanged.
  • A host with a space in msPKI-Enrollment-Servers no longer ends the scan with a traceback.
  • Text reports show control characters in directory names as escapes, and Markdown reports
    escape < outside code spans.

ad-doctor 0.1.4

Choose a tag to compare

@brkozer brkozer released this 06 Nov 23:57

Added

  • Trust hygiene: AD-INF-015, AD-INF-016, AD-INF-017. msDS-SupportedEncryptionTypes and
    whenChanged are now read on trusts. AD-INF-015 reports downlevel (NT4 style) trusts and
    inbound trusts without AES, at low when AES is merely not set. AD-INF-016 reports trusts
    unchanged for over 40 days, the limit of PingCastle's T-Inactive, at low for realm trusts.
    AD-INF-017 reports outgoing forest trusts without Selective Authentication (DISA V-243485).
    The Entra Kerberos trust is skipped. AD-INF-006 keeps SID filtering and TGT delegation, so
    nothing is reported twice. An older snapshot says in a note what was not evaluated.
  • AD-DEL-005: constrained delegation to SPNs no account holds. A target counts as held
    when an account has the exact SPN, or the SPN its class maps to through sPNMappings, which
    is now collected. Otherwise the host decides: no computer object is medium, or high with
    protocol transition; a host that exists is low; a host outside the scanned domain is low
    with review wording. No DNS query is made. redact now also covers the hosts of SPNs and
    delegation targets, which need not belong to a computer in the snapshot.

ad-doctor 0.1.3

Choose a tag to compare

@brkozer brkozer released this 06 Nov 23:55

Added

  • AD-ADCS-019: HTTPS enrollment endpoints where EPA is not verified. The enrollment probe
    now also asks /certsrv/ over HTTPS and every URL in msPKI-Enrollment-Servers (CES) for
    their authentication methods, with one unauthenticated GET each. An endpoint that offers
    NTLM or Negotiate over HTTPS is listed with EPA as unknown, at low: IIS shows EPA only to a
    client that authenticates, and this probe sends no credentials, so a listed endpoint may or
    may not be relayable and an unlisted one is not proven safe. A CES URL over plain HTTP
    counts for AD-ADCS-010 and AD-CHAIN-003. New option --https-port (443, 0 skips it). An
    older snapshot says in a note that this was not evaluated.