Skip to main content

entra-stale-accounts

Tests PyPI Python License: MIT

A read-only CLI that flags inactive Microsoft Entra ID (Azure AD) accounts past a configurable threshold. Point it at your own tenant, get a table or CSV of accounts that haven't signed in for N days — including accounts that have never signed in.

It never modifies anything: the only Microsoft Graph call it makes is a read of the user list. When Graph throttles (429), it waits out the Retry-After interval and retries instead of failing.

Install

pip install entra-stale-accounts

On macOS (or any system where Python blocks global pip installs), use pipx instead — it installs the CLI in its own isolated environment and puts the command on your PATH:

brew install pipx
pipx ensurepath
pipx install entra-stale-accounts

Usage

# Every enabled account with no sign-in in 90+ days
entra-stale-accounts check --days 90

# Same, as CSV — redirect to a file to hand to a manager or import elsewhere
entra-stale-accounts check --days 90 --output csv > stale.csv

# Also include already-disabled accounts, for a fuller audit
entra-stale-accounts check --days 90 --include-disabled

# Add a column of each account's license SKUs — which stale accounts
# are still holding paid licenses you could reclaim?
entra-stale-accounts check --days 90 --licenses
$ entra-stale-accounts check --help
Usage: entra-stale-accounts check [OPTIONS]

  List enabled Entra ID accounts with no sign-in activity in the last N days.

Options:
  --days INTEGER          Inactivity threshold in days  [default: 90]
  --output [table|csv]    Output format  [default: table]
  --include-disabled      Also show already-disabled accounts
  --env-file TEXT         Path to a .env file with tenant credentials
  --help                  Show this message and exit.

Accounts that have never signed in are always flagged — no activity at all is at least as noteworthy as an old sign-in — and sort to the top of the results.

Setup

The tool authenticates with your own Entra ID app registration via client credentials. Nothing is hardcoded — any tenant works.

1. Create an app registration

In Entra admin center → App registrations → New registration. No redirect URI needed.

Grant these application permissions under Microsoft Graph, then click Grant admin consent:

Permission Why
User.Read.All Read the user list
AuditLog.Read.All Read the signInActivity field
Organization.Read.All Optional — lets --licenses show your tenant's own SKU names

Without Organization.Read.All, --licenses still works: common SKUs resolve through a built-in table and anything unknown shows as its raw GUID.

Create a client secret under Certificates & secrets.

2. Configure credentials

Copy .env.example to .env and fill in your values:

ENTRA_TENANT_ID=your-tenant-id
ENTRA_CLIENT_ID=your-app-client-id
ENTRA_CLIENT_SECRET=your-client-secret

Plain environment variables work too, and take precedence over the .env file.

3. The licensing requirement (read this)

The Graph field this tool depends on — signInActivity — requires a Microsoft Entra ID P1 or P2 license on the tenant, not just API permissions. Without it, the field comes back empty or the request is denied — that's a licensing wall, not a bug. P1 is included in Microsoft 365 Business Premium (not Basic) and is also available standalone.

Accounts whose signInActivity is missing are reported as never signed in — if every account shows never, suspect the license, not your users.

Example output

USER PRINCIPAL NAME            DISPLAY NAME  ENABLED  LAST SIGN-IN  DAYS
-----------------------------  ------------  -------  ------------  -----
nina@contoso.onmicrosoft.com   Never Nina    true     never         never
sam@contoso.onmicrosoft.com    Stale Sam     true     2026-01-01    229

2 stale account(s) past a 90-day threshold.

Troubleshooting

"blocked by your organization's Device Guard policy" on Windows. Smart App Control (and enterprise App Control/WDAC policies) block the unsigned entra-stale-accounts.exe launcher that pip generates. The tool itself still runs fine through the signed Python interpreter — substitute the command name and keep every flag the same:

python -m entra_stale_accounts.cli check --days 90

CSV opens in Excel as one mangled column. In Windows PowerShell, > writes UTF-16, which Excel mis-detects. Redirect through Out-File instead:

entra-stale-accounts check --days 90 --output csv | Out-File stale.csv -Encoding utf8

A .env in the current directory is ignored. Fixed in 0.1.4 — upgrade with pip install --upgrade entra-stale-accounts, or pass --env-file .env explicitly on older versions.

Every account shows never. Almost always the licensing wall described in Setup, not a bug.

Development

git clone https://github.com/pobrienDev/entra-stale-accounts
cd entra-stale-accounts
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest

The test suite mocks every Graph call — it runs in milliseconds and never needs real credentials or a live tenant.

Origin

Generalized from account-lifecycle automation built for a production Microsoft 365 environment: while automating provisioning, I kept needing to check for stale accounts, so the pattern became a standalone, general-purpose tool any admin can install.

License

MIT

Metadata

Release files for entra-stale-accounts 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for entra-stale-accounts 0.2.0
File Size Uploaded
entra_stale_accounts-0.2.0.tar.gz 20.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for entra-stale-accounts 0.2.0
File Interpreter ABI Platform
entra_stale_accounts-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 35.6 kB

Release files / entra_stale_accounts-0.2.0.tar.gz

Download URL entra_stale_accounts-0.2.0.tar.gz
Size 20.7 kB
Tags Source
SHA-256 checksum
How to use checksums
25fa8224f4a9e1e76c21869ef5298b0e81f12e66a012d3f1c848b4b9a3987656
BLAKE2b-256 checksum
How to use checksums
224ab7e6693b6ef30dd790594626e5fb37f9d20275b3645387b462cd330eff43
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 28, 2026.

Transparency log

Release files / entra_stale_accounts-0.2.0-py3-none-any.whl

Download URL entra_stale_accounts-0.2.0-py3-none-any.whl
Size 14.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a510b7c89a3718e64f280f3438c800fbf73e9454e81232ac0930c011ee77577a
BLAKE2b-256 checksum
How to use checksums
bd9afd86f7e38e88b98c112fc0b6202dbf2ca13c5c6afcd1ad104950cb0ae387
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 28, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.1

2 release files

This release

0.2.0 This release

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page