Active Directory Vulnerability Scanner is a modular, Python-based tool that connects to domain controllers via LDAP, LDAPS, and/or SMB, runs a comprehensive battery of security checks, and produces a self-contained HTML dashboard report with a risk score and letter grade.
- Multi-protocol: LDAP, LDAPS, SMB (user-selectable; defaults to all three)
- Flexible auth: password, pass-the-hash (NTLM
LM:NTorNT), Kerberos ccache, or interactive prompt - 40+ security checks covering critical AD attack surfaces across eight categories
- BloodHound integration: automatic AD graph data collection saved to
Reports/Artifacts/ - Risk score: ratio-based scoring per category, overall score maps to letter grade A–F
- HTML report: self-contained, light/dark mode, severity filter chips, collapsible finding cards with remediation guidance
- Multiple export formats: HTML, JSON, CSV, DOCX
- Modular: drop a new
check_*.pyinchecks/and it is auto-discovered at runtime - Scoring customisation: tune deductions per finding via
scoring.tomlwithout touching check code
Before installing ADScan, ensure the following are available on your system:
| Prerequisite | Version | Purpose |
|---|---|---|
| Python | 3.10+ | Core runtime |
| Git | any | Required to install pre2k from GitHub |
| uv | any | Installs external CLI tools into isolated venvs |
Certipy (ADCS checks) requires Python 3.12+ in its own isolated venv. uv handles this automatically — it is not a requirement for ADScan itself.
Install uv if you do not have it:
curl -LsSf https://astral.sh/uv/install.sh | shgit clone https://github.com/dehobbs/ADScan.git
cd ADScan
# Option A — uv (recommended)
uv venv && source .venv/bin/activate
uv pip install -e .
# Option B — pip
python3 -m venv .venv
source .venv/bin/activate
pip install -e .Kali / externally-managed Python: always create a virtual environment first to avoid the "externally managed environment" pip error.
Some checks invoke external tools as subprocesses. Each tool is installed into
its own isolated virtual environment via uv tool install so its
dependencies never conflict with ADScan's packages.
One-command setup (installs all tools at once):
python adscan.py --setup-toolsOr install individually:
uv tool install certipy-ad # ADCS/PKI scanner (Python 3.12+ venv)
# NetExec — recommended install via pipx (or apt on Kali/ParrotSec):
apt install netexec # Kali / ParrotSec / BlackArch
# or:
pipx install git+https://github.com/Pennyw0rth/NetExec # other systems
uv tool install git+https://github.com/garrettfoster13/pre2k.git # Pre-Windows 2000 account tester
uv tool install bloodhound # BloodHound AD ingestorTools are optional. If a tool is missing when a check runs, ADScan will attempt to auto-install it via
uv tool install. Ifuvis not on PATH, the check is skipped gracefully with an informational finding.
pip install -e ".[kerberos]" # gssapi bindings for Kerberos over LDAP
pip install -e ".[dev]" # pytest, ruff, mypy for developmentpython adscan.py --list-checks # should print all 40+ checks
python adscan.py --setup-tools # installs / verifies all external tools# Password authentication
python adscan.py -d corp.local --dc-ip 10.10.10.5 -u alice -p 'P@ssw0rd!'
# Interactive password prompt (no -p flag — password is not echoed)
python adscan.py -d corp.local --dc-ip 10.10.10.5 -u alice
# Pass-the-hash
python adscan.py -d corp.local --dc-ip 10.10.10.5 -u alice \
--hash aad3b435b51404eeaad3b435b51404ee:8846f7eaee8fb117ad06bdd830b7586c
# Kerberos ticket reuse (KRB5CCNAME set in environment)
export KRB5CCNAME=/tmp/alice.ccache
python adscan.py -d corp.local --dc-ip 10.10.10.5 -u alice --kerberos
# All output formats
python adscan.py -d corp.local --dc-ip 10.10.10.5 -u alice -p 'P@ssw0rd!' --format all
# Run only Kerberos-related checks
python adscan.py -d corp.local --dc-ip 10.10.10.5 -u alice -p 'P@ssw0rd!' --checks kerberospython adscan.py -d <domain> --dc-ip <dc_ip> -u <user> -p <pass> [options]
| Flag | Description | Default |
|---|---|---|
-d / --domain |
Target domain FQDN (e.g. corp.local) |
required |
--dc-ip |
Domain controller IP or hostname | required |
-u / --username |
Username for authentication | required |
-p / --password |
Password (omit to be prompted securely) | — |
--hash LM:NT |
NTLM hash; LM:NT or NT-only. Mutually exclusive with -p and --kerberos |
— |
--kerberos |
Use Kerberos ticket from KRB5CCNAME env var |
— |
--ccache PATH |
Path to Kerberos ccache file (implies --kerberos) |
— |
--protocol |
ldap | ldaps | smb | all |
all |
--timeout SEC |
Connection timeout in seconds | 30 |
| Flag | Description | Default |
|---|---|---|
-o / --output |
Report path stem (extension added automatically) | Reports/adscan_report_<timestamp> |
--output-dir DIR |
Directory for all report files | Reports/ |
--format |
html | json | csv | docx | all |
html |
--log-file PATH |
Write full DEBUG log to a file in addition to console | — |
-v / --verbose |
Show DEBUG-level detail on the console | off |
| Flag | Description |
|---|---|
--checks SLUG[,…] |
Run only the specified checks (comma-separated slugs) |
--skip SLUG[,…] |
Exclude specific checks |
--list-checks |
Print all available checks with slugs, categories, and weights, then exit |
| Flag | Description |
|---|---|
--setup-tools |
Install all external CLI tools via uv tool install and exit |
--scoring-config PATH |
Use a custom TOML scoring config instead of scoring.toml |
python adscan.py -d corp.local --dc-ip 10.10.10.5 -u alice -p 'P@ssw0rd!'
# Or prompt securely:
python adscan.py -d corp.local --dc-ip 10.10.10.5 -u alicepython adscan.py -d corp.local --dc-ip 10.10.10.5 -u alice \
--hash aad3b435b51404eeaad3b435b51404ee:8846f7eaee8fb117ad06bdd830b7586c
# NT hash only (blank LM):
python adscan.py -d corp.local --dc-ip 10.10.10.5 -u alice \
--hash 8846f7eaee8fb117ad06bdd830b7586cKerberos ticket reuse is useful when NTLM is disabled or in assumed-breach scenarios.
# Obtain a TGT with impacket
getTGT.py corp.local/alice:'P@ssw0rd!' -dc-ip 10.10.10.5
export KRB5CCNAME=$(pwd)/alice.ccache
# Use the ticket
python adscan.py -d corp.local --dc-ip 10.10.10.5 -u alice --kerberos
# Or supply the ccache path explicitly
python adscan.py -d corp.local --dc-ip 10.10.10.5 -u alice --ccache /tmp/alice.ccacheKerberos over LDAP requires the
gssapipackage (pip install -e ".[kerberos]") and a valid/etc/krb5.conf. Kerberos over SMB is handled natively by impacket.
Checks are auto-discovered from checks/check_*.py. Run --list-checks to
see all available checks with slugs for use with --checks and --skip.
| Check | What it tests |
|---|---|
| Domain Password Policy | Minimum length, complexity, lockout, max age |
| Account Hygiene | Stale, never-logged-on, and PASSWD_NOTREQD accounts |
| Computer Account Password Age | Machine passwords not rotated in > 30 days |
| Pre-Windows 2000 Computer Accounts | Computer accounts with predictable default passwords (CVE prerequisite) |
| Passwords in Descriptions | Cleartext credentials stored in AD description fields |
| SID History | Accounts with SID history that could allow privilege escalation |
| Shadow Credentials | msDS-KeyCredentialLink attribute abuse |
| Check | What it tests |
|---|---|
| Kerberos Attack Surface | AS-REP roasting, Kerberoasting, DES encryption, pre-auth |
| Unconstrained Delegation | User and computer accounts with unconstrained delegation |
| Constrained Delegation | S4U2Self/S4U2Proxy delegation with protocol transition |
| RC4 / Legacy Kerberos Encryption | RC4 and DES encryption permitted on accounts and DCs |
| RBCD on Domain Object / DCs | Resource-based constrained delegation misconfiguration |
| NoPac (CVE-2021-42278/42287) | PAC validation bypass — checks all DCs via nxc nopac module |
| Check | What it tests |
|---|---|
| Privileged Accounts | krbtgt age, built-in admin, DA count, stale privileged accounts |
| LAPS Deployment | Legacy and Windows LAPS schema and coverage |
| LAPS Coverage | Per-OU LAPS coverage gaps |
| ACL / Permissions | DCSync rights, GenericAll/Write/Owner over high-value objects |
| AdminSDHolder ACL | Non-standard ACEs on the AdminSDHolder object |
| Service Accounts | User accounts used as service accounts lacking gMSA adoption |
| Protected Admin Users | High-privilege accounts missing Protected Users group membership |
| Protected Users Group Membership | Protected Users group population |
| Check | What it tests |
|---|---|
| SMB Signing Enforcement | SMB signing enforcement and SMBv1 detection across all hosts |
| Legacy Protocols | NTLM, WDigest, LLMNR, NetBIOS-NS configuration |
| Protocol Security | LDAP signing and channel binding enforcement |
| Check | What it tests |
|---|---|
| Domain Controllers | DC count, OS versions, FSMO roles, AD recycle bin |
| Domain Trusts | Trust type, direction, SID filtering, transitivity |
| Group Policy Objects | Unlinked GPOs, empty GPOs, GPO permissions |
| GPP / cpassword (MS14-025) | Cleartext passwords in SYSVOL Group Policy Preferences |
| Optional Features | AD Recycle Bin, PAM feature status |
| Replication Health | Site link intervals, replication failures |
| Miscellaneous Hardening | Machine account quota, null sessions, guest account |
| Legacy FRS SYSVOL | FRS vs DFSR replication status |
| Pre-Windows 2000 Compatible Access | Group membership (Everyone, Anonymous, Authenticated Users) |
| BloodHound Data Collection | Full AD graph snapshot — ZIP saved to Reports/Artifacts/ |
| Check | What it tests |
|---|---|
| ADCS / PKI Vulnerabilities | ESC1–ESC16 certificate template and CA misconfigurations |
| Check | What it tests |
|---|---|
| DNS & Infrastructure | Wildcard DNS records, AD-integrated DNS hygiene |
| Orphaned AD Subnets | Subnets defined in Sites & Services with no site assigned |
| Check | What it tests |
|---|---|
| Foreign Security Principals in Privileged Groups | Cross-forest principals in DA/EA/BA/Schema Admins |
| Check | What it tests |
|---|---|
| Advanced Audit Policy | Presence and coverage of advanced audit subcategories |
# List all checks and their slugs
python adscan.py --list-checks
# Run only Kerberos checks
python adscan.py -d corp.local --dc-ip 10.10.10.5 -u alice -p 'P@ss' --checks kerberos
# Run ADCS and delegation checks only
python adscan.py -d corp.local --dc-ip 10.10.10.5 -u alice -p 'P@ss' --checks adcs,delegation
# Skip SMB (firewalled) and DNS
python adscan.py -d corp.local --dc-ip 10.10.10.5 -u alice -p 'P@ss' --skip smb,dns
# Skip BloodHound collection (time-sensitive engagement)
python adscan.py -d corp.local --dc-ip 10.10.10.5 -u alice -p 'P@ss' --skip bloodhoundSlugs are matched against the module filename (minus check_ prefix), words
in CHECK_NAME, and CHECK_CATEGORY values — so kerberos, delegation,
and roasting all match relevant checks.
When the bloodhound tool is installed, ADScan automatically collects a full
AD graph snapshot during the scan using bloodhound-python. The resulting
ZIP archive is saved to Reports/Artifacts/ alongside other scan artifacts.
The collection runs last (after all security checks) and uses the same
credentials supplied to ADScan. When --dc-ip is an IP address, ADScan
automatically resolves the DC FQDN via a DNS SRV lookup
(_ldap._tcp.dc._msdcs.<domain>) so bloodhound-python receives the
hostname it requires.
Import the ZIP into the BloodHound application to visualise attack paths, shortest paths to Domain Admin, and Kerberos delegation chains.
To skip BloodHound collection:
python adscan.py ... --skip bloodhoundReports are written to Reports/ by default.
| Format | Flag | Description |
|---|---|---|
| HTML | --format html (default) |
Self-contained dashboard — embedded CSS/JS, light/dark mode, severity filters |
| JSON | --format json |
Machine-readable findings list |
| CSV | --format csv |
Flat finding rows for spreadsheet analysis |
| DOCX | --format docx |
Word document with findings and command output evidence |
| All | --format all |
Produces all four formats in one run |
Supporting artifacts (nxc output, BloodHound ZIP, Certipy JSON) are written
to Reports/Artifacts/.
ADScan uses a ratio-based model, not a simple deduction from 100:
- Each check declares a weight (maximum points it can contribute)
- A clean check earns its full weight; findings reduce earned points by their deduction
- Category score = earned / possible × 100 (per category)
- Overall score = total earned / total possible × 100
| Score | Grade |
|---|---|
| 90–100 | A |
| 75–89 | B |
| 60–74 | C |
| 40–59 | D |
| 0–39 | F |
| Severity | Deduction |
|---|---|
| Critical | 20 pts |
| High | 15 pts |
| Medium | 8 pts |
| Low | 5 pts |
| Info | 0 pts |
Edit scoring.toml or create a scoring.local.toml (gitignored) for
engagement-specific overrides:
[severity_weights]
critical = 25
high = 15
medium = 5
low = 2
info = 0
[overrides]
# Raise impact
"User Accounts with Unconstrained Delegation" = 25
# Suppress from score (still appears in report)
"Lockout Observation Window Too Short" = 0# Use a custom scoring file
python adscan.py ... --scoring-config engagement_scoring.tomlADScan uses several external CLI tools for checks that require network
enumeration or specialised analysis. Each is installed in its own isolated
Python virtual environment via uv tool install.
| Slug | Command | Package | Checks That Use It |
|---|---|---|---|
certipy |
certipy |
certipy-ad (PyPI) |
ADCS / PKI (ESC1–ESC16) |
nxc |
nxc |
netexec — see install guide |
SMB signing, SMBv1, NoPac |
pre2k |
pre2k |
GitHub: garrettfoster13/pre2k |
Pre-Windows 2000 computer accounts |
bloodhound |
bloodhound-python |
bloodhound (PyPI) |
BloodHound data collection |
Install all tools at once:
python adscan.py --setup-toolsNetExec note: The recommended install is
apt install netexec(Kali/ParrotSec) orpipx install git+https://github.com/Pennyw0rth/NetExec(other systems). Ifnxcis already on PATH, ADScan will use it directly — no uv required.
For all other tools,
uvmust be on PATH for--setup-toolsauto-install. If uv is not available, install the tool manually and ensure its executable is on PATH.
ADScan/
├── adscan.py # CLI entry point and orchestration loop
├── scoring.toml # Default severity weights and per-finding overrides
├── scoring.local.toml # Local overrides — gitignored, safe for engagement use
├── requirements.txt # Pinned dependencies
├── pyproject.toml # Package metadata and tool config
├── lib/
│ ├── connector.py # ADConnector — LDAP/LDAPS/SMB connection manager
│ ├── tools.py # External CLI tool manager (uv-based isolation)
│ ├── scoring.py # Ratio-based scoring engine
│ ├── report.py # HTML / JSON / CSV / DOCX report generation
│ ├── audit_log.py # Structured audit trail
│ └── debug_log.py # LDAP query debug log
├── checks/ # Security check modules (auto-discovered)
├── verifications/ # Manual verification and remediation guidance
├── Reports/ # Generated reports (gitignored)
│ └── Artifacts/ # Tool output files, BloodHound ZIP
└── Logs/ # Audit and debug logs (gitignored)
Create checks/check_<name>.py with the following module-level attributes and function:
CHECK_NAME = "Human-Readable Check Name"
CHECK_ORDER = 50 # controls execution order (lower runs first)
CHECK_CATEGORY = ["Category Name"]
CHECK_WEIGHT = 15 # max points this check contributes to the score (0 = info-only)
def run_check(connector, verbose=False):
findings = []
# ... query connector.ldap_search(), connector.smb_available(), etc.
# Return [] for a clean result, or append finding dicts for issues found.
findings.append({
"title": "Short Finding Title",
"severity": "high", # critical / high / medium / low / info
"deduction": 15, # points deducted (overridden by scoring.toml if set)
"description": "What was found and why it matters.",
"recommendation": "What to do about it.",
"details": ["optional list", "of detail strings"],
})
return findingsNo registration needed — the file is auto-discovered on the next run.
Create verifications/verify_<name>.py to add manual verification steps and
remediation guidance to the finding cards in the HTML report.
MATCH_KEYS = ["short finding title"] # lowercase substrings matched against finding titles
TOOLS = [
{
"tool": "NetExec", # display name shown on the card header
"icon": "netexec", # netexec | impacket | ps | cmd
"desc": "One-line description of what this command does.",
"code": "nxc smb <DC> -u <user> -p <pass> --shares",
"confirm": "Expected output or what to look for.",
},
{
"tool": "PowerShell",
"icon": "ps",
"steps": ["Open an elevated PowerShell prompt.", "Run the command below."],
"code": "Get-ADUser -Filter * -Properties PasswordNeverExpires | ...",
"confirm": "Accounts listed here should be reviewed.",
},
]
REMEDIATION = {
"title": "Brief remediation approach",
"steps": [
{"text": "Step one description.", "code": "optional command or script"},
{"text": "Step two description.", "steps": ["sub-step A", "sub-step B"]},
],
}
REFERENCES = [
"https://example.com/relevant-article",
]Icon values control which tab a tool card appears under in the report:
netexecorimpacket→ Linux tabps,cmd, or anything else → Windows tab
Matching: MATCH_KEYS entries are matched as lowercase substrings against
finding titles. The first match wins, so use specific enough strings to avoid
collisions with other findings.
If the check invokes an external CLI tool, add a ToolSpec entry to
TOOL_REGISTRY in lib/tools.py and call ensure_tool() in the check:
# lib/tools.py — add to TOOL_REGISTRY
"mytool": ToolSpec(
package="mytool-package",
exe="mytool",
description="What this tool does",
),# checks/check_<name>.py
from lib.tools import ensure_tool
def run_check(connector, verbose=False):
exe = ensure_tool("mytool")
if exe is None:
return [{"title": "...", "severity": "info", "deduction": 0, ...}]
# subprocess.run([exe, ...])If the default severity-tier deduction is wrong for a finding, add an override
to scoring.toml:
[overrides]
"Your Exact Finding Title" = 20 # overrides the severity-tier defaultADScan is intended for authorised security assessments only. Always obtain written permission before scanning an Active Directory environment. Unauthorised use may violate computer crime laws.