← IAM Ideas
IAM Ideas 2026-06-01

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.

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