← IAM Ideas
IAM Ideas 2026-06-01

Iiq Request Catalog Linter

Iiq Request Catalog Linter

IIQ Request Catalog Linter

A read-only linter that scores your IdentityIQ access-request catalog for the hygiene defects that confuse requesters and stall approvals — for the IAM engineer who owns the LCM request portal.

Date: 2026-06-01 Type: Utility Theme: UX + Identify Platform: SailPoint IdentityIQ Status: Idea

What it is

A small Python utility that reads every requestable ManagedAttribute (entitlement) and Bundle (role) from the IdentityIQ Entitlement Catalog and lints each one against eight catalog-hygiene rules — missing descriptions, cryptic display names, missing or terminated owners, missing Classifications, duplicate names, and dead (zero-member) requestable entries. It prints a severity-ranked report, can emit JSON/CSV for a ticket or dashboard, and exits non-zero when HIGH findings exceed a gate so it can run on a schedule. It is strictly read-only: it names what to fix, it never writes to IIQ.

Who it serves

The IAM engineer or LCM administrator who owns the access-request portal — the person who fields "what is CN=APP-FIN-GL-RW,OU=Groups,DC=corp,DC=com and do I need it?" tickets, chases approvals that silently routed to a terminated owner, and gets asked by audit why the request catalog is full of undescribed privileged entitlements.

The IIQ pain it addresses

In IIQ, account-attribute values are promoted to first-class ManagedAttribute objects in the Entitlement Catalog, where each can carry a display name, a description, an owner, a requestable flag, and Classifications. Roles (Bundle objects) carry the same request-facing metadata. At 500+ connectors and 50k+ identities, that catalog has tens of thousands of requestable rows, and the metadata rots:

  • Requesters can't tell what they're asking for. A requestable entitlement with no description, or whose display name is a raw AD DN / Azure GUID / Okta group key, shows up in the Request Access UI as gibberish. Requesters guess, request the wrong thing, or open a helpdesk ticket — and approvers rubber-stamp because they can't tell either.
  • Approvals stall on missing owners. Since IIQ 8.2 the default is to require approval on entitlement changes, and the Entitlement Update business process routes that approval to the entitlement owner"if no owner has been specified for the entitlement, the approval is routed to the fallback approver, which by default is the owner of the application." A requestable item with no owner (or a terminated owner) is an approval black hole.
  • Privileged access hides in plain sight. A privileged-looking entitlement with no Classification doesn't get caught by the risk/cert filters that key off Classifications — it's requestable, undescribed, and invisible to governance at the same time.
  • The catalog accumulates duplicates and dead entries. Two near-identical "Finance Analyst" roles, or a requestable=true entry held by zero identities, are catalog cruft that nobody owns and the OOB UI won't surface as a problem.

Native IIQ has no view that scores catalog quality. The Entitlement Catalog page lists items and lets you edit them one at a time; it does not tell you which of your 30,000 requestable rows are unfit to be shown to a human.

How it works

  1. Load the catalog via one of three adapters set in config:
    • demo — a built-in synthetic catalog (runs offline, no IIQ connection);
    • xml-export — parse an iiq console export (export -clean catalog.xml ManagedAttribute, then ... Bundle); the supported, OOB extraction path;
    • rest — a stub for a shop-deployed custom IIQ REST resource (8.4 ships no OOB ManagedAttribute REST endpoint), credentials read from an env-var named in config.
  2. Filter to requestable items only — non-requestable rows never reach the portal.
  3. Lint each item against the eight rules (all independently toggleable), plus a cross-item duplicate-display-name pass.
  4. Report to stdout: a summary line, a findings-by-rule bar chart, a top-applications rollup, and a HIGH-first detail list. Optionally write JSON and/or CSV.
  5. Gate: exit 1 if HIGH findings exceed fail_on_high_over, else 0 (config/parse errors exit 2) — so it can fail a scheduled job or a pre-launch check.

What's in this folder

  • README.md — this overview.
  • metadata.md — provenance, IIQ surface, doc references, cover prompt.
  • requirements.md — scope, rule table, behavioural contract, test checklist.
  • script.py — the linter (Python 3.10+, standard library only).
  • sample-config.json — illustrative config; demo mode, no secrets.
  • sample-output.txt — captured stdout of python script.py -c sample-config.json.
  • cover-image.png — concept-art cover (16:9, no text).

How to run / read it

# Offline demo against the built-in synthetic catalog:
python script.py -c sample-config.json
# or force demo mode regardless of config:
python script.py --demo

# Against a real catalog: set input.mode to "xml-export" and point
# input.xml_export_path at an `iiq console` export of ManagedAttribute + Bundle.

Read sample-output.txt to see what a run looks like (18 findings across the 10-item demo catalog, exit code 1 because HIGH > 0). No build, no install, no network in demo mode.

Estimated impact

For a shop with ~30k requestable catalog rows, the linter turns an open-ended "the catalog is a mess" complaint into a finite, ranked worklist in seconds. The two HIGH owner rules alone target the approvals that silently dead-end — typically a handful of multi-day delays per week that each cost an engineer 20–40 minutes to trace by hand. Run weekly as a gate, it keeps newly-aggregated entitlements from entering the portal undescribed and unowned, which is the single biggest driver of "what is this?" request-portal tickets.

Why this fits an IIQ shop with 500+ connectors

Every aggregation against those 500+ connectors mints new ManagedAttribute rows, and the long tail of connectors is exactly where display names stay raw and owners stay blank — the ManagedAttribute Customization rule only ever covered the apps someone bothered to write logic for. The linter is connector-agnostic (it reads the catalog, not each source), runs read-only off a nightly console export, and scales linearly with catalog size. It produces the worklist that a follow-on bulk-edit campaign or a tightened Customization rule then drains.

Sources

  1. Local IIQ docs (repo-relative):
  2. External research:
Requirements

Requirements — IIQ Request Catalog Linter

Scope

  • A read-only linter for the SailPoint IdentityIQ 8.4 Lifecycle Manager (LCM) access-request catalog. It inspects every requestable ManagedAttribute (entitlement) and Bundle (role) and reports catalog-hygiene defects.
  • It never writes back to IIQ. Output is a stdout report plus optional JSON/CSV.
  • It is safe to run on a schedule (cron / Windows Task Scheduler / CI) as a gate.

Inputs

  • A config JSON (sample-config.json is the reference shape). Required keys:
    • input.mode — one of demo | xml-export | rest.
    • rules — boolean map enabling/disabling each rule.
    • min_description_chars (int), privileged_keywords (string[]), fail_on_high_over (int gate), output.json_path, output.csv_path.
  • For xml-export: input.xml_export_path → an iiq console export of ManagedAttribute and Bundle objects (export -clean <file> ManagedAttribute, then ... Bundle).
  • For rest: input.rest_base_url, input.rest_resource, and input.auth_env (the name of an environment variable holding the bearer token — never the token itself).

Rules (each independently toggleable)

Rule Severity Trips when
MISSING_DESCRIPTION HIGH Requestable item has no description shown in the portal.
CRYPTIC_DISPLAY_NAME HIGH Display name is a DN / GUID / SID, or (entitlements) equals the raw value.
MISSING_OWNER HIGH Requestable but no owner → approval falls back to the application owner.
STALE_OWNER HIGH Owner identity is inactive (terminated).
SHORT_DESCRIPTION LOW Description shorter than min_description_chars or just echoes the name.
NO_CLASSIFICATION MEDIUM Privileged-looking entitlement with no Classification attached.
DUPLICATE_DISPLAY_NAME MEDIUM Two+ requestable items share an identical display name.
ORPHANED_REQUESTABLE MEDIUM Requestable but held by zero identities (likely deprecated).

Behavioural requirements

  • Non-requestable catalog items are skipped (they never reach the portal).
  • The demo mode must run with no IIQ connection and no network.
  • Argument parsing: -c/--config <path> and --demo (forces demo mode).
  • Exit codes: 0 clean/within-gate, 1 HIGH findings exceed fail_on_high_over, 2 configuration/parse error. The gate is what lets it run as a CI check.
  • No third-party dependencies — Python 3.10+ standard library only.
  • No secrets in source or config; the only credential reference is an env-var name.

Non-goals

  • No write-back / remediation (it names targets; fixing them is a gated workflow, e.g. a ManagedAttribute Customization rule or a bulk-edit campaign).
  • No live correlation to certifications or risk scores (separate artifacts).
  • No GUI — this is a pipeline/CLI tool; its output feeds a report or a ticket.

Test checklist

  • python script.py --demo exits 1 and prints the 8-rule breakdown.
  • Setting every rules.* to false yields 0 findings and exit 0.
  • A non-requestable item in the catalog never appears in findings.
  • xml-export mode parses <ManagedAttribute> and <Bundle> and tolerates missing optional fields without crashing.
  • rest mode raises a clear NotImplementedError (stub) and exits 2.
  • Missing config path exits 2 with a stderr message.

More from IAM Ideas