Surface stale and orphaned objects in on-premises Active Directory.
Active Directory environments accumulate objects that nobody is actively managing. Users leave, their accounts stay. Service accounts outlive the projects that created them. Workstations get decommissioned but their computer objects stay in the directory, still in scope for group policy and still appearing in security group memberships. relic connects to a domain controller over LDAP and tells you what's there, how old it is, and what risk it carries.
More context is available in docs/demo.md.
Stale computer accounts — computers that have not authenticated since the threshold. Computer accounts authenticate when the machine starts and periodically during operation. A computer that has been silent for months either no longer exists or is no longer receiving policy.
Dormant user accounts — users who have not logged in within the threshold window. Includes service accounts identified by the presence of servicePrincipalName.
Disabled accounts with group memberships — the most immediately actionable finding. The account is already disabled, but its group memberships are still live. Removing them carries no operational risk and closes a re-enablement path.
Accounts with non-expiring passwords — DONT_EXPIRE_PASSWORD set in userAccountControl. Flagged on its own as MEDIUM; escalates to HIGH when the account is also stale.
Service accounts with aging passwords — accounts with SPNs whose pwdLastSet is older than the threshold are flagged as Kerberoasting exposure. A service account with an SPN and a multi-year-old password is a target regardless of whether the account appears active.
flowchart LR
LDAP["Domain controller<br/>LDAP or LDAPS bind"] --> Objects["Directory objects<br/>users, computers, disabled accounts, service accounts"]
Objects --> Signals["Hygiene signals<br/>last logon, password age, SPN, groups, flags"]
Signals --> Risk["Risk rules<br/>medium or high findings"]
Risk --> Reports["Review output<br/>terminal table, JSON, CSV"]
Reports --> Cleanup["Operator cleanup<br/>remove memberships, rotate passwords, disable stale objects"]
| Condition | Severity |
|---|---|
| Disabled account still holds group memberships | HIGH |
| Service account (SPN), password unchanged >365 days | HIGH |
| Computer account inactive >365 days | HIGH |
| Non-expiring password + inactive >180 days | HIGH |
| Stale user or computer account | MEDIUM |
| Non-expiring password (active account) | MEDIUM |
| Disabled account, no group memberships | MEDIUM |
| Service account password unchanged >90 days | MEDIUM |
# Full scan — all object types
rl -s dc01.corp.local --domain corp.local
relic --server dc01.corp.local --domain corp.local
# Show only HIGH and MEDIUM findings
rl -s dc01.corp.local --domain corp.local -F
relic --server dc01.corp.local --domain corp.local --only-flagged
# Adjust the inactivity threshold
rl -s dc01.corp.local --domain corp.local -d 180
# Target specific object types
rl -s dc01.corp.local --domain corp.local -U --disabled
# Write reports
rl -s dc01.corp.local --domain corp.local -o results.json --output-csv results.csv
# Use LDAPS and an explicit base DN
rl -s dc01.corp.local --ssl --base-dn "DC=corp,DC=local"
# Disable flagged objects (dry run first)
rl -s dc01.corp.local --domain corp.local --disable -n
rl -s dc01.corp.local --domain corp.local --disable# Simple bind (username + password from environment)
export RELIC_BIND_PASSWORD="<pass>"
rl -s dc01.corp.local --domain corp.local -u svc_relic --password-env RELIC_BIND_PASSWORD
# NTLM
rl -s dc01.corp.local -u "CORP\svc_relic" --password-env RELIC_BIND_PASSWORD --base-dn "DC=corp,DC=local"PowerShell equivalent:
$env:RELIC_BIND_PASSWORD = "<pass>"
rl -s dc01.corp.local --domain corp.local -u svc_relic --password-env RELIC_BIND_PASSWORDLDAPS (--ssl or --port 636) is recommended to avoid transmitting credentials in cleartext. -P still works for quick local testing, but --password-env or --password-stdin is safer for repeatable runs.
| Short | Long | Description |
|---|---|---|
-s HOST |
--server HOST |
Domain controller hostname or IP |
-u USER |
--username USER |
Bind username |
-P PASS |
--password PASS |
Bind password |
--password-env VAR |
Read bind password from environment variable | |
--password-stdin |
Read bind password from stdin | |
-p N |
--port N |
LDAP port (default: 389) |
-d N |
--days N |
Inactivity threshold in days |
-U |
--users |
Scan user accounts |
-C |
--computers |
Scan computer accounts |
-o FILE |
--output FILE |
Write JSON report to FILE |
-F |
--only-flagged |
Show MEDIUM and HIGH only |
-q |
--quiet |
Summary only, no table |
-n |
--dry-run |
Report without making changes |
| Flag | What it scans |
|---|---|
| (default — no flags) | All object types |
--users |
Dormant user accounts |
--computers |
Stale computer accounts |
--disabled |
Disabled accounts with group memberships |
--never-expires |
Accounts with non-expiring passwords |
JSON and CSV output includes: name, type, dn, last_logon_str, age_days, pwd_age_days, disabled, never_expires, group_count, risk, risk_reasons, os, description.
git clone https://github.com/srkyn/relic.git
cd relic
pip install .
rl --versionOr run directly:
pip install ldap3 tabulate
python relic.py --server dc01.corp.local --domain corp.local --dry-runrelic and lapse solve the same class of problem in different environments. lapse targets Entra ID (cloud) via Microsoft Graph API. relic targets on-premises Active Directory via LDAP. Between them they cover identity hygiene across both environments.
relic.py: the scannerdocs/demo.md: sanitized example output and interpretationtests/test_relic.py: unit tests (48 cases)docs/design-notes.md: implementation details and limitationsCHANGELOG.md: release history
lastLogonTimestampreplication lag means age readings may be off by up to 14 days.- Does not evaluate nested group membership chains.
- Does not parse password policy to determine actual expiry status.
- Does not support Kerberos/GSSAPI authentication.
- May miss objects in OUs the bind account cannot read.
- Read-only except when
--disableis used.
python -m py_compile relic.py
python -m unittest discover -s tests -v
rl --version