← IAM Ideas
IAM Ideas 2026-07-27

Iiq Mover Entitlement Residue Auditor

Finds access an employee's old department left behind after a manager or department change — because IIQ's built-in Manager Transfer event prints two names to a log and stops there.

Iiq Mover Entitlement Residue Auditor

IIQ Mover Entitlement Residue Auditor

Finds access an employee's old department left behind after a manager or department change — because IIQ's built-in Manager Transfer event prints two names to a log and stops there.

Date: 2026-07-27 Type: Utility Theme: Protect + Detect Platform: SailPoint IdentityIQ Status: Idea

What it is

A Python utility that scans recent Mover events (manager or department changes) and flags entitlements the identity still holds from before the transfer that don't match what their new department's peers actually use. It's read-only: it reports candidates for review and doesn't revoke anything.

Who it serves

IAM engineers running the pre-certification access checks, and the identity governance lead who has to explain to an auditor why someone in Treasury still has AP payment-run rights eight months after leaving Accounts Payable.

The IIQ pain it addresses

IIQ ships a Manager Transfer lifecycle event, triggered on a Manager Change to an identity. Out of the box, it prints the old and new manager's names to sysout and takes no action on identity or entitlements. SailPoint's own documentation says shops are expected to customize it, usually to fire a certification for the new manager or provision birthright access for the new team. Plenty of shops never get around to that customization, or only handle the provisioning half and skip the review.

That leaves a gap Joiner/Leaver tooling doesn't cover: the person didn't leave, so nothing disables their accounts, and nothing about a department change forces a look at what they already have. Access from the old role just sits there. It's not orphaned; the account is active, the identity is active, and none of the usual dormant-account or terminated-user reports catch it. It only shows up if a certifier happens to notice during the next scheduled campaign, which might be months away.

How it works

  1. Pull recent Mover events. IIQ's Access History captures Identity Attribute Change events (manager, department) as part of its nightly Dispatch Access History run, alongside Entitlement Added/Removed events for the same identities. The utility reads these from a replica of the Access History database, filtered to the configured lookback window (sample-config.json ships with 150 days so the bundled demo data reads cleanly; 90 days is closer to a real quarterly cadence).
  2. Snapshot before and after. For each Mover, it builds the entitlement set held the day before the transfer and the set held as of the extract date. The intersection is what survived the transfer untouched.
  3. Check it against the new team. Each surviving entitlement gets compared to the mover's new-department peer group. If fewer than peer_group_min_support of their peers hold it, and there are enough peers to make that comparison meaningful (peer_group_min_report_count), it's flagged. An entitlement everyone in the new team already carries isn't residue; it's probably just typical for the role.
  4. Rank by severity. Privileged entitlements held more than 30 days past the transfer come back HIGH. Everything else privileged, or non-privileged past 60 days, comes back MEDIUM. The rest is LOW.
  5. Report. Prints a summary and findings table to stdout; writes mover-residue-findings.json unless --dry-run is set. That JSON is meant to feed a targeted Manager Access Review certification for the new manager, or a ticket. The utility stops at detection.

What's in this folder

  • README.md — this file
  • requirements.md — functional, non-functional, and security requirements
  • script.py — Python 3.10+ utility, standard library only
  • sample-config.json — illustrative configuration (.invalid host, password sourced from an env var name, not a literal value)
  • sample-output.txt — stdout from four sample runs: a full run, a --dry-run, and two failure cases (missing config file, missing required key)

How to run / read it

python3 script.py --config sample-config.json

Add --dry-run to see the findings without writing mover-residue-findings.json. No dependencies to install; it's standard library only. access_history_db.mode is set to "sample" in the shipped config, so it runs against an embedded synthetic dataset with no database connection or credentials involved.

Read sample-output.txt to see all four runs, including what the two failure modes print to stderr and their exit codes.

Estimated impact

The bundled sample (8 Mover events, a synthetic entitlement history across seven applications) surfaces 9 residue findings, 3 of them privileged. Scaled to an organization where Mover events run a few dozen per quarter (a fraction of total JML volume, since most lifecycle activity is Joiner/Leaver, not internal transfer), this utility turns a gap that's currently invisible between certification cycles into a short list a manager or IAM engineer can clear in one sitting, instead of waiting for the next scheduled campaign to catch it by accident.

Why this fits an IIQ shop with 500+ connectors

