- C# 89.1%
- TSQL 9.5%
- PowerShell 1.4%
Previously a per-machine failure to write findings to the tracking database was logged but swallowed, so the run could exit 0 while the database silently missed a machine's findings. ScanEngine now records the affected machines (LivePersistFailures) and Program reports a clear summary and returns a new exit code 3 (completed, but the database is incomplete). Scanning and the console/CSV/JSON snapshots still complete so no work is lost. Exit code 3 is documented in README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|---|---|---|
| Cli | ||
| Core | ||
| database | ||
| Output | ||
| Remote | ||
| Scanners | ||
| .gitattributes | ||
| .gitignore | ||
| ADAccountUsage.csproj | ||
| ARCHITECTURE.md | ||
| build.ps1 | ||
| CONTRIBUTING.md | ||
| Directory.Build.props | ||
| LICENSE | ||
| PERMISSIONS.md | ||
| Program.cs | ||
| README.md | ||
| THIRD-PARTY-NOTICES.md | ||
ADAccountUsage
A self-contained Windows command-line tool that locates everywhere a specific Active Directory account is being used, so the account's password can be rotated safely without breaking non-interactive workloads.
It scans one or more machines (and optionally the local machine) for references to the target account across services, scheduled tasks, IIS, SQL Server, SSRS, the registry, the credential store, recent logon events, and application configuration files, then reports the findings to the console, CSV, JSON, and/or a SQL Server tracking database.
Why
It is common to configure scheduled tasks, Windows services, IIS application pools, and other non-interactive software to run under a personal AD account. When that password needs to change, every one of those configurations will start failing on the next run. This tool produces a comprehensive inventory of those locations so they can be migrated to a managed service account first.
What it scans
| Source | Local | Remote | Notes |
|---|---|---|---|
| Windows Services | yes | yes | "Log On As" account via WMI (Win32_Service) |
| Scheduled Tasks | yes | yes | Task principal via the Task Scheduler 2.0 COM API |
| IIS Application Pools | yes | yes | processModel identity in applicationHost.config; also anonymous-auth identity |
| Credential Manager | yes | no | Stored credential user names, plus the credential type (Domain vs Generic), persistence scope, and whether a secret is stored - so a domain-authentication credential is distinguishable from a website sign-in. The secret itself is never read |
| Registry (autologon) | yes | yes | Winlogon Default/Alt auto-logon account |
| SQL Server Agent | yes | yes | Job owners, server credentials, Agent proxies across all installed instances (default and named, discovered from the registry); tries mandatory then TDS 8.0 strict encryption (opt-in via --scan-sql-server) |
| SSRS | yes | yes | Unattended execution / service account in rsreportserver.config |
| Security Event Log | yes | yes | Logon (4624) and explicit-credential logon (4648) events, over the last --history-days days (default 14); the source machine comes from WorkstationName, falling back to the source IP (reverse-resolved to a host name unless --no-reverse-dns) |
| Kerberos tickets (DC) | yes | yes | On domain controllers, TGT (4768) and service-ticket (4769) requests for the account, with the client source address (reverse-resolved). Runs automatically when a DC is scanned; select/skip independently with the kerberos scanner key |
| Configuration Files | yes | yes | .config / .xml / .json references to the account |
Remote scanning of file-based sources (IIS, SSRS, config files) uses the
administrative shares (e.g. \\host\C$\...).
Two ways to scan remote machines
There are two remote models, and they can be combined with a fallback chain:
-
Direct-remote (default). The tool, running on your workstation, reaches into each remote machine using per-source remote APIs (WMI for services, Task Scheduler COM, Remote Registry, remote Event Log, and admin file shares). This needs no software on the target, but each API has its own remote-access requirement - for example Credential Manager cannot be read remotely at all, and the Registry scan needs the (default-disabled) Remote Registry service.
-
Deploy / agent mode (
--deploy, push). The tool copies itself to each target, runs there locally (so every source - including Credential Manager and the registry - is read with full local fidelity and no per-source remote dependency), returns its results, and cleans up. Transports are tried in order and fall back:- WinRM (PowerShell Remoting) - copies, runs, retrieves, and cleans up inside one remote session; works even when admin file shares are blocked.
- Remote scheduled task - copies via the admin share, runs a one-shot task as SYSTEM, retrieves the JSON, and deletes the task and temp folder.
- Direct-remote - the default behavior above, used if neither deploy transport succeeds.
ADAccountUsage -a CONTOSO\hannah --machine-file servers.txt --deploy --csv findings.csvSelect/limit transports with
--deploy-transport winrm,task(orauto), and set a per-machine cap with--deploy-timeout <minutes>. The deployed agent writes only JSON, which is pulled back and merged centrally - so the orchestrator (not the agent) writes to SQL Server, avoiding Kerberos "double-hop" issues.Notes: deploy mode requires local Administrator on each target; the scheduled-task agent runs as SYSTEM, so Credential Manager then reflects SYSTEM's vault (not other interactive users' vaults); and copying/running an executable remotely is the kind of activity EDR/AV may flag - code-sign and allowlist the binary in a managed estate. See PERMISSIONS.md and ARCHITECTURE.md.
Selecting which scanners run
By default every scanner runs (SQL Server is opt-in via --scan-sql-server). To
target specific sources - e.g. a quick re-check of just tasks and stored credentials
without the slow config-file sweep - use --only or --skip with these keys:
services, tasks, iis, credman, registry, sql, ssrs, eventlog, kerberos, config.
ADAccountUsage -a CONTOSO\hannah -IncludeLocalMachine --only tasks,credman
ADAccountUsage -a CONTOSO\hannah -m web01,web02 --skip eventlog,config
--only and --skip are mutually exclusive; --only sql implies
--scan-sql-server.
The event-log and Kerberos scanners look back --history-days days (default 14).
Lower it for frequent runs that only need recent activity - e.g. a daily scheduled
run with --history-days 1. Combined with the tracking database (which keeps
FirstSeenUtc and the vNewlyDiscovered view), this lets you spot newly-appeared
usage day over day.
Account matching
The account is matched, case-insensitively and exactly, in all of its canonical
forms. Given CONTOSO\hannah (or hannah@contoso.com, or hannah --domain CONTOSO)
the tool searches for:
hannahCONTOSO\hannahhannah@contoso.com(when a DNS domain is known)
Because the down-level (CONTOSO\hannah) and UPN (hannah@contoso.com) domain
names are not derivable from one another, when you supply only one the tool looks
the other up in Active Directory at startup (via the NameTranslate ADSI
component, querying a Global Catalog and then the local domain). Any resolved form
is reported in the startup summary, e.g.:
Resolved additional account form(s) from AD: hannah@contoso.com
If the lookup cannot run (offline / not domain-joined) or the account is not found,
the tool prints a note and continues with the forms it has. You can then supply the
missing half explicitly with --domain (see below), or disable the lookup entirely
with --no-domain-resolution.
Configuration-file matching uses whole-token boundaries so that hannah does not
match inside hannahsmith. A bare-username match that is merely part of a
filesystem path (e.g. hannah in C:\Users\hannah\AppData, including UNC paths and
paths containing spaces) is ignored, since that is a path and not an account
reference; domain-qualified forms (DOMAIN\hannah, hannah@dns) are always kept.
To match only the exact string you supply (no domain expansion and no AD
lookup), use --no-username-expansion - useful for an ambiguous literal such as a
service-account name: -a BackupService --no-username-expansion searches for
BackupService alone, not DOMAIN\BackupService or BackupService@domain.tld.
Usage
ADAccountUsage --account <account> [machine selection] [options]
Required
-a,--account <account>- the account to search for. AcceptsDOMAIN\user,user@domain.com, or a bareuser.
Machine selection (at least one required)
-m,--machines <list>- comma-separated machine names.--machine-file <path>- text file with one machine per line (blank lines and lines starting with#are ignored).-IncludeLocalMachine- include the local computer in the scan.--discover [mode]- add machines discovered from Active Directory (modead, the default) to the scan targets, so you do not have to maintain a machine list by hand. Combine with the discovery filters below.--list-machines <path>- run discovery, write the resolved machine list to<path>(one host per line, with#header comments), and exit without scanning. The file can be reviewed and later reused with--machine-file.
Discovery filters (with --discover / --list-machines)
--ou <distinguishedName>- limit discovery to an organizational-unit subtree, e.g.--ou "OU=Servers,DC=contoso,DC=com".--servers-only- only computers whoseoperatingSystemcontains "Server".--stale-days <n>- exclude computers whose last domain logon (lastLogonTimestamp) is older thanndays, so decommissioned hosts are skipped.--include-disabled- include disabled computer accounts (default: enabled only).
Only ad (Active Directory) discovery is implemented today; subnet and dns are
reserved mode names that currently return a "not yet supported" message.
Profiles (saved option sets)
Re-running the same long command line is tedious, so options can be saved to a named profile and replayed:
--save-profile <name>- save the supplied options as a profile, then run the scan. Add--dry-runto save without scanning.--profile <name>- load options from a saved profile. Any further command-line options are appended, so they override the saved scalar values (e.g.--profile nightly --history-days 1).
A profile is the set of command-line arguments stored as JSON under
%APPDATA%\ADAccountUsage\profiles\<name>.json; pass a path ending in .json to use an
explicit file instead. Because connections always use Windows authentication and the
tool never accepts a password on the command line, a profile can never contain a secret.
Scan options
--domain <domain>- explicit domain used to qualify a bare username.--scan-all-drives- walk every fixed drive for configuration files instead of only the common application locations.--scan-sql-server- enable the SQL Server Agent / credential / proxy scan (off by default because it connects to SQL Server instances).--sql-trust-server-cert- during the SQL Server scan, accept a self-signed server TLS certificate. Off by default (the certificate chain is validated and the connection fails closed against an untrusted certificate); enable only for internal hosts known to present self-signed certificates.--max-parallel <n>- number of machines scanned concurrently (default 4).
Output (defaults to --console)
--console- formatted report to stdout.--csv <path>- write findings to a CSV file.--json <path>- write findings to a JSON file.- SQL Server tracking database (Windows authentication only). The connection is
built from discrete options and always uses Integrated Security; the tool never
accepts a username or password, so no secret is ever stored or logged. The schema is
applied/updated automatically on startup.
--sql-server <host>- server, ashostorhost\instance. Enables SQL output.--sql-port <n>- TCP port (when not using the default port or instance name).--sql-database <name>- tracking database name (defaultADAccountUsage).--sql-encrypt <mode>-auto(default: trymandatory, thenstrict),optional,mandatory, orstrict(TDS 8.0).--sql-host-name-in-certificate <fqdn>- certificate subject name, required for strict encryption when connecting by an instance or NetBIOS name that does not match the certificate (e.g. SQL Server 2025 with strict-only encryption).--sql-multi-subnet-failover- resolve all availability-group listener IPs in parallel.
--do-not-log-errors- do not persist scan warnings/errors to the database.
Misc
-v,--verbose- verbose diagnostics to stderr.-h,--help- usage.
Default scan locations (configuration files)
Without --scan-all-drives, the following locations are searched (recursively, and
translated to the admin share for remote machines):
%ProgramData%,%ProgramFiles%,%ProgramFiles(x86)%,%ProgramW6432%%SystemDrive%\inetpub- IIS sites (web.config)%SystemRoot%\Panther- Windows Setup answer files (unattend.xml)%SystemRoot%\System32\Sysprep- sysprep answer files%SystemRoot%\System32\GroupPolicy- local Group Policy%SystemRoot%\System32\inetsrv\config- IIS configuration%SystemRoot%\SYSVOL- on domain controllers, Group Policy Preferences (Groups.xml,Services.xml,ScheduledTasks.xml, ...), a classic place service accounts are referenced
For local scans only, %APPDATA% (recursively) and each directory on %PATH%
are also searched. PATH directories are scanned non-recursively (only the files
directly in them) - they list folders that hold executables and their .exe.config,
not trees to crawl; recursing them would walk all of C:\Windows and
C:\Windows\System32.
Only .config, .xml, and .json files are read by default. Use
--extra-extensions to also scan other file types: given alone it adds
log,ini,txt,ps1,cmd (e.g. setupact.log in Panther, task-sequence logs,
domain-join scripts), or pass a comma-separated list such as
--extra-extensions ini,ps1. These types are noisier and can be large (files over
5 MB are skipped), so they are opt-in.
Examples
Scan the local machine for your account and print to the console:
ADAccountUsage -a CONTOSO\hannah -IncludeLocalMachine
Scan a set of servers and write a CSV:
ADAccountUsage -a hannah@contoso.com -m web01,web02,sql01 --csv findings.csv
Scan a large server list from a file, include SQL Server, and record results in the tracking database for periodic trend analysis:
ADAccountUsage -a hannah --domain CONTOSO --machine-file servers.txt --scan-sql-server ^
--sql-server mon01 --sql-database ADAccountUsage
Discover the server fleet from Active Directory and scan it directly, writing to a strict-encryption (TDS 8.0) tracking database on a named instance:
ADAccountUsage -a hannah --domain CONTOSO --discover --servers-only ^
--sql-server mon01\SQL2025 --sql-encrypt strict ^
--sql-host-name-in-certificate mon01.contoso.com
Generate a reusable machine list from a specific OU, then save a profile and replay it daily, capturing only the last day's event-log/Kerberos activity:
ADAccountUsage -a hannah --discover --ou "OU=Servers,DC=contoso,DC=com" --list-machines servers.txt
ADAccountUsage -a hannah --discover --servers-only --scan-sql-server --sql-server mon01 --save-profile nightly
ADAccountUsage --profile nightly --history-days 1
Output and exit codes
On startup the tool prints a scan configuration summary to stderr (account forms, machines, scan options, parallelism, and outputs) and runs preflight checks: if SQL Server output is selected it verifies connectivity and that the tracking schema exists, and for CSV/JSON it confirms the target file is writable. If a preflight check fails the tool aborts immediately, before scanning, so a long run is never wasted on an output that cannot be written.
Findings are delivered two ways:
- SQL Server output is written live - a scan run is opened up front and each machine's findings are persisted to the database as that machine completes, so progress is durable even if the scan is interrupted (Ctrl+C) and the database can be watched while the scan runs.
- Console, CSV, and JSON are written once at the end as a complete snapshot of all findings.
stdout carries the console report (when enabled); diagnostics and the startup summary go to stderr, so the two can be redirected independently. Exit codes:
0- completed (whether or not findings were located)1- fatal error2- invalid arguments3- completed, but one or more machines' findings could not be written to the SQL Server tracking database (file/console outputs are complete; the database is missing those machines). Investigate the database connection and re-run.130- cancelled (Ctrl+C)
SQL Server tracking database
database/CreateDatabase.sql creates the empty ADAccountUsage database - the
only step an administrator must perform (the application never creates a database).
The schema itself (database/Schema.sql) is applied and updated automatically by
the application on startup, so upgrades pick up new tables/columns without a manual
migration; it can also be applied by hand with
sqlcmd -d ADAccountUsage -i Schema.sql.
The schema provides a Finding table (deduplicated on a hash of machine + source +
location + account, tracking FirstSeenUtc / LastSeenUtc / TimesSeen and a
Resolved flag), a ScanRun audit table, a ScanError table (warnings/errors from
each run), upsert/resolve stored procedures, and analysis views/functions:
vUnresolvedFindings- everything still needing attention (excludes resolved and ignored)vFindingsByMachine,vFindingsBySource- rollupsvNewlyDiscovered- findings first seen in the last 7 daysfnFindingsSeenSince(@sinceUtc)- findings observed on/after a datevScanRuns- run history with status and durationvScanErrors- scan warnings/errors joined to their runvIgnoredFindings- findings marked as red herrings
Ignoring red-herring findings
Some matches are noise rather than a deliberate use of the account. There are three layers:
- Built-in OS-noise filter (automatic): per-user Windows scheduled tasks such as
Optimize Start Menu Cache Files-S-1-5-21-...andUser_Feed_Synchronization-...are dropped from every output by default. Keep them with--include-builtin-noise. - Per-finding ignore flag: mark any specific finding as ignorable -
EXEC dbo.usp_IgnoreFinding @FindingId = 123, @Reason = N'...'. It is preserved across re-scans (likeResolved) and excluded fromvUnresolvedFindings. Undo withdbo.usp_UnignoreFinding. - Reusable ignore rules: the
IgnoreRuletable holds patterns (SourceType+LocationLIKE pattern + optionalMachineName).dbo.usp_UpsertFindingauto-ignores new findings that match a rule, anddbo.usp_AddIgnoreRule @LocationPattern = N'...', @SourceType = '...'adds one and applies it to existing rows. It is seeded with the built-in OS-noise patterns.
Scan warnings and errors (unreachable machines, access denied, the Remote Registry
service being off, an installed SQL Server instance that could not be scanned,
deploy-transport failures, ...) are written to ScanError so that scheduled,
unattended runs are auditable. In --deploy mode the agent's own warnings/errors
are returned to the orchestrator and persisted too, so nothing is lost on remote
targets. Disable this with --do-not-log-errors (they still go to stderr).
Scheduling the tool to write to this database lets you watch the inventory shrink as you migrate each workload, and detect when the account starts being used somewhere new.
Permissions
The tool only reads; it never changes configuration and never copies secret values. However, reading these sources requires elevated rights on each target. See PERMISSIONS.md for the exact permissions and how to run it under a preconfigured account.
Building
Requires the .NET 10 SDK.
The simplest way to produce a release build is the helper script, which publishes a
self-contained single-file executable, copies it to artifacts\, and writes a
SHA-256 checksum next to it (useful for verifying the binary before deploy mode
copies it to remote machines):
.\build.ps1
Options: -Configuration (default Release), -Runtime (default win-x64),
-OutputPath (default .\artifacts), and -Clean to remove bin/obj/output
first. Example: .\build.ps1 -Runtime win-arm64 -Clean.
Equivalent manual commands:
dotnet build # local debug build
dotnet publish -c Release # self-contained single-file exe
The published ADAccountUsage.exe is a single self-contained file (no .NET install
needed on the target); build.ps1 places it in artifacts\ADAccountUsage.exe.
Security
This tool is read-only and deliberately avoids handling secrets:
- It reads account names only; it never reads stored passwords (Credential
Manager blobs, Winlogon
DefaultPassword, IIS/SSRS encrypted passwords). - It uses integrated authentication everywhere; no password is ever embedded in a connection string or command line by the tool.
- Findings and logs never contain secret material.
- CSV output neutralises spreadsheet formula injection: any field beginning with
=,+,-,@, tab, or carriage return (which can include remote-controlled text such as a service or task name) is prefixed so spreadsheet applications treat it as text rather than a formula.
See ARCHITECTURE.md for design details and CONTRIBUTING.md for development guidelines.
License
MIT. Third-party dependencies are listed in THIRD-PARTY-NOTICES.md.