Simon Vedder

Cloud Engineer · Zürich

Read-only audit · HTML report

AppLifecycleAnalyzer

Find expiring credentials, inactive apps and abandoned registrations in your Entra tenant. One file, read-only, one HTML report you can filter and hand over.

iwr https://raw.githubusercontent.com/simon-vedder/app-lifecycle-analyzer/main/AppLifecycleAnalyzer.ps1 -OutFile AppLifecycleAnalyzer.ps1

licenseMITPowerShell7+accessread-onlyinstallone file, nothing to installcoversEntra ID

pwsh · one file, nothing installed
# One file. Read it first if you like.
iwr https://raw.githubusercontent.com/simon-vedder/app-lifecycle-analyzer/main/AppLifecycleAnalyzer.ps1 -OutFile AppLifecycleAnalyzer.ps1
# Sign in with read scopes only, read, write the report.
./AppLifecycleAnalyzer.ps1
  Found 214 app registrations
  12 of 214 apps are deactivated (isDisabled = true)
  Total apps with sign-in info: 168
  Expired creds:     23
  Inactive (>90 d):  41
Report written: ./AppLifecycleAnalysis_20260908-091412.html

Quick start

Download it, run it, read the report

From a workstation with PowerShell 7. Three delegated read scopes on Graph are all it needs. It reads your tenant and writes one HTML file, and changes nothing.

Needs three Graph read scopes, and Entra ID P1 for the activity data · Prerequisites

  1. Get the script

    One file. Read it first if you like.

    iwr 'https://raw.githubusercontent.com/simon-vedder/app-lifecycle-analyzer/main/AppLifecycleAnalyzer.ps1' -OutFile 'AppLifecycleAnalyzer.ps1'
  2. Run it

    It signs you in read-only, reads every registration, and writes the report next to itself. It needs Microsoft.Graph.Authentication and Microsoft.Graph.Applications, and offers to install them.

    ./AppLifecycleAnalyzer.ps1
  3. Choose what counts as dormant and expiring

    The defaults are 90 days without a sign-in and 30 days to expiry. Both are yours to tighten.

    ./AppLifecycleAnalyzer.ps1 -InactiveDays 60 -ExpiryWarningDays 14
  4. Name a tenant and a path

    For a report you hand to somebody, in a tenant that is not your default.

    ./AppLifecycleAnalyzer.ps1 -TenantId 'contoso.onmicrosoft.com' -OutputPath './apps-q3.html'

Every parameter, with the permissions it needs

What it does

What it does

Every credential

Secrets, certificates and federated credentials per app, each with the state of its expiry against your warning window.

Two sign-in sources merged

The report endpoint and the recent sign-in logs, laid over each other, because neither is complete on its own.

Apps with no service principal

A registration nobody ever consented to is named as that, not left as a blank row.

Dormant told from disabled

No recorded sign-in, no credential, and the deactivated flag are three different states.

Filter

One HTML file with tiles and filters, and a CSV export of whatever you filtered to.

The cleanup command in the row

Remove-MgApplicationPassword with the keyId filled in, per credential or in bulk. The script never runs it.

Under the hood

Know what to renew and what to investigate

One pass over the tenant, four Graph reads, one file out. Everything the report shows is in the file it writes — no external resources, no call home.

  1. Connect and collect

    Three delegated read scopes on Graph, consented at sign-in, read-only unless you ask for more with -RequestWriteScopes. It reads every app registration with its secrets, certificates and federated credentials; every service principal; the deactivated flag; and sign-in activity from the report endpoint with the recent sign-in logs merged over it, because neither source is complete on its own.

  2. Classify

    Each credential gets an expiry state against your warning window, each app an activity state against your inactivity window. Apps with no service principal, no credential or no recorded sign-in are called that, not left blank.

  3. Decide

    One HTML file with tiles, filters, sortable columns and a CSV export of whatever you filtered to. Each row carries the command that would clean it up; a detail view shows every credential with its own.

Proof

What it shows

one file, no external resources
The HTML report: summary tiles, filters, and one row per app with expiry, next expiry date, activity, credentials and a cleanup button

FAQ

Questions people ask first

Is it read-only?

Yes, by default. It asks for Application.Read.All, Directory.Read.All and AuditLog.Read.All, reads, and writes an HTML file. The cleanup commands in the report are text you copy. If you want to run them from the same session you have to pass -RequestWriteScopes, which adds Application.ReadWrite.All to the consent — a deliberate, visible opt-in.

Do I need Entra ID P1 or P2?

Only for sign-in activity, which comes from the audit logs. Without a licence the report still lists every app, every credential and every expiry date; the activity column says there is no sign-in data rather than guessing that the app is dormant.

Why does it read the beta endpoint?

Three things only exist there today — the deactivated flag on an application, the service principal sign-in activity report, and the recent sign-in logs. That is a real dependency on a surface Microsoft can change; if a call fails the script warns and carries on with what it has instead of stopping.

What about managed identities?

Out of scope. They have no secrets to expire and a different cleanup path, so mixing them into this report would make it longer without making it more useful. This covers app registrations and the service principals behind them.

How long does a run take?

A handful of paged Graph calls for the whole tenant, not one per app. A few hundred registrations is a couple of minutes, and most of that is waiting on the sign-in report.

What does the report need to open?

A browser. It is one HTML file with the data, the filters and the styling inside it — no CDN, no fonts to fetch, nothing that stops working when you mail it to someone.

Need the list with the context behind it?

Cleaning up app registrations is mostly a conversation, not a script — who owned this, what breaks if it goes, who renews the one that stays. I run this audit as the opening step of that conversation and stay for the rest of it.

Get in touch