Movers don't stay inside one connector. A transfer from Accounts Payable to Treasury touches SAP ECC, Workday, and an Active Directory group in the sample data alone; a transfer from Field Support to Cloud Platform Engineering touches ServiceNow, AWS IAM, GitHub, and a PagerDuty-managed AD group. At 500+ connectors, the old-department access a mover keeps is scattered across whichever applications that department used, which means no single connector's aggregation task or certification campaign is positioned to catch the leftover access on its own. A cross-application check run off Access History, rather than off any one connector's data, is the only vantage point that sees the whole picture.

Sources

Requirements

Requirements — IIQ Mover Entitlement Residue Auditor

Purpose

Detect entitlements that a transferred identity (a Mover — a manager or department change) still holds after the transfer date, that are not part of the entitlement profile typical of their new department's current peers.

Functional requirements

  • FR1: The script MUST accept a --config <path> argument pointing to a JSON config file matching the shape of sample-config.json.
  • FR2: The script MUST read lookback_days, peer_group_min_support, peer_group_min_report_count, and output_path from the config file and fail with a non-zero exit code and a descriptive stderr message if any is missing.
  • FR3: The script MUST identify Mover events — identities whose manager or department attribute changed — within the last lookback_days days of the extract date. In production this is the Manager Change trigger IIQ's built-in Manager Transfer lifecycle event fires on; in this artifact it is read from the Access History event stream (see IIQ surface below), not from IIQ's own lifecycle event log, because the Manager Transfer event's default action takes no action on identity or entitlements and does not itself persist a queryable "still holds pre-transfer access" record.
  • FR4: For each Mover event, the script MUST compute the set of entitlements held immediately before the transfer date and the set held as of the extract date, and MUST treat their intersection as residue candidates.
  • FR5: The script MUST compare each residue candidate against the current entitlement profile of the mover's new-department peer group (other identities currently in department_after) and MUST only flag candidates below peer_group_min_support where the peer group has at least peer_group_min_report_count members. Candidates with insufficient peer data (peer count below the threshold) MUST be skipped, not flagged, to avoid false positives from thin peer samples.
  • FR6: The script MUST assign a HIGH / MEDIUM / LOW severity to each finding based on the entitlement's privileged flag and days elapsed since the transfer.
  • FR7: The script MUST print a summary (event count, finding count, severity breakdown) and a findings table to stdout on every run.
  • FR8: Unless --dry-run is passed, the script MUST write all findings to the JSON file named in output_path.
  • FR9: The script MUST run against the bundled synthetic dataset with no network access and no credentials, using sample-config.json as-is.

Non-functional requirements

  • NFR1: No third-party dependencies — standard library only (argparse, json, dataclasses, datetime).
  • NFR2: Python 3.10+ (uses X | None union syntax and dataclass(frozen=True)).
  • NFR3: Every function signature is type-hinted.
  • NFR4: The script must never write to output_path when --dry-run is set.
  • NFR5: Runtime for the bundled sample dataset (8 transfer events, ~35 entitlement grant rows) must stay under 1 second — the algorithm is O(events × grants), which is acceptable at the scale a Mover-only feed produces (typically dozens to low hundreds of transfers per lookback window, not the full identity population).

Security requirements

  • SR1: No hard-coded credentials anywhere in script.py or sample-config.json.
  • SR2: sample-config.json MUST reference the production database password only via an environment-variable name (password_env), never a literal value.
  • SR3: All illustrative hostnames MUST use the .invalid TLD.
  • SR4: The script is read-only against IIQ / the Access History replica — it does not call any provisioning, workflow-launch, or write endpoint. Remediation (revoking residue access, or opening a targeted certification for the new manager) is an explicit human or downstream-workflow decision, not something this utility performs.

Production data-source note

load_transfer_events() and load_entitlement_grants() are stubbed to return an embedded synthetic dataset when access_history_db.mode is "sample". In production, mode is "live" and both functions instead query a read replica of the Access History database — populated nightly by IIQ's Dispatch Access History task — for Identity Attribute Change events (manager/department) and Entitlement Added/Removed events, joined against spt_managed_attribute for the privileged flag. This script does not attempt a live database connection in the sample artifact; wiring the real query is a follow-on implementation task, not part of this idea's scope.

Out of scope

  • Auto-remediation (revoking flagged entitlements, disabling accounts).
  • Launching an IIQ certification directly — the findings JSON is designed to be a clean hand-off to a downstream "targeted Manager Access Review" certification generation task, not a replacement for one.
  • Movers detected via any mechanism other than a manager or department attribute change (e.g., title-only changes with no manager/department shift are out of scope).
  • Cross-platform variants (Identity Security Cloud / IdentityNow use a different lifecycle event and history model).

More from IAM Ideas