Skip to content

Invariants (policy as code)

An invariants file declares the state the team expects. Anything that drifts from it becomes an AD-INV-* finding, so a review decision ("tier 0 is exactly these people") is enforced on every scan instead of living in a wiki.

ad-doctor scan dc01.corp.example --ldaps -u 'CORP\auditor' --invariants invariants.toml
ad-doctor analyze 2026-11.json --invariants invariants.toml

Every section is optional. See examples/invariants.toml.

[groups]
# exact membership (sAMAccountName of the users, transitive)
"Domain Admins" = ["Administrator", "adm_alice", "adm_bob"]
"Schema Admins" = []
"Enterprise Admins" = []

[limits]
tier0_users = 5            # at most this many distinct accounts across all tier-0 groups

[settings]
machine_account_quota = 0
min_pwd_length = 14        # at least
lockout_threshold = 10     # at least
recycle_bin = true
ldap_signing_required = true

[dcsync]
# principals allowed to replicate secrets besides the domain controllers and tier 0
allowed = ["MSOL_0123456789ab"]

[templates]
# the only certificate templates that may be published
published = ["User", "Machine", "DomainController", "KerberosAuthentication", "WebServer", "SubCA"]

The invariant checks

ID Fires when
AD-INV-001 a declared group's membership differs from the list (missing or unexpected members)
AD-INV-002 more distinct accounts sit in tier-0 groups than limits.tier0_users allows
AD-INV-003 a declared domain setting does not hold
AD-INV-004 a principal outside the declared list can DCSync
AD-INV-005 a certificate template is published that the declared list does not allow

These IDs are not in the normal check registry; they run only with --invariants. When one fires it appears as a SARIF rule and a JUnit test case like any other finding (a passing invariant adds neither), and each has a catalog page.