Elevation Window Auditor
Normalizes time-boxed privileged elevations from Entra ID PIM, AWS IAM Identity Center, and Okta Privileged Access into one ledger, and flags the ones still active past their approved window, missing…
Elevation Window Auditor
Normalizes time-boxed privileged elevations from Entra ID PIM, AWS IAM Identity Center, and Okta Privileged Access into one ledger, and flags the ones still active past their approved window, missing a required justification or ticket, or granted with no window at all.
Date: 2026-11-05 Theme: Detect + Protect Type: Web App Platform: PIM / JIT Access Status: Prototype
What it is
A single-page console that loads elevation events from three platforms into one normalized table, computes six policy checks against each record, and opens with a banner naming the worst case instead of making the reader scroll for it. Click any row to see why it was flagged, in plain language tied to the actual field values, plus the raw adapter-shaped payload behind it.
Who it serves
The security engineer who owns PIM policy, the on-call lead who approves break-glass activations, or whoever has to answer "was anyone holding admin longer than they should have?" after an incident review. Also useful heading into a SOX or ISO 27001 walkthrough, where "show me every elevated session from the quarter and what it was for" is exactly the question an auditor asks.
The IAM pain it addresses
Entra PIM's own documentation says a deactivated role is removed from the active assignment within seconds. But it also admits the catch: if a downstream application cached the fact that a user held a role, the user "may still get access" after that role deactivates, and the fix depends on the application's own architecture. That's a real gap between what the PIM schedule says happened and what a connector reading from a downstream system actually observes, and nothing in the PIM console itself tells you where that gap exists across your own fleet of integrations.
AWS and Okta each have a version of the same problem from a different angle. AWS IAM Identity Center enforces a hard session cap of 12 hours per permission set, but it ships no approval workflow of its own. Microsoft's own blog on bridging Entra PIM into IAM Identity Center is explicit that the approval step comes from PIM, not from AWS. AWS's CloudTrail AssumeRole event carries the credential's expiration timestamp inside the response, but there's no separate "session expired" event to tell you it actually ended on schedule — you'd have to go looking for API calls made after that timestamp to know if it didn't. Okta Privileged Access is a distinct, separately licensed product from core Okta, and its own marketing page is the only place that spells out that access is time-bound and expires automatically. Useful, but it still leaves the "did it actually expire" question to whoever bothers to check the session log.
None of the three platforms will tell you, on their own, whether an elevation someone approved for ninety minutes is still sitting open six hours later. The sample data seeds exactly that case: alice.renner@example.com activated Global Administrator in Entra PIM for a four-hour window to roll back a Conditional Access policy after an SSO outage, with a ticket and an approver attached — and the last read from the connector, four hours after that window closed, still shows the assignment active. That's the record the banner names on load.
How it works
- Ledger table. Every elevation from all three adapters in one sortable list — platform, principal, role or resource, activation time, expected expiry, last observed state, and flags. Flagged rows sort to the top.
- Worst-case banner. If any record is still active past its expected expiry, that's the headline, ranked by how far past expiry it is — not just whichever record happens to carry the most flags. A standing SSH grant with no window is a real problem too, but a Global Admin session that outlived its approval by six hours is the one that gets read first.
- Search and filter. Text search across principal and role, a platform dropdown, and a flagged-only toggle.
- Detail panel. Selecting a record shows every field, which of the six checks fired and why (quoting the actual duration, ticket, or approver value involved), and the adapter-shaped raw event behind it — a Graph
unifiedRoleAssignmentScheduleRequest, a CloudTrailAssumeRolerecord, or an Okta Privileged Access grant event. Each snippet carries a visible note that it's structurally illustrative and needs validating against a real tenant before anyone treats it as paste-ready. - Demo revoke. A "Revoke now" button on flagged, still-active records appends to a local action log and flips the record's observed state. It doesn't call any platform API — this is a prototype, and the button says so.
What's in this folder
README.md— this filecover-image.png— cover art for the idea's card and detail pagemetadata.md— generation metadata and sourcingrequirements.md— the normalized elevation schema, the six flag rules with their exact conditions, and the banner's tie-break logicindex.html— the single-page app; carries an inline copy of the sample data so it still runs iffetch()of local JSON is blocked underfile://style.css— layout and the navy/electric-blue/teal visual treatmentscript.js— data loading, the flag computation, filtering, detail rendering, and the demo revoke actionsample-data.json— 16 elevations across the three adapters: 10 carry at least one flag, 6 are clean controls so the reader can see what "fine" looks like next to what isn't
How to run / read it
Open index.html in any modern browser. No build step, no server, no login.
Estimated impact
No measured number — this is a prototype, not a result from a live deployment. The credible claim is narrower and still worth stating: once a security team is running PIM, IAM Identity Center, and Okta Privileged Access at once, the only way today to catch an overrun elevation is someone manually cross-referencing three consoles' session logs against three sets of approval records, and that check almost never happens outside of an incident post-mortem. A tool that runs the six comparisons automatically turns that into something that can run every time an access review is due, not just after something has already gone wrong.
Adapters and scale
Three adapters, each reading a documented API or log surface: Entra ID PIM (Graph unifiedRoleEligibilityScheduleRequest and unifiedRoleAssignmentScheduleRequest, including scheduleInfo.expiration and ticketInfo), AWS IAM Identity Center (permission-set session duration settings plus CloudTrail AssumeRole events), and Okta Privileged Access (its grant-activation log). None of these require a full identity sync — only the activation/session events for a time window, which is what makes this hold up at 50k+ identities and 500+ apps: the volume driver is elevations per day across whatever subset of tier-0 roles and resources an org actually gates behind JIT, not total headcount. A one-person security team checking three consoles by hand doesn't scale past a handful of elevations a week; a script pulling the same three event feeds does, regardless of how many identities sit behind them.
Sources
- Official documentation / standards:
https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-how-to-activate-role— confirms PIM removes an active assignment "within seconds" of deactivation or expiration, and documents the downstream application-caching gap that can leave access visible after a role deactivateshttps://learn.microsoft.com/en-us/graph/api/resources/unifiedroleassignmentschedulerequest?view=graph-rest-1.0andhttps://learn.microsoft.com/en-us/graph/api/resources/unifiedroleeligibilityschedulerequest?view=graph-rest-1.0— the Graph object shapes,scheduleInfo.expirationandticketInfofields used in the Entra ID sample records and raw snippetshttps://docs.aws.amazon.com/singlesignon/latest/userguide/howtosessionduration.html— permission-set session duration defaults (1 hour) and the 12-hour maximumhttps://docs.aws.amazon.com/IAM/latest/UserGuide/cloudtrail-integration.html— confirmsAssumeRoleis the logged event and that the credentialexpirationtimestamp sits insideresponseElements, not as a separate expiry eventhttps://www.okta.com/products/privileged-access/— confirms Okta Privileged Access is a distinct, separately licensed product and that its JIT grants are described as time-bound and auto-expiringhttps://top10proactive.owasp.org/archive/2024/the-top-10/c1-accesscontrol/— the OWASP Proactive Controls section naming just-in-time, just-enough access as a least-privilege control
- Community / current research:
https://aws.amazon.com/blogs/security/implementing-just-in-time-privileged-access-to-aws-with-microsoft-entra-and-aws-iam-identity-center/— AWS's own write-up confirming the approval workflow in this pairing comes from Entra PIM, not from IAM Identity Center itselfhttps://endpointweekly.com/blog/entra-pim-license-expiry-deletes-eligible-assignments.html(2026-07-28) — a practitioner write-up on a related failure mode: a lapsed Entra ID P2 license silently removes PIM-eligible role assignments while leaving standing, permanent admin assignments in the same tenant untouched, the kind of silent breakage this idea's approach is meant to catch before an incident does
Requirements
Requirements — Elevation Window Auditor
Purpose
Normalize time-boxed privileged-access activations from three adapters into one ledger and flag any activation where the approved window, the approval metadata, or the observed end state doesn't match policy. Surface the worst case on load without the reader hunting for it.
Normalized elevation record
Every adapter maps its native activation object to this shape before it reaches the UI:
| Field | Type | Notes |
|---|---|---|
elevationId |
string | Stable ID, adapter-prefixed (ENTRA-, AWS-, OKTA-) |
platform |
string | Entra ID PIM | AWS IAM Identity Center | Okta Privileged Access |
principal |
string | Email/UPN of the person who activated |
role |
string | Role name (Entra), permission set (AWS), or target resource/role (Okta) |
scope |
string | Directory scope, AWS account ID, or server/resource name |
activatedAt |
ISO 8601 | When the activation request was granted |
approvedDurationMinutes |
number | null | null means no expiration was set on the activation |
expectedExpiresAt |
ISO 8601 | null | activatedAt + approvedDurationMinutes; null if no window |
justification |
string | null | Free-text reason attached to the request |
ticketNumber |
string | null | Linked change/incident ticket, if any |
approver |
string | null | Who approved the request; null if self-activated with no approval step |
requiresApproval |
boolean | Org policy: does this role/resource require an approver? |
requiresTicket |
boolean | Org policy: does this role/resource require a linked ticket? |
observedUsageMinutes |
number | null | Minutes between activation and the last API call/session event seen in the platform's own log, where that signal exists |
lastObservedState |
string | active | revoked, as last read from the platform |
lastObservedAt |
ISO 8601 | Timestamp of that last read |
sourceSnippet |
object | Illustrative raw payload shaped like the adapter's own API/log object |
Flag rules
All flags are computed from fields already in the record — never from the reader's wall-clock time, so the demo reads the same regardless of when it's opened.
| Flag | Condition | What it means |
|---|---|---|
OVERRUN |
lastObservedState === 'active' AND expectedExpiresAt is set AND lastObservedAt > expectedExpiresAt |
The platform's own schedule says this should have ended; the last read says it hasn't. |
NO_WINDOW |
approvedDurationMinutes === null AND expectedExpiresAt === null |
Activated through a JIT workflow but given no expiration — functionally a standing grant. |
MISSING_JUSTIFICATION |
justification is null/empty |
No reason recorded for the activation. |
MISSING_TICKET |
requiresTicket === true AND ticketNumber is null |
Policy requires a linked ticket for this role/resource; none is attached. |
NO_APPROVER |
requiresApproval === true AND approver is null |
Policy requires approval; no approver is recorded. |
WINDOW_OVERSIZED |
observedUsageMinutes and approvedDurationMinutes are both set, AND observedUsageMinutes < approvedDurationMinutes * 0.15 |
The granted window is at least 6.5x longer than the work it was used for. |
A record can carry more than one flag. A record with none of the above renders clean, and the UI must show at least one clean record alongside the flagged ones so the reader can see the contrast.
Functional requirements
- On load, parse
sample-data.jsonand render every record in a sortable table: platform, principal, role/resource, activated, expected expiry, last observed state, flags. - Flagged records sort above clean ones by default. A banner names the single worst record: if any record carries
OVERRUN, the banner names the one with the largest gap betweenexpectedExpiresAtandlastObservedAt; otherwise it names the record with the most flags. This keeps a still-active, past-expiry elevation as the headline even when a lower-severity record happens to carry more flags. - Provide a text search over
principalandrole, a platform filter dropdown, and a "flagged only" toggle. Filters combine with AND. - Selecting a row opens a detail panel showing every field plus the adapter-shaped
sourceSnippet, with a visible line stating the snippet is structurally illustrative and must be validated against the target environment before use. - The detail panel lists which flag rules fired and why, in plain language referencing the actual field values (not just the flag name).
- A "Revoke now" button on flagged, still-active records appends an entry to a local, in-memory action log rendered at the bottom of the page. It does not call any external API — this is a prototype, not a live integration — and the button label/tooltip says so.
- No build step.
index.htmlopens directly overfile://. Iffetch()ofsample-data.jsonis blocked underfile://in the reader's browser, fall back to an inline copy of the same array embedded inindex.html. - Vanilla HTML/CSS/JS only. No external runtime dependencies.
Out of scope
- Live adapter connections (Graph, AWS SDK, Okta API calls).
- Writing an actual revocation back to any platform.
- Persisting the in-memory action log across a page reload.
More from IAM Ideas