Skip to main content
INS // Insights

Privilege Creep Detection That Builds a Revoke List

Updated September 2026 · 5 min read

Quarterly access reviews fail in a predictable way. Managers get a spreadsheet of everyone in their org and click Approve because the names look familiar. Nobody asks whether the GitHub admin grant from two roles ago is still used. Privilege creep detection is the job that turns that spreadsheet into a revoke list: current grants on one side, last-used on the other, leftover access in the middle.

We do not replace human review. We stop wasting it on grants that have a usage signal and an obvious owner. NIST AC-6 is least privilege as a continuous property, not a one-time provisioning choice. TSC CC6.2 samples whether recertification is real. A 100% approve rate with no revokes is a smell, not a control.

Why Privilege Creep Detection Has to Beat Recertification

Joiners get birthright. Movers get new birthright and keep the old groups. Project grants never expire. Someone is added to prod-break-glass for an incident and stays. The next review shows "member of 14 groups" with no last-used column. The manager is not lazy. The packet is useless.

A Salesforce user access review that only lists profiles has the same hole. A user access review automation job that only dumps memberships has it too. Creep is a diff problem.

The Usage-vs-Grant Job We Build

For each in-scope system we can query, we join three facts:

  1. Grant — group, role, permission set, IAM policy, GitHub team, Okta group.
  2. Usage — last successful use of that grant (app last login, AWS last-used on the action family, GitHub last org activity, Salesforce last login, SaaS SCIM last sign-in).
  3. Role context — current HRIS job code and the birthright map from joiner mover leaver automation.
def revoke_candidates(grants, usage, birthright, unused_days=90):
    out = []
    for g in grants:
        last = usage.last_used(g.user, g.entitlement)
        in_birthright = g.entitlement in birthright[g.job_code]
        stale = last is None or (today() - last).days > unused_days
        leftover = (not in_birthright) and stale
        privileged = g.entitlement in PRIVILEGED
        if leftover or (privileged and stale):
            out.append({
                "user": g.user,
                "entitlement": g.entitlement,
                "system": g.system,
                "last_used": last,
                "reason": "not_birthright_and_unused" if leftover else "privileged_unused",
                "suggested": "revoke",
            })
    return out

Thresholds are per system, not a religion. Ninety days is a starting point for a SaaS admin role. Break-glass AWS roles might be 30 days of non-use and still valid if they are explicitly time-boxed. We do not auto-revoke production IAM from a heuristic. We put it on the list.

Systems with no usage API (the homegrown admin panel) get a weaker signal: last login to the app, or "unknown." Unknown privileged stays on the list. Unknown birthright read-only does not. That bias is deliberate.

What Reviewers See

The default view is not the full population. It is:

  • Suggested revokes, grouped by manager
  • Privileged unused, even if the grant is still in birthright (maybe the birthright map is wrong)
  • Grants that are in neither birthright nor usage — the project leftovers
  • A small "keep" path that requires a reason and an expiry

Rubber-stamp Approve All is disabled for the revoke queue. Reviewers can keep an item. They have to type why. Those keep-reasons become the next campaign's first filter: if the same unused admin is kept three times with "just in case," that is a finding for the birthright map, not a pass.

This is the operational half of SOC 2 CC6.2 recertification evidence. The evidence is decisions on a ranked list, not a screenshot of a complete user export.

Movers Are the Primary Source

If you only detect creep at quarter-end, you are late. A transfer should have already revoked prior-role access. When the mover event is missing or partial, privilege creep detection is the backstop. We tag candidates whose job code changed inside the window. Those rows go to the new manager and the old manager, because the old manager is the one who knows the leftover was real.

Contractors and guests are a separate unused-access pile (see Microsoft 365 guest access review). Employees accumulate internal grants. Do not mix the packets. Reviewers will approve guests they have never met if they appear between two direct reports.

What We Will Not Auto-Revoke

  • Anything that would lock a human out of email or IdP sign-in without the offboarding event
  • Privileged AWS or production deploy roles without a second approver
  • Grants whose usage API is down this week (we fail open on automation, fail closed on reporting — the list still shows "usage unknown")

The ceiling is honest: last-used is a heuristic. Some entitlements are watch-only and never "used." We maintain an allowlist of those entitlements per system rather than pretending the algorithm is perfect. The upgrade path is per-permission telemetry where the vendor exposes it (AWS IAM last-used, Salesforce LoginHistory). Until then, heuristic plus human.

Evidence for the Sample

  • Campaign id, window start/end, systems in scope
  • Count of grants reviewed vs suggested revokes vs actual revokes
  • Timestamped decisions with reviewer identity
  • Follow-up: entitlements actually removed (API write or ticket close)

A 0% revoke rate with a large unused-privileged count is a control failure even if every row is "approved." We put that ratio on the cover sheet so nobody hides it in a 40-tab workbook.

Privilege creep detection sits in security automation next to automate access reviews. The GRC tool still hosts the campaign if you want it there. We feed it a list worth reviewing.

See what an Access & Credential Governance Diagnostic finds in your environment: rutagon.com/contact · 907-841-8407 · contact@rutagon.com.

Frequently Asked Questions

Does privilege creep detection auto-revoke access?

Not for production-privileged grants. We generate a revoke list and write back only after a reviewer decision, except for already-terminated workers who should have been caught by offboarding. Heuristic revoke without a human is how you brick an on-call rotation.

What if a system has no last-used API?

We downgrade to last app login or mark usage unknown. Privileged unknown stays in the queue. Low-risk birthright grants do not. The gap is documented so the next engineering sprint can add a better signal.

How is this different from a normal quarterly access review?

A normal review asks "does this person still work here and seem to belong." Creep detection asks "which grants are unused or leftover from a prior role" and makes those the default work queue.

Can we use the same unused-days threshold everywhere?

No. Break-glass, data-export, and admin roles need a tighter unused window than a read-only wiki group. We set thresholds per entitlement class.

Will this replace joiner-mover-leaver automation?

No. JML should prevent the leftover. Creep detection catches what JML missed and what project grants never expired. You want both, not a quarterly cleanup as the only lifecycle.