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=trueentry 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
- 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 aniiq consoleexport (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.
- Filter to requestable items only — non-requestable rows never reach the portal.
- Lint each item against the eight rules (all independently toggleable), plus a cross-item duplicate-display-name pass.
- 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.
- Gate: exit
1if HIGH findings exceedfail_on_high_over, else0(config/parse errors exit2) — 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;demomode, no secrets.sample-output.txt— captured stdout ofpython 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
- Local IIQ docs (repo-relative):
- 8.4 Lifecycle Manager — LCM access-request model and the requestable-item flow used in §How it works.
- 8.4 IIQ Application Management — Entitlement Catalog,
ManagedAttributedisplay name / description / owner / requestable fields. - 8.4 IIQ Classifications — Classification objects and the
NO_CLASSIFICATIONrule. - 8.4 IIQ Role, Group and Population Management —
Bundle(role) requestable metadata. - 8.4 IIQ Console —
exportcommand used by thexml-exportadapter.
- External research:
- https://documentation.sailpoint.com/identityiq/help/application_management/entitlement_catalog.html — Entitlement Catalog: ManagedAttribute promotion, requestable flag, owner, display name and description.
- https://documentation.sailpoint.com/identityiq_84/help/appmgmt/entitlementcat.html — IIQ 8.4 Entitlement Catalog reference.
- https://documentation.sailpoint.com/identityiq/help/system_configuration/identityiq_global_settings/entitlement_catalog_attributes.html — entitlement catalog (extended) attributes that the linter can be extended to check.
- https://community.sailpoint.com/t5/IdentityIQ-Wiki/Hiding-access-filter-attributes-from-request-access-manage/ta-p/160275 — Compass thread on tuning what the Request Access page shows end users.
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) andBundle(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.jsonis the reference shape). Required keys:input.mode— one ofdemo|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→ aniiq consoleexport ofManagedAttributeandBundleobjects (export -clean <file> ManagedAttribute, then... Bundle). - For
rest:input.rest_base_url,input.rest_resource, andinput.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
demomode must run with no IIQ connection and no network. - Argument parsing:
-c/--config <path>and--demo(forcesdemomode). - Exit codes:
0clean/within-gate,1HIGH findings exceedfail_on_high_over,2configuration/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 Customizationrule 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 --demoexits1and prints the 8-rule breakdown. - Setting every
rules.*tofalseyields0findings and exit0. - A non-requestable item in the catalog never appears in findings.
-
xml-exportmode parses<ManagedAttribute>and<Bundle>and tolerates missing optional fields without crashing. -
restmode raises a clearNotImplementedError(stub) and exits2. - Missing config path exits
2with a stderr message.
More from IAM Ideas