PowerShell · Microsoft Graph · NHI Governance

Entra ID
NHI Audit

I built a tool that reviews sample cloud accounts for access risks and produces a report with suggested actions.

PowerShell 7 Microsoft Graph SDK 8 risk factors Mock + live modes
At a glance 30-second read

What it is

I built four PowerShell commands to connect, collect account information, check selected risks, and export a report. The demo uses 28 sample records. The tool can also connect to a real Microsoft Entra tenant when configured. NHI means non-human identity: an account used by software.

What I explored

I explored how to turn account-risk checks into a report people can review.

Skills and tools

  • PowerShell 7 module design (manifest, public/private structure)
  • Microsoft Graph SDK integration (delegated + app-only auth patterns)
  • Entra ID NHI taxonomy: SP, app registration, managed identity
  • Eight-factor risk scoring with explainable findings
  • Self-contained HTML report + structured JSON for SIEM / ticketing
  • Mock-vs-live mode for testability
PowerShell 7 Microsoft Graph Entra ID Active Directory NHI IAM
01 — Sample Output 28 mock NHIs · auto-scored

A mock audit report for review.

The sample report starts with counts, then lists accounts by risk. Each finding has a suggested action. The HTML report can be shared as one file.

reports/sample-report.html [ ACTUAL OUTPUT ]
Contoso (mock) — NHI Audit
Tenant: Contoso (mock) · Mode: mock · Generated: 2026-05-24 12:00 UTC
5
Critical
4
High
2
Medium
4
Low
13
Healthy
28
Total
legacy-payroll-sync
100 / 100 · Critical
Application · 0 owners · Last sign-in: 2024-12-08
[High] NHI001 — No owner assigned
→ Assign a team owner via az ad sp owner add or Entra portal.
[Critical] NHI004 — Credential expired 251 days ago but identity still enabled
→ Rotate immediately or disable identity if no longer needed.
[High] NHI008 — Marked 'legacy' and still enabled
→ Confirm migration plan; set decommission date.
intern-poc-api-2024
85 / 100 · Critical
Application · 0 owners · Last sign-in: 2024-08-22
[High] NHI001 — No owner assigned
[Critical] NHI004 — Credential expired 343 days ago but identity still enabled
datadog-monitor
0 / 100 · Healthy
Application · 1 owner · Last sign-in: 2026-05-24
No findings — healthy
Open the full sample report on GitHub →
02 — Risk Factors eight scored signals

What the scorer looks for.

I chose specific risk checks. Each finding has a severity, a short explanation, and a suggested action.

CodeFactorSeverity
NHI001No owner assignedHigh
NHI002Created >14 days ago, never signed inHigh
NHI003No sign-in for >90 days (180+ → High; 90–180 → Medium)High / Med
NHI004Credential expired but identity still enabledCritical
NHI005Credential expires within 30 daysMedium
NHI006Oldest credential >365 days (rotation policy breach)Medium
NHI007Account disabled but credentials still presentMedium
NHI008Tagged 'legacy' and still enabledHigh

Adding a factor is a function in Test-NhiRisk.ps1. The scoring is linear (per-factor weights add to 0–100) so individual contributions stay readable in a report — no opaque ML model.

03 — Try it mock mode · no tenant required

Clone, run, open the report.

PowerShell 7+ on macOS, Linux, or Windows. No Microsoft Graph SDK needed for the mock demo — only for live-tenant mode.

entra-id-nhi-audit · demo [ COPY/PASTE ]
$ git clone https://github.com/iambrucedavis/entra-id-nhi-audit
$ cd entra-id-nhi-audit
$ pwsh scripts/run-demo.ps1 # mock mode, no tenant
$ open reports/nhi-audit.html
# Real tenant — requires Microsoft.Graph SDK + user consent
$ Install-Module Microsoft.Graph -Scope CurrentUser -Force
$ pwsh scripts/connect-live-tenant.ps1 -TenantId <guid>

The cmdlets pipe naturally. Get-EntraNhiInventory | Test-NhiRisk | Where-Object RiskLevel -eq 'Critical' gives you just the critical findings; | Export-NhiAuditReport writes the report.

04 — Downstream JSON is shaped for ingestion

Built to plug into the rest of the stack.

The HTML report is for humans. The JSON output is for everything else.

SIEM Alerts
Pipe the JSON to Splunk / Sentinel / Datadog. Alert on any record with RiskLevel: Critical — auto-page the on-call IAM engineer with the remediation already attached.
Ticketing
One JIRA / Linear ticket per Critical or High finding. The Remediation field becomes the ticket description; the owner team gets assigned automatically.
Slack Digest
Group findings by owner team, surface the top three risks per team, post daily. The owner team sees their NHI hygiene before the security team has to ask.
Trend Tracking
Archive each run's JSON; diff against the previous run. The metric that matters isn't today's count — it's whether the count is going down month over month.