← All KB Checks
HIGHScriptOnlyCheck

Inactive Users Retaining Roles

nowisor-inactive-users-with-roles··Source on GitHub →

Inactive Users Retaining Roles

This page describes the nowisor-inactive-users-with-roles check from the nowisor Instance Scan Pack v1.0.0. The check queries the sys_user_has_role m2m table for grants belonging to inactive (sys_user.active = false) users and emits one finding per inactive user. Each finding's evidence includes the user's name, sys_id, total role grants, and the array of role names. Findings are capped at 200 per scan to keep the report tractable on extreme cases.

What this finding means

Every finding identifies one user account marked inactive (sys_user.active = false) that still carries one or more role assignments in sys_user_has_role. The combination — inactive flag set, role grants intact — represents incomplete deprovisioning.

The risk model is structural: while the user is inactive, the role grants are dormant — the platform does not allow inactive users to authenticate, so the grants don't translate to immediate access. But the grants remain attached to the account. Any path that reactivates the account (a compromised user_admin who flips the active flag, an integration that bulk-syncs from an HR system that incorrectly marks someone re-hired, an update set replay that restores an older state of the user record) restores all the grants without a new role-grant audit event. The attacker inherits whatever privileges the inactive account held, including admin if that's what the deprovisioned employee had.

A note on the check's history: the v1.0.0-build version of this check was a TableCheck that queried sys_user with the encoded query active=false^rolesISNOTEMPTY. That predicate relied on sys_user.roles — the legacy denormalized comma-list field that ServiceNow maintains for backward compatibility but does not reliably populate on Zurich Patch 6. Tier 2 verification surfaced the gap (the platform's source-of-truth for role grants is the sys_user_has_role m2m table, not the legacy field), and the check was rewritten as a ScriptOnlyCheck that queries the m2m directly. The Tier 2 retrospective in the agent pack documents this history; from v1.0.0 GA forward, the check produces correct findings against the actual role-grant data.

Why it matters for your compliance

Stale access is one of the most-cited audit findings in IAM-focused regulatory frameworks because the cost of remediation is low and the failure mode is well-documented.

NIS2 Article 21§2(j) — identity and access management. NIS2's IAM expectations explicitly include the deprovisioning side of the lifecycle. An entity that can demonstrate joining and granting controls but cannot demonstrate effective revocation is failing the symmetric obligation. The finding is direct evidence that the revocation step in the JML process is not coupled to the role-grant lifecycle.

ISO 27001 A.5.18 — access rights. A.5.18's control text addresses the full lifecycle: granted, reviewed, revoked. The "revoked" obligation requires that access rights be removed when no longer required — including when the user is no longer active. Inactive users retaining role grants is the canonical A.5.18 finding.

The check is severity 2 (high), not critical, because the impact is dependent on a secondary event — the reactivation of the account. A finding on its own is not active exposure; it's pre-positioned exposure for any future account-reactivation event, whether legitimate or malicious. Closing the finding is preventive rather than reactive.

The attack path

The attack path runs through reactivation. The inactive user is the staging point; the reactivation is the trigger.

Step 1 — Identify a target inactive user with privileged role grants. The attacker enumerates sys_user with active=false and cross-references against sys_user_has_role. They find an account: a former IT director who left the company eighteen months ago, marked inactive but with admin, security_admin, and user_admin roles intact.

Step 2 — Find a reactivation path. The attacker needs to flip sys_user.active from false to true on the target account. Options vary by instance:

Step 3 — Authenticate. The attacker authenticates as the (newly-reactivated) user — using local credentials if the account had them, or via SSO if the upstream identity is still provisioned. The platform sees a valid authentication for an active user; nothing in the login flow flags the recent reactivation.

Step 4 — Operate. The reactivated account now has its original role grants: admin, security_admin, user_admin. The attacker has the same privileges the former employee held. The role-grant audit history shows the grants being created eighteen months ago, before the user was inactive — there is no recent grant event to alert on. Operations performed with these grants appear as normal admin activity.

The defense is twofold. Revoke role grants when the user is deactivated (the structural fix this check drives). And monitor reactivation events as anomalies — a sys_user.active flip from false to true is rare and worth flagging.

How to fix it

The fix is a deprovisioning process integration, not a single configuration change. For an immediate cleanup, follow the per-user pattern below; for the structural fix, automate the cleanup as part of the JML process.

Immediate cleanup — for each finding:

  1. Open the user record. Confirm the user is genuinely inactive (sometimes the active flag has been incorrectly set on a current employee — surface those as a separate issue).

  2. Open the user's Has Role related list. This shows the rows in sys_user_has_role linking the user to their roles.

  3. Decide on each grant: delete (preferred for terminated users), or document a business reason to retain (rare; usually only applies to regulated retention periods where the account must be preserved for audit but should not be reactivatable to its prior privileges).

  4. For deletions: remove the m2m rows. The user retains the inactive flag; only the role grants are cleared. Re-running the scan should show the finding cleared for this user.

Structural fix — automate deprovisioning:

Set up a Business Rule or scheduled job that triggers when sys_user.active transitions from true to false:

// Conceptual outline — Business Rule on sys_user (after-update)
// Triggers when active changes to false
if (previous.active == true && current.active == false) {
    var roleGr = new GlideRecord('sys_user_has_role');
    roleGr.addQuery('user', current.sys_id);
    roleGr.query();
    while (roleGr.next()) {
        // Archive or log the grant before deletion (audit evidence)
        roleGr.deleteRecord();
    }
}

The archive-before-delete pattern preserves evidence of what was revoked (important for compliance documentation) while preventing dormant privileged grants.

Sub-process: integration handling. If your sys_user.active flag is driven by an HR integration, ensure the integration is the trigger for the deprovisioning BR — not just the manual flip from an admin. Integrations that bulk-update users should fan-out role revocations through the same logic.

How to verify the fix

/*
 * Verify nowisor-inactive-users-with-roles remediation progress
 * Read-only, safe for production.
 * Counts inactive users still holding role grants.
 */
(function inactiveRoleHolders() {
    var distinctUsers = {};
    var gr = new GlideRecord('sys_user_has_role');
    gr.addQuery('user.active', false);
    gr.query();
    while (gr.next()) distinctUsers[gr.getValue('user')] = true;

    var count = 0;
    for (var k in distinctUsers) if (distinctUsers.hasOwnProperty(k)) count++;
    gs.print('Distinct inactive users with active role grants: ' + count);
    if (count === 0) {
        gs.print('[PASS] No stale role grants.');
    } else {
        gs.print('[REVIEW] ' + count + ' user(s) need deprovisioning.');
    }
})();

The expected end state is zero. Track the count over time as the deprovisioning automation rolls out.

A note on Aqib Mushtaq (if you see this in your findings): this was the test case discovered during Tier 2 verification on dev265484, with 42 role grants. The example is real and illustrative — even a fresh PDI can accumulate stale role grants quickly through bulk imports and test-data setup.

What to do next

Stale access compounds across the IAM cluster:

For organizations migrating to a mature JML process, the nowisor advisor product is being built to correlate inactive-user findings with the activity audit (sys_audit history per user) to surface which inactive accounts had privileged activity in the months before deactivation — the subset that warrants priority remediation. Get the IAM evidence flow →