#!/usr/bin/env python3 """Read-only LDAP audit of an Active Directory domain. Connects with a basic (non-admin) bind account and enumerates OUs, groups, users, and computers to produce a structural/health report. Designed to work with whatever a standard authenticated-user account can see over LDAP -- no elevated rights required. """ import argparse import datetime import getpass import json import re import sys from collections import Counter, defaultdict from ldap3 import ALL, SUBTREE, Connection, Server, Tls import ssl # --- UserAccountControl bit flags (subset relevant to an audit) --- UAC_ACCOUNTDISABLE = 0x0002 UAC_LOCKOUT = 0x0010 UAC_PASSWD_NOTREQD = 0x0020 UAC_DONT_EXPIRE_PASSWD = 0x10000 UAC_SMARTCARD_REQUIRED = 0x40000 UAC_TRUSTED_FOR_DELEGATION = 0x80000 UAC_NOT_DELEGATED = 0x100000 UAC_DONT_REQ_PREAUTH = 0x400000 FILETIME_EPOCH = datetime.datetime(1601, 1, 1) def filetime_to_datetime(value): try: v = int(value) except (TypeError, ValueError): return None if v == 0 or v == 0x7FFFFFFFFFFFFFFF: return None try: return FILETIME_EPOCH + datetime.timedelta(microseconds=v / 10) except OverflowError: return None def md_escape(value): """Escape a value before it goes into a Markdown table cell. AD attributes (descriptions, sAMAccountName, OS strings, DNs, ...) are directory content, not report-generated text -- they can contain anything a writer or an attacker put there, including raw HTML. Markdown doesn't escape inline HTML by default, so an unescaped '