Skip to main content

nhi-scan

Every workload, integration, and AI agent now has an identity — and nobody owns most of them. nhi-scan inventories your non-human & agent identities, assigns each a defensible risk tier, and maps its posture to the OWASP Non-Human Identities (NHI) Top 10 — with a least-privilege remediation for every finding.

It runs entirely locally and is deterministic: the same inventory always produces the same tiers and findings, so an assessment is explainable to an engineer and defensible to an auditor. No LLM is in the verdict path.

See PITCH.md for the why, with sourced market data — and A Control Framework for Non-Human & Agentic Identity for the control model this tool implements (30 controls mapped to OWASP NHI Top 10, NIST AI RMF, CSF 2.0, and 800-53).

Machine identities now outnumber human ones by more than 82:1 (CyberArk, 2025), and AI agents are the fastest-growing class — each one a privileged, often autonomous identity. nhi-scan treats the agent as what it is: an NHI that needs an owner, least privilege, short-lived credentials, and a tier.

What's new (v0.3.0): owner validity (an identity whose owner has left is caught as orphaned, not "governed"), native staleness from sign-in activity, directory-role privilege, an Okta collector, a ~20× faster enriched Entra gather via Graph $batch, and full Windows support. See the CHANGELOG for the full history since v0.1.

Quickstart (30 seconds)

pip install nhi-scan                           # from PyPI
# or from source:  git clone https://github.com/rpmsft9/nhi-scan && cd nhi-scan && pip install -e .

nhi-scan scan examples/sample-inventory.json   # full Markdown risk report

Sample output:

# Non-Human Identity Risk Report
**8** identities · **21** findings · **1** orphaned · **3** long-lived secrets

| Tier        | Identities |
| ----------- | ---------: |
| 🔴 Critical | 3 |
| 🟠 High     | 1 |
| 🟡 Moderate | 4 |

Point it at a JSON inventory (or generate one with a collector — see Collectors below) and you get a tiered, OWASP-NHI-mapped report with a remediation for every finding. New here? Start with the story and field notes at rajpenchala.com.

Why this exists

Discovery tools tell you how many secrets and service accounts you have. A CISO needs the next two answers: which ones are crown jewels, and what do I fix first. nhi-scan is that prioritization layer.

  • Risk-tiering, not a flat list. A transparent floor-tier rules engine assigns Tier 1–4 from posture (privilege, credential type, rotation age, ownership, exposure, autonomy). The most severe rule that matches sets the tier, and every matching rule is recorded — so you can always answer "why is this Tier 1?"
  • OWASP NHI Top 10 mapping. Ten posture checks emit findings tied to NHI1–NHI10, each with the concrete evidence and a remediation. The regulatory content is a versioned data layer (owasp.py), not code.
  • Agent-aware. Autonomous AI agents with elevated privilege tier as crown jewels — the emerging gap that classic secret scanners don't model.

Prerequisites

nhi-scan is pure Python and runs on Linux, macOS, and Windows.

  • Python ≥ 3.10 (python --version). The core install has zero third-party dependencies.
  • pip (and, to run from source, git).
  • Collectors are invoked as modules from the repository root (python -m tools.collectors.*).

To generate an inventory from a live cloud, install and sign in to the matching read-only CLI. Hand-written, CSV, and MCP inventories need none of these.

Source CLI needed macOS (Homebrew) Windows (winget)
Entra ID / Entra Agent ID Azure CLI (az) brew install azure-cli winget install Microsoft.AzureCLI
AWS IAM AWS CLI v2 (aws) brew install awscli winget install Amazon.AWSCLI
GCP Google Cloud SDK (gcloud) brew install --cask google-cloud-sdk winget install Google.CloudSDK
Merge step (optional) jq brew install jq winget install jqlang.jq

On Windows the cloud CLIs install as .cmd shims and their exports carry a UTF-8 BOM; both are handled — the collectors locate the CLI on PATH on every OS and read JSON BOM-tolerantly.

Install

pip install nhi-scan            # core, zero third-party dependencies (JSON inventories)
pip install "nhi-scan[yaml]"    # + YAML inventory support

From source (for development / running the collectors):

git clone https://github.com/rpmsft9/nhi-scan && cd nhi-scan
pip install -e '.[dev]'         # editable install + pytest

Required roles & permissions

Every collector is read-only — it reads identities and their posture, never writes and never touches a secret (auth stays with the CLI you signed in with). Grant the least-privilege role below for the source you're inventorying:

Platform Native collector Least-privilege read role / permission
Entra ID (service principals) gather_entra → entra Microsoft Graph application permission Application.Read.All (or Directory.Read.All). Directory role Global Reader is sufficient.
Entra Agent ID (AI agents) gather_entra_agents → entra_agents AgentIdentity.Read.All + Application.Read.All (Directory.Read.All covers both). Directory role Global Reader.
AWS IAM gather_aws → aws The AWS-managed IAMReadOnlyAccess policy.
GCP (service accounts) gather_gcp → gcp roles/iam.securityReviewer (project Viewer also works).
Okta (service apps + API tokens) gather_okta → okta A Read-Only Administrator SSWS token, or an OAuth token scoped okta.apps.read + okta.apiTokens.read. Provided via OKTA_ORG_URL / OKTA_API_TOKEN env vars (no CLI needed).
Ping and other IdPs export → csv_import No native collector yet — export your apps / service accounts / tokens to CSV using a read-only administrator or reporting role (e.g. PingOne a read-only Identity Data Admin), then feed the CSV to csv_import. The role is only used for your export, not by nhi-scan.

No role grants write access; nothing above can modify the directory or the identities it reads.

Which platforms are native? Entra ID, Entra Agent ID, AWS IAM, GCP, and Okta have first-class collectors. Everything else (Ping, GitHub, secrets managers, …) is inventoried by exporting a read-only report to CSV — see Preparing your inventory.

Usage

nhi-scan inventory examples/sample-inventory.json     # counts by type and risk tier
nhi-scan scan      examples/sample-inventory.json     # full Markdown risk report
nhi-scan scan      examples/sample-inventory.json --json   # machine-readable JSON
nhi-scan diff      examples/sample-inventory.json examples/sample-inventory-after.json   # drift between two scans

See examples/sample-report.md for a full rendered report.

What a scan surfaces

8 identities · 21 findings · 1 orphaned · 3 long-lived secrets

🔴 Critical  3     🟠 High  1     🟡 Moderate  4     🟢 Baseline  0

The top-ranked identity in the sample is an autonomous collections AI agent holding ledger:* in production — flagged Tier 1 (critical) for overprivilege and autonomy, exactly the kind of NHI that never appears on a human-identity review.

The inventory format

A JSON or YAML list of NHI records. Only id and name are required; everything else defaults to a safe, conservative posture, so a partial inventory still assesses.

{
  "id": "agent-collections",
  "name": "collections-ai-agent",
  "type": "ai_agent",
  "owner": "cx-automation@bank.example",
  "environment": "prod",
  "privilege": "privileged",
  "credential": "static_secret",
  "secret_storage": "env",
  "last_rotated_days": 30,
  "last_used_days": 0,
  "exposure": "internal",
  "scopes": ["accounts:read", "accounts:update", "ledger:*"],
  "autonomous": true
}
Field Values Drives
type service_account, api_key, oauth_app, service_principal, managed_identity, workload_identity, ci_cd_token, pat, webhook, secret, ai_agent inventory + agent rules
privilege admin, privileged, scoped, read_only tiering, NHI5
credential static_secret, api_key, certificate, federated, managed, short_lived_token, none NHI4, NHI7
secret_storage vault, env, plaintext, none NHI2
last_rotated_days / last_used_days integer or null NHI7 / NHI1 (staleness)
owner / owner_active string / bool NHI1 — presence and validity; owner_active: false (a deprovisioned owner) is treated as orphaned
exposure internet, external_partner, internal tiering, NHI6
scopes list of strings (* / :* = wildcard) NHI5
tools list of strings — an agent's tools / connectors / MCP servers agent reach (drift)
autonomous, third_party, human_used, shared_across_env, used_by booleans / list agent rules, NHI3/8/9/10

Detecting drift — when an agent's reach grows

Privilege, credential age, and owner change only when someone touches the identity. An AI agent's reach doesn't: give it a new tool, connector, or MCP server and its blast radius grows while those attributes — and often the risk tier — look identical. nhi-scan diff compares two inventories and surfaces exactly that:

nhi-scan diff before.json after.json          # Markdown
nhi-scan diff before.json after.json --json    # machine-readable

It reports added/removed identities, tier escalations, and — most importantly — a "reach grew without a tier change" section: identities that gained tools or scopes while privilege, credential age, and owner stayed the same. Run it on a schedule (or in CI) to catch reach creep between point-in-time scans.

Where tools comes from. nhi-scan reads the agent's declared tool manifest — it doesn't infer reach. Populate tools from wherever an agent's capabilities are defined: its MCP server config (the tools each server exposes), its agent-framework manifest (LangChain / Semantic Kernel / AutoGen function lists), or its registered plugins / connectors / actions in platforms like Copilot Studio, Microsoft Agent 365 / Entra Agent ID. Feeding that manifest in is what turns "an agent got a new connector" from an invisible change into a diffable one.

Preparing your inventory

Copy examples/template-inventory.json — it's an annotated template with the allowed values inline and both a minimal and a fully-populated record. Then build one record per identity by pulling from where your NHIs actually live:

Source What to pull
Entra / Okta / Ping service principals, enterprise & OAuth apps, workload identities
AWS / Azure / GCP IAM users & access keys, roles, service accounts, managed identities
Secrets managers / vaults stored secrets and their rotation age
GitHub / GitLab / CI PATs, deploy tokens, pipeline credentials
Agent platforms (e.g. Entra Agent ID) your AI agents — set type: ai_agent and autonomous

Start minimal, then enrich. Begin with the fields you can get cheaply — id, name, type, owner, environment, privilege — run a scan, then add credential, last_rotated_days, and scopes to sharpen the results. Missing fields fall back to conservative defaults rather than failing, so a partial inventory still produces a useful report.

Generate the inventory automatically

For anything past a pilot, don't hand-write the file — generate it from your environment with the collectors. Each is a read-only transform (source API JSON in → nhi-scan JSON out) for Entra ID, Entra Agent ID, AWS IAM, GCP service accounts, Okta, and CSV exports — each paired with a read-only gather_* script that collects the identity's granted permissions (app roles and delegated scopes; attached, inline, and group-inherited IAM policies; project role bindings; Okta OAuth grants), so privilege is measured, not guessed:

python -m tools.collectors.gather_entra        | python -m tools.collectors.entra        > entra-nhi.json
python -m tools.collectors.gather_entra_agents | python -m tools.collectors.entra_agents > agents-nhi.json
python -m tools.collectors.gather_aws          | python -m tools.collectors.aws          > aws-nhi.json
python -m tools.collectors.gather_gcp          | python -m tools.collectors.gcp          > gcp-nhi.json
python -m tools.collectors.gather_okta         | python -m tools.collectors.okta         > okta-nhi.json
python -m tools.collectors.csv_import identities.csv > csv-nhi.json
# merge every source, then scan
jq -s 'add' *-nhi.json > inventory.json && nhi-scan scan inventory.json

Run the collect → merge → scan pipeline on a schedule to keep the inventory live. See the collectors guide for the read-only permissions and gather scripts.

Risk policy

Tiering rules live in nhiscan/tiering.py; OWASP checks in nhiscan/checks.py. Both are ordered, pure-function lists — edit them to match your control standard. Thresholds (rotation window, staleness window, wildcard scopes) are constants at the top of nhiscan/models.py.

Tests

pytest -q

License

MIT.

Release files for nhi-scan 0.3.1

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

Source distribution (sdist)

Source distribution for nhi-scan 0.3.1
File Size Uploaded
nhi_scan-0.3.1.tar.gz 32.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for nhi-scan 0.3.1
File Interpreter ABI Platform
nhi_scan-0.3.1-py3-none-any.whl Python 3 none any Details

Total release size: 59.3 kB

Release files / nhi_scan-0.3.1.tar.gz

Download URL nhi_scan-0.3.1.tar.gz
Size 32.7 kB
Tags Source
SHA-256 checksum
How to use checksums
0d46bd4c0d149f5236dca23619747407fc0c7a34ce780ad301829897f824f5bf
BLAKE2b-256 checksum
How to use checksums
077f44c98809df595a2adb49c5d74a50c9f2ca7176fdc9caa8f4d76642cc7cb8
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 Sep 26, 2026.

Transparency log

Release files / nhi_scan-0.3.1-py3-none-any.whl

Download URL nhi_scan-0.3.1-py3-none-any.whl
Size 26.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
77b62f3587c0cf05e80d1ea3725b5693f4cdf0f3ca2b73a95d320686dbf17867
BLAKE2b-256 checksum
How to use checksums
17fadeccff20805bb0eaf344424b9f2aff42f645aded27858f753af347016df8
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 Sep 26, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.1 This release

2 release files

0.3.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