Find everywhere an Active Directory account is used across your estate before rotating its password.
  • C# 89.1%
  • TSQL 9.5%
  • PowerShell 1.4%
Find a file
Hannah Vernon 9c1d86036a Fail the run when live SQL persistence fails for any machine
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>
2026-06-30 10:26:20 -05:00
Cli Add AD machine discovery, option profiles, and Windows-auth SQL flags 2026-06-30 10:06:42 -05:00
Core Fail the run when live SQL persistence fails for any machine 2026-06-30 10:26:20 -05:00
database Add ways to ignore red-herring findings (filter, flag, rules) 2026-06-29 14:47:32 -05:00
Output Add AD machine discovery, option profiles, and Windows-auth SQL flags 2026-06-30 10:06:42 -05:00
Remote Add scanner selection (--only/--skip) and configurable history window (--history-days) 2026-06-29 17:34:03 -05:00
Scanners Split Kerberos into its own selectable scanner; remove --no-kerberos-events 2026-06-29 17:45:22 -05:00
.gitattributes Initialize repository with line ending normalization configuration 2026-06-26 11:07:46 -05:00
.gitignore Add build.ps1 to produce the single-file executable 2026-06-26 16:31:17 -05:00
ADAccountUsage.csproj Add AD machine discovery, option profiles, and Windows-auth SQL flags 2026-06-30 10:06:42 -05:00
ARCHITECTURE.md Add AD machine discovery, option profiles, and Windows-auth SQL flags 2026-06-30 10:06:42 -05:00
build.ps1 Add build.ps1 to produce the single-file executable 2026-06-26 16:31:17 -05:00
CONTRIBUTING.md Add build.ps1 to produce the single-file executable 2026-06-26 16:31:17 -05:00
Directory.Build.props Add .NET 10 console project scaffold with single-file publish and NuGetAudit configuration 2026-06-26 11:09:00 -05:00
LICENSE Implement scanners, output formatters, tracking database, and documentation 2026-06-26 11:45:12 -05:00
PERMISSIONS.md Auto-apply DB schema on startup and log scan errors to the database 2026-06-26 17:25:36 -05:00
Program.cs Fail the run when live SQL persistence fails for any machine 2026-06-30 10:26:20 -05:00
README.md Fail the run when live SQL persistence fails for any machine 2026-06-30 10:26:20 -05:00
THIRD-PARTY-NOTICES.md Add AD machine discovery, option profiles, and Windows-auth SQL flags 2026-06-30 10:06:42 -05:00

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:

    1. WinRM (PowerShell Remoting) - copies, runs, retrieves, and cleans up inside one remote session; works even when admin file shares are blocked.
    2. 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.
    3. Direct-remote - the default behavior above, used if neither deploy transport succeeds.
    ADAccountUsage -a CONTOSO\hannah --machine-file servers.txt --deploy --csv findings.csv
    

    Select/limit transports with --deploy-transport winrm,task (or auto), 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:

  • hannah
  • CONTOSO\hannah
  • hannah@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. Accepts DOMAIN\user, user@domain.com, or a bare user.

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 (mode ad, 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 whose operatingSystem contains "Server".
  • --stale-days <n> - exclude computers whose last domain logon (lastLogonTimestamp) is older than n days, 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-run to 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, as host or host\instance. Enables SQL output.
    • --sql-port <n> - TCP port (when not using the default port or instance name).
    • --sql-database <name> - tracking database name (default ADAccountUsage).
    • --sql-encrypt <mode> - auto (default: try mandatory, then strict), optional, mandatory, or strict (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 error
  • 2 - invalid arguments
  • 3 - 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 - rollups
  • vNewlyDiscovered - findings first seen in the last 7 days
  • fnFindingsSeenSince(@sinceUtc) - findings observed on/after a date
  • vScanRuns - run history with status and duration
  • vScanErrors - scan warnings/errors joined to their run
  • vIgnoredFindings - 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-... and User_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 (like Resolved) and excluded from vUnresolvedFindings. Undo with dbo.usp_UnignoreFinding.
  • Reusable ignore rules: the IgnoreRule table holds patterns (SourceType + Location LIKE pattern + optional MachineName). dbo.usp_UpsertFinding auto-ignores new findings that match a rule, and dbo.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.