Introduction
Azure DevOps organizations accumulate permissions over time. Groups are created, users are added, Entra (Azure AD) groups are nested into project groups, and team structures evolve. For organizations subject to compliance requirements, security reviews, or simply wanting to understand who has access to what, the Azure DevOps portal provides a per-group, per-namespace view that does not scale.
The Azure DevOps REST APIs expose the underlying security model — security namespaces, Access Control Lists (ACLs), Access Control Entries (ACEs), and bitmask-encoded permissions — but consuming these APIs and translating raw data into actionable output requires significant effort.
The blog post introduces ADO Permissions Output, an open-source PowerShell toolset that extracts Azure DevOps security permissions across 30+ security namespaces, resolves cryptic tokens and GUIDs into human-readable names, and produces structured JSON and CSV output suitable for auditing, compliance, and import into Power BI.
The toolset is available on GitHub: ADO-Permissions-Output
The Problem
Consider a typical Azure DevOps organization with multiple projects, dozens of custom groups, Entra-backed security groups, and permissions set at the repository, build pipeline, release pipeline, area path, and service endpoint levels. An auditor needs to answer questions like:
- Which groups have Deny permissions on a specific Git repository?
- Who has Edit build pipeline access across all projects?
- Are there disabled Entra users still showing as members of ADO groups?
- Which users have access but have never logged in?
The ADO portal answers these one group at a time. The REST APIs answer them in bitmasks and GUIDs. This tool bridges the gap.
What the Tool Does
At a high level, the tool:
- Authenticates to Azure DevOps using a Personal Access Token (PAT)
- Enumerates all security namespaces in the organization
- Fetches all groups, users, and teams
- For each namespace, retrieves ACLs with extended info (effective and inherited permissions)
- Decodes bitmask permissions against the namespace action list
- Resolves security tokens (GUIDs, paths) to friendly names (project names, repo names, query paths, etc.)
- Outputs structured JSON per project with Allow, Deny, Effective, and Inherited permissions clearly labeled
- Optionally generates a group membership report with user entitlement status
Architecture Overview
Flowchart of PowerShell and JSON files, their purposes, the REST API endpoints that are called, and the outputs files.The solution consists of three PowerShell files:
|
File |
Purpose |
|
SecurityMain.ps1 |
Entry point — loads modules, sets up directories, orchestrates execution |
|
SecurityHelper.psm1 |
Core engine — namespace enumeration, ACL fetching, bitmask decoding, token resolution |
|
ProjectAndGroup.psm1 |
Group membership reporting, user entitlement enrichment, directory setup |
Configuration is driven by ProjectDef.json, which specifies output directories, filenames, and which namespaces to extract.
All REST API calls route through a centralized Invoke-AdoRestMethod wrapper that provides automatic retry with exponential back-off for HTTP 429 (throttle) and transient server errors.
Setting Up the Pipeline
The tool is designed for unattended execution in an Azure Pipelines pipeline. The included `main.yml` defines a parameterized pipeline that can be run manually from the ADO UI. Additionally, a trigger can be configured to run on a schedule.
Prerequisites
- A Personal Access Token with read permissions across security, graph, build, release, work items, service endpoints, dashboards, and analytics scopes
- A Variable Group named ADOPermissions containing the PAT as a secret variable
- The Build Service identity needs Contribute permission on the repository (for committing output back)
Running the Pipeline
When you run the pipeline, the "Run pipeline" dialog presents parameters for the organization name, project name, and optional features like the membership report and AAD group recursion.
Azure DevOps Pipeline Run dialog from YAML configuration.The pipeline extracts permissions, commits the output back to the repository, and optionally publishes the output as a pipeline artifact.
Understanding the Permissions Output
The primary output is a JSON file per project. Each entry represents a single permission assignment:
{
"Namespace": "Git Repositories",
"Project": "MyProject",
"Object": "my-repo",
"Type": "Group",
"UserGroupName": "Contributors",
"PermissionType": "Allow",
"Permission": "Contribute",
"Bit": 4
}
Permissions are reported as:
- Allow — Explicitly granted
- Deny — Explicitly denied
- Allow (Effective) — Granted through inheritance
- Allow (Inherited) — Inherited from a parent scope
- Deny (Effective) and Deny (Inherited) — Same patterns for deny permissions
Token Resolution
One of the most valuable features is that raw security tokens are resolved inline. Instead of seeing repoV2/c847308e-d632-4e7f-a7fb-6f4db280bbaa/a1b2c3d4-..., the output shows the actual repository name, build definition name, query path, area path, or service endpoint name.
This resolution covers:
- Project names
- Git repository names
- Build and release definitions
- Work item queries (including nested folder paths)
- Area paths and iterations
- Dashboards (project and team level)
- Service endpoints
- Variable groups and secure files
- Agent pools
- Environments
- Plans and process templates
- Analytics views
The Membership Report
When -IncludeMembership is enabled, the tool generates a separate report showing who belongs to each group and what parent groups each group belongs to.
JSON output of user and group memberships per Azure DevOps group.Detecting Stale and Ghost Members
The membership report includes Status and LastAccessedDate from the User Entitlements API, along with a ResolvedVia field that indicates how each member was discovered.
|
ResolvedVia |
Status |
LastAccessedDate |
Meaning |
|
ADO Membership API |
active |
Recent date |
Active user, using ADO |
|
ADO Membership API |
active |
Null or very old |
Has access, never logged in |
|
ADO Membership API |
disabled |
Any |
Admin disabled their ADO access |
|
ADO Membership API |
Null |
Null |
ADO identity exists but entitlement removed |
|
Hierarchy Group Expansion |
active |
Recent date |
Active user, also in an Entra group |
|
Hierarchy Group Expansion |
Null |
Null |
Ghost member — visible in ADO UI via Entra group but has no ADO entitlement |
AAD/Entra Group Recursion
When -RecurseAADGroups is enabled, the tool resolves the actual members of Entra (Azure AD) groups that are nested inside ADO groups. This uses the ADO Contribution HierarchyQuery API — the same API that the ADO portal uses to display group members.
This is significant because the standard ADO Graph Memberships API does not return individual members of Entra groups — it only shows the Entra group itself as a member. The HierarchyQuery approach reveals the real users, including those whose Entra accounts have been disabled or deleted but still appear in the ADO UI through group membership.
Importing into Power BI
The JSON output is directly importable into Power BI for visualization and analysis.
- Open Power BI Desktop
- Get Data > JSON
- Select the permissions or membership JSON file
- The data loads as a table ready for filtering, pivoting, and visualization
Alternatively, use the -OutputFormat CSV parameter to produce CSV files for direct import via Data > From Text/CSV.
Power BI Dashboard layout of Namespaces, project permissions, user and group names, and count of project permissions.Common Power BI analyses:
- Permission heatmap by namespace and group
- Users with Deny permissions across all projects
- Group membership overlap between projects
- Stale users (active entitlement but no recent access)
- Ghost members from Entra group expansion
Key Design Decisions
Sequential execution. The tool processes namespaces sequentially rather than in parallel. This avoids the ADO API throttle penalty box (HTTP 429), which can delay an entire pipeline run. The retry wrapper handles transient 429s with Retry-After header respect, but sequential processing prevents them from occurring in the first place.
PAT authentication only. The tool uses Personal Access Token authentication with Basic auth headers. This keeps the solution simple — no Entra app registrations, managed identities, or module dependencies. The PAT is stored in an ADO Variable Group marked as secret.
Read-only operation. The tool does not modify any permissions, groups, or resources. All API calls are GET or POST (for subject lookups and HierarchyQuery). It is safe to run against production organizations.
Getting Started
- Clone the repository: git clone https://github.com/sckissel/ADO-Permissions-Output.git
- Create a PAT with the required scopes (see the README for the full list)
- For pipeline execution, follow the setup instructions in the README to create the Variable Group and pipeline definition.
- For local testing:
./SecurityMain.ps1 `
-PAT "<your-pat>" `
-VSTSMasterAcct "yourorg" `
-projectName "YourProject" `
-allProjects "False" `
-DirRoot "C:\ADOSecurity" `
-IncludeMembership "True" `
-RecurseAADGroups "True" `
-OutputFormat "Both"
Conclusion
Auditing Azure DevOps permissions at scale requires more than the portal provides. This toolset bridges the gap between the raw security APIs and actionable audit output, resolving cryptic tokens into readable names, surfacing effective and inherited permissions, and detecting stale or ghost group members through Entra group expansion.
The tool is open source, requires only PowerShell 7 and a PAT, and is designed for unattended pipeline execution with output committed back to the repository for version-tracked audit history.
Feedback, issues, and contributions are welcome on GitHub: ADO-Permissions-Output
Thanks for reading!
Disclaimer
The sample scripts are not supported under any Microsoft standard support program or service. The sample scripts are provided AS IS without warranty of any kind. Microsoft further disclaims all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of the scripts be liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to use the sample scripts or documentation, even if Microsoft has been advised of the possibility of such damages.