You describe a task — "reset a user's MFA", "read audit logs", "manage Conditional Access policies" — and Entra RoleLens returns the minimum built-in Entra ID role required to do it, and nothing more. You can also compare any two roles side by side and see exactly what one has that the other lacks, permission by permission.
It replaces the 50-tab Microsoft docs crawl that every Entra admin does when someone asks: "what role do I assign without giving them too much?"
| Mode | What it does |
|---|---|
| Task → Role | Describe what you need to do in plain language. Get back the minimum built-in role, a direct link to Microsoft's source, and a privilege warning if the role is elevated. |
| Role Diff | Select any two built-in roles. See every permission one has that the other lacks in a clean three-column view — unique to A, shared, unique to B. |
| What's New timeline | A live feed of what Microsoft changed in the role catalog — which permissions were added/removed, privilege reclassifications, and brand-new roles. New roles and permission changes both expand to the official description, the permissions involved, a link to Microsoft's docs, and an AI-generated real-world scenario. |
| Shadow Detection | Roles present in the Graph API but absent from public documentation are flagged as isShadowRole: true — catching unreleased Microsoft roles before announcement. |
| Always current | The full role catalog and task mappings refresh nightly via a secure, passwordless OIDC pipeline. Every change Microsoft makes is detected, logged, and live by morning. |
Entra RoleLens cross-references the live Microsoft Graph API against Microsoft's public documentation on every nightly run. Roles that exist in the API but are not yet documented are flagged as shadow roles — this means the tool can surface new Microsoft roles before they appear in any documentation.
The shadow role count is logged in every pipeline run and visible in the pipeline status endpoint:
GET /api/status → shadow_role_count
A role only appears as a least-privilege recommendation once a task points to it. Task mappings are scraped nightly from Microsoft's delegate-by-task page, so most roles map themselves automatically. The exceptions are brand-new or shadow roles Microsoft hasn't documented yet.
coverage_report.py runs every night and:
- writes
data/coverage.jsonwith the full breakdown (covered vs. uncovered roles), and - maintains a single idempotent Roles awaiting task coverage issue listing only the new/undocumented roles that still need a task — updated each run and closed automatically when the list is empty. Implicit/default directory roles and zero-permission workload roles (e.g. Purview content roles, which grant no Entra directory actions and are governed in their own service portal) are excluded — they can't take a task mapping.
This keeps the one remaining manual step — seeding a task for a role Microsoft hasn't documented yet — explicit and visible, rather than silent.
Auto-generated from the nightly pipeline · Last updated by GitHub Actions
Global Administrator (2026-10-22)
Security Administrator (2026-10-22)
Conditional Access Administrator (2026-10-22)
Application Developer (2026-10-14)
How every request enters through Cloudflare's WAF + DDoS edge, how the frontend, Worker API, and D1/KV actually connect — and where the nightly pipeline feeds in:
Which free tier hosts what, how Cloudflare's WAF and DDoS protection sit in front of the public app, and how the nightly pipeline authenticates to Microsoft without storing a credential:
See Technical stack below for the full per-layer cost breakdown, and Passwordless pipeline for the OIDC handshake in detail.
Why it exists. Microsoft's built-in role descriptions are short and abstract. When a brand-new role appears, or an existing role gains or loses a permission, an admin has no fast way to tell what that actually means for someone assigned it. One nightly loop asks a model for a short, concrete "here is a situation where this matters" narrative to sit alongside the official text. A second weekly loop exists only to keep the first one working: Cloudflare retires Workers AI models on its own schedule (18 in a single wave on 2026-05-30), and a pinned model id silently stops generating scenarios when that happens.
What AI does and does not touch. The generated narrative is display-only prose in one UI panel. It never feeds search ranking, role or permission data, the role diff, or the coverage report — those are all deterministic, and the search path contains no LLM at all. Every narrative is labeled AI-generated and rendered after the authoritative content: the role's official description, its full or changed permissions, the deterministic role-fact badges on a new role (privileged / PIM-eligible / admin-unit-scopable / where it is configured — computed locally, never by the model), and the Microsoft docs link. Generation is best-effort: a failed or empty call just leaves the field blank and the panel falls back to the verified content alone. Both "minimum role for a task" and "role diff" work with the AI engine switched off entirely.
Scope is deliberately narrow — only two kinds of changelog entry get a scenario:
- A new role, and a permission added to or removed from an existing role. Both represent an actual capability change: something someone assigned the role can now do, or can no longer do.
- Description updates, renames, and privilege flips do not get one. A description update already shows an exact word-diff of what Microsoft reworded — a paraphrase adds little. A privilege flip already carries a deterministic, hardcoded "why" explanation in the UI, which is more reliable than an LLM guess. A rename is cosmetic. None of these represent a capability change, so a manufactured scenario would be padding, not signal.
Combined, this runs to roughly a dozen calls a month against the full changelog history to date — nowhere near Workers AI's 10,000 free neurons/day.
Light continuity. When a role already has a prior scenario on record (say, its own "new role" narrative from months earlier), a later permission-change call includes that prior text as tone-consistency context, so the two don't contradict or restate each other. This is read straight out of the changelog history already committed to the repo — no vector store, no separate memory service, nothing beyond what free tier already gives for free.
Two loops, both fully automatic:
- Nightly (
generate_scenarios.py, right afterdiff_roles.py): for each eligible entry (new role, or a permission change), computes the deterministic role facts, then — only if the changelog entry has no scenario yet — calls Cloudflare Workers AI for the narrative, including any prior scenario for the same role as context. Idempotent: a failed call is simply retried next run, no duplicate work. - Weekly (
check_ai_model.py, Wednesdays 06:00 UTC): checks the configured model against Workers AI's live catalog. If it is gone or deprecated, it walks a small hand-maintained candidate list and switches to the first entry that is still listed, not deprecated, and returns a completion in a live smoke test — shipped as an auto-merged PR so CI still gates the swap. If every candidate fails, it opens a tracking issue instead of guessing.
Dependencies are scanned for vulnerabilities on every push to master and weekly on Sundays using Trivy. Results (HIGH and CRITICAL CVEs) are uploaded to GitHub Security → Code Scanning. The dashboard below is regenerated and committed automatically after each scan.
Auto-generated by Trivy on every push and weekly scan — the scanner version shown in the dashboard header is captured live from each run, never hand-typed here.
| Layer | Technology | Cost |
|---|---|---|
| Frontend | Cloudflare Pages · Global CDN · 330+ PoPs | €0 |
| API | Cloudflare Workers · TypeScript · 5 routes | €0 |
| Database | Cloudflare D1 · SQLite · counts refresh nightly, see Data quality | €0 |
| Cache | Cloudflare KV · master.json · pipeline_status | €0 |
| Auth | Entra ID · Workload Identity Federation · OIDC | €0 |
| Pipeline | GitHub Actions · Python 3.11 · nightly cron | €0 |
| Analytics | Umami · self-hosted · privacy-first | €0 |
| Domain | aboutcloud.io · already owned | €0 |
| Total | €0 / month |
Search engine: Pure SQL keyword matching against a weighted task_search table. Keywords extracted in the Worker, matched against D1. No LLM in the query path. Median response time: < 5ms.
The nightly pipeline authenticates to Microsoft Entra ID without any stored credentials using Workload Identity Federation:
GitHub Actions requests a short-lived OIDC JWT from GitHub's identity provider
│
▼
Microsoft Entra ID validates the JWT against a Federated Credential
(scoped to: repo=arusso-aboutcloud/entra-rolelens, branch=master)
│
▼
Entra ID issues a temporary access token — no secret stored anywhere
│
▼
fetch_roles.py calls graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions
GitHub secrets required: AZURE_CLIENT_ID + AZURE_TENANT_ID only. No client secret. No certificate.
This tool requires zero manual maintenance for daily operation. Every night at 01:00 UTC, a GitHub Actions workflow runs automatically:
01:00 UTC — GitHub Actions wakes up (free tier · ~3 min runtime)
│
├── azure/login@v2 OIDC handshake → temporary Entra access token
│ (Workload Identity Federation · EntraRoleFetcher-API)
│
├── fetch_roles.py Calls Microsoft Graph API via OIDC token (live source of truth)
│ ├── Graph API graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions
│ │ Authenticated via OIDC token
│ │ → data/roles_graph_raw.json (source of truth for IDs; permissions reconciled with docs)
│ └── Docs scrape Also scrapes MicrosoftDocs/entra-docs for descriptions
│ → data/roles.json (human-readable metadata)
│
├── scrape_tasks.py Scrapes the Microsoft Learn least-privileged-by-task page
│ Parses task → minimum role mappings across dozens of feature areas
│ → data/tasks.json
│
├── diff_roles.py Compares today's roles against yesterday's snapshot
│ Detects ADDED, REMOVED, and MODIFIED roles
│ Logs every change with timestamp to D1 role_changes table
│
├── generate_scenarios.py
│ For each newly-added role: asks Cloudflare Workers AI for a
│ short real-world scenario, saved to the changelog entry
│ Best-effort — a failed call just leaves the field unfilled
│
├── enrich.py Cross-references roles_graph_raw.json vs roles.json
│ Permissions = union of Graph + docs (never under-states a role)
│ Roles in Graph API but not in docs → isShadowRole: true
│ Builds master.json and resolves role names to IDs
│
├── validate.py Schema and quality checks
│ On failure: auto-opens a GitHub Issue and aborts the push
│ The live data is never overwritten with invalid data
│
├── coverage_report.py Flags new/undocumented roles that have no task mapping yet
│ → data/coverage.json (covered vs. uncovered breakdown)
│ Maintains one idempotent "Roles awaiting task coverage" issue
│
└── push_to_cloudflare.py
Pushes master.json to Cloudflare KV (global cache)
Upserts all roles and tasks to Cloudflare D1 (SQLite)
Retries transient 5xx/429s with backoff before failing
Logs changelog entries to D1 role_changes table
Commits updated data files back to this repo
Separately, weekly (check-ai-model.yml, Wednesdays): check_ai_model.py checks whether Cloudflare has deprecated the model generate_scenarios.py uses and, if so, live-tests a fallback list and auto-patches it via PR. See AI automation engine.
If the pipeline fails — a GitHub Issue is opened automatically. The previous night's data stays live. Nothing breaks for users.
The commit history of this repo is a permanent, searchable record of every role change Microsoft has made since launch.
entra-rolelens/
├── .github/
│ ├── workflows/
│ │ ├── refresh.yml # Nightly pipeline — OIDC auth + dual data sources
│ │ ├── check-ai-model.yml # Weekly — keeps the Workers AI model current
│ │ ├── auto-patch-deps.yml # Weekly — patches vulnerable transitive npm deps
│ │ └── dependabot-auto-merge.yml
│ └── ISSUE_TEMPLATE/ # missing_task.md · bug_report.md
├── pipeline/ # Python scripts — run by GitHub Actions
│ ├── fetch_roles.py # Graph API (OIDC) + docs scrape — dual source
│ ├── scrape_tasks.py # Scrapes task → role mappings
│ ├── diff_roles.py # Detects role changes
│ ├── generate_scenarios.py # AI scenario per new role / permission change (Workers AI)
│ ├── enrich.py # Union permissions + shadow detection → master.json
│ ├── validate.py # Quality gate
│ ├── coverage_report.py # Flags new roles lacking task coverage
│ ├── push_to_cloudflare.py # Writes to KV + D1, retries transient failures
│ └── check_ai_model.py # Weekly — detects/patches a deprecated AI model
├── worker/ # Cloudflare Worker — TypeScript API
│ ├── src/index.ts # 5 routes: search, diff, role, roles, status
│ └── wrangler.toml
├── frontend/ # Static UI — deployed to Cloudflare Pages
│ └── index.html # Single file · dark theme · no framework
├── data/ # Auto-committed nightly by the pipeline
│ ├── roles_graph_raw.json # Live Graph API response — source of truth
│ ├── roles.json # Docs-sourced role metadata
│ ├── tasks.json # task → role mappings
│ ├── master.json # Merged dataset pushed to KV
│ ├── changelog.json # Role changes detected this run + AI scenarios
│ ├── coverage.json # Task-coverage report (new roles needing a task)
│ └── previous_master.json # Yesterday's enriched (master.json) snapshot for diffing
└── assets/
├── app-architecture.svg # Application architecture — components + data flow
├── ai-automation-engine.svg # AI scenario generation + model-deprecation watchdog
├── architecture.svg # Infrastructure/hosting diagram
└── project-banner.png # Project banner
| Source | URL | Used for |
|---|---|---|
| Microsoft Graph API | graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions |
Live role definitions · OIDC authenticated · source of truth |
| MicrosoftDocs/entra-docs | github.com/MicrosoftDocs/entra-docs |
Role descriptions · metadata |
| Microsoft Learn | learn.microsoft.com/.../delegate-by-task |
Task → minimum role mappings |
Why dual sources? The Graph API is authoritative for role IDs; the documentation scrape supplies task → role mappings and human-readable metadata. The two sources also disagree on a role's permissions during Microsoft rollout windows — the docs lead for brand-new roles (e.g. the agent-identity permissions), the Graph API leads for some mature roles. RoleLens reconciles them with a union per role, so the catalog never under-states what a role grants — keeping role detail, diff, and least-privilege search consistent with what Microsoft has published (and with "What's new"). This dual sourcing also powers the shadow role detector: roles Microsoft has deployed to the API but not yet documented.
- 145+ built-in roles - covers all named Entra ID built-in roles including preview roles
- 238 task mappings - sourced from Microsoft's official documentation and community contributions
- 9 unlisted roles - present in the Graph API but not yet in Microsoft's public documentation
- 0 partially documented roles - in roles reference but missing from task mappings
- Nightly diff - every permission change Microsoft makes is logged to the role_changes D1 table
- Self-healing pipeline - validation gate prevents bad data reaching production
The task dataset lives in data/tasks.json. If a mapping is wrong, a task is missing, or a role recommendation is outdated:
- Check the Microsoft Learn least-privileged-by-task page for the authoritative mapping
- Open an issue with the task description and the Microsoft Learn source URL
- Or submit a PR directly to
data/tasks.json— see CONTRIBUTING.md
Every merged contribution is picked up by the nightly pipeline and live within minutes.
Other free, open-source Entra ID tools from the same team:
| Tool | What it does |
|---|---|
| Entra Tracker (source) | Live tracker for Entra ID retirements, breaking changes, and previews |
| EntraPass (source) | Passkey (FIDO2) readiness scanner for Entra ID tenants |
| AADSTS Entra Errors (source) | Searchable AADSTS error code reference |
MIT — see LICENSE
Made by aboutcloud.io · Antonio Russo

