- What is QAZPT?
- Installation
- Usage
- Documentation
- Limitations and future work
- Comparison with other tools
QAZPT (Quarkslab Azure Permission Tracker) is a tool designed to analyze and track permissions — EntraID roles, application roles, and delegated permissions — through direct or indirect inheritance within Microsoft EntraID (formerly Azure Active Directory) environments. Currently, it does not take Azure Resource Manager (ARM) into account. It helps identify how permissions are propagated through various entities such as users, applications, and service principals, enabling security teams to better understand and manage access controls.
Caution: QAZPT is still a work in progress (WIP) and its results should be interpreted carefully. Additionally, inheritance computation is complex and cannot cover all edge cases. Please refer to the documentation for more details on how inheritance is computed and its limitations.
- Analyze and visualize inheritance between EntraID entities (Users, AppRegistrations, ServicePrincipals), uncovering complex relationships and potential permission escalation paths.
- Identify and visualize gained permissions through inheritance, helping to detect over-privileged entities.
- Export detailed reports in CSV and JSON formats for further analysis, containing entities, their own permissions, and inherited permissions.
- Export the full graph to a Neo4j database for interactive exploration and advanced Cypher queries.
QAZPT can assist both offensive and defensive security work in EntraID environments:
- Evaluate the security posture of EntraID environments by uncovering effective permissions for each entity
- Identify potential privilege escalation paths across EntraID entities
- Ensure no entity gains unintended permissions and that access controls are properly configured
- Assist in detecting post-exploitation persistence techniques by mapping out inherited permissions
- Monitor and audit permission changes over time to detect anomalies by comparing reports
uv sync
uv run qazpt --helpor with pip:
pip install .
qazpt --helpqazpt --help
usage: QAZPT [-h] [-v] [-d] {inheritance} ...
positional arguments:
{inheritance}
inheritance Analyze inheritance
options:
-h, --help show this help message and exit
-v, --verbose
-d, --debug Enable debug mode (more verbose)
-C CACHE_DIR, --local-cache-dir CACHE_DIR
Use the specified local directory for caching data or load cached data. Default is ~/.qazpt_cacheusage: QAZPT inheritance [-h] [-t | -p | -g DB_NAME | -c CSV_NAME | --as-json [JSON_PATH] | --as-neo4j] [--neo4j-uri BOLT_URI] [-A] [-P] [-f TYPE,TYPE,...] [-x TYPE,TYPE,...]
[--hide-delegated-permissions] [--neo4j-user USERNAME] [--neo4j-password PASSWORD] [-r] [--legacy-dfs] [--direct-origin] [--true-origin]
options:
-h, --help show this help message and exit
-t, --inheritance-tree
[Default] Output inheritance as trees
-p, --show-gained-permissions
Output entities with their gained permissions
-c, --as-csv CSV_NAME
Output entities, permissions and indirects permissions gained through inheritance as CSV
--as-json [JSON_PATH]
Output entities, permissions and indirects permissions gained through inheritance as JSON file if specified, else to stdout
--as-neo4j Export the graph to a Neo4j database.
--neo4j-uri BOLT_URI Bolt URI of the Neo4j instance (used with --as-neo4j, default: bolt://127.0.0.1:7687)
-A, --show-all When analyzing inheritance, show also entities without any permissions
-P, --privileged-only
When analyzing inheritance, only show privileged entities
-f, --filter-by-type TYPE,TYPE,...
When analyzing inheritance, only show entities of type included in the comma separated list. Available types are: User, ServicePrincipal,
ManagedIdentityServicePrincipal, AppRegistration
-x, --exclude-by-type TYPE,TYPE,...
Same as --filter-by-type but exclude the types instead of including them
--hide-delegated-permissions
When analyzing inheritance, do not show delegated permissions
--neo4j-user USERNAME
Neo4j username (used with --as-neo4j, default: neo4j)
--neo4j-password PASSWORD
Neo4j password (used with --as-neo4j, default: qazpt)
-r, --refresh When analyzing inheritance, refresh the data
--legacy-dfs When computing inheritance, use legacy cycle-safe DFS algorithm instead of DAG condensation algorithm (default is condensed). Computation is faster butwill
produce non-exhaustive results in cyclic graphs. Use this flag if your environment is acyclic or if you encounter issues with the condensation algorithm.
--direct-origin When distributing condensed inheritance, inherited permissions are shown with their direct neighbors as the origin. If A(X)->B(Y)->C(Z), A will get (YZ)
with B as origin.Use this flag if you prefer to see the path of inheritance more explicitly.Using this flag may cause more verbose outputs in graphs with a
lot of strongly connected components (several nodes with cyclic inheritance).This flag is mutually exclusive with --true-origin.
--true-origin [Default] When distributing condensed inheritance, inherited permissions are shown with their original owners as the origin. If A(X)->B(Y)->C(Z), A will get (Y) with
B as origin and (Z) with C as origin.Use this flag if you prefer to see the original source of inherited permissions.Showing true origin permissions may
result in more verbose outputs in graphs with populated strongly connected components.This flag is mutually exclusive with --direct-origin. This is the
default behavior.To print the inheritance tree of the EntraID entities, use the --inheritance-tree option, which is the default behavior:
qazpt inheritance
### Users ###
alice/alice@contoso.onmicrosoft.com/00000000-0000-0000-0000-000000000001
--[ownership]--> AppRegistration/general_admin_app/00000000-0000-0000-0000-000000000002
--[sp_of_app]--> ServicePrincipal/general_admin_app/00000000-0000-0000-0000-000000000003 [HAS PRIVILEGED ROLE]
--[Entraid_Role (Global Administrator)]--> All entities
### Applications ###
AppRegistration/general_admin_app/00000000-0000-0000-0000-000000000002
--[sp_of_app]--> ServicePrincipal/general_admin_app/00000000-0000-0000-0000-000000000003 [HAS PRIVILEGED ROLE]
--[Entraid_Role (Global Administrator)]--> All entitiesAdd -P, --privileged-only to only show entities that hold a privileged built-in role within EntraID.
Use --direct-origin to show the immediate neighbors as the origin of inherited permissions. By default, the original source (e.g. the owner) of inherited permissions is shown.
To get the effective gained inherited permissions:
qazpt inheritance --show-gained-permissions [--hide-delegated-permissions]
### User ###
alice/alice@contoso.onmicrosoft.com/00000000-0000-0000-0000-000000000001
Entra ID Role :
- Global Administrator inherited from AppRegistration/general_admin_app/00000000-0000-0000-0000-000000000002
### ServicePrincipal ###
ServicePrincipal/AppTest_userpasswordprofile_readwrite/00000000-0000-0000-0000-000000000004
Entra ID Role :
- Global Administrator inherited from bob/bob@contoso.onmicrosoft.com/00000000-0000-0000-0000-000000000005 [HAS PRIVILEGED ROLE]
- Global Administrator inherited from alice/alice@contoso.onmicrosoft.com/00000000-0000-0000-0000-000000000001
AppRoleAssignments :
- Graph/Toto.Titi inherited from bob/bob@contoso.onmicrosoft.com/00000000-0000-0000-0000-000000000005 [HAS PRIVILEGED ROLE]
Entities that are already highly privileged — meaning those that hold an EntraID role or a Microsoft Graph permission enabling them to control all other entities — will not appear here, as they virtually gain nothing and their inherited permissions are not computed.
If an entity inherits from a highly privileged entity, the inherited permissions are limited to that highly privileged entity's own permissions.
To get a full list of entities, their own permissions, and their inherited permissions, the data can be exported as a CSV file using --as-csv, or in JSON format using the --as-json option. Note that if no file path is provided for the JSON output, it will be printed to standard output.
qazpt inheritance --as-csv inheritance_output.csvqazpt inheritance --as-json [file_name.json]QAZPT can export the full inheritance graph to a Neo4j database for interactive exploration.
Start a Neo4j instance. A docker-compose.yml is provided at the root of the project:
docker compose up -dThis starts Neo4j 5 with the Browser UI at http://localhost:7474 and the Bolt endpoint at bolt://127.0.0.1:7687 (credentials: neo4j / qazpt).
qazpt inheritance --as-neo4jBy default, ServicePrincipals with no permissions and no concrete ownership/control links are omitted to keep the graph readable. Pass --show-all to include them:
qazpt inheritance --as-neo4j --show-allConnection options (all have defaults matching the provided docker-compose.yml):
| Option | Default | Description |
|---|---|---|
--neo4j-uri |
bolt://127.0.0.1:7687 |
Bolt URI of the Neo4j instance |
--neo4j-user |
neo4j |
Neo4j username |
--neo4j-password |
qazpt |
Neo4j password |
Note: Each export clears prior QAZPT data by deleting only nodes with QAZPT labels (User, ServicePrincipal, ManagedIdentityServicePrincipal, AppRegistration, EntraIDRole, AppRole, DelegatedPermission). Other nodes and data in the same database are not affected.
Combined with other filters:
# Only export Users and ServicePrincipals
qazpt inheritance --as-neo4j --filter-by-type User,ServicePrincipal
# Only show privileged entities
qazpt inheritance --as-neo4j --privileged-onlyEntity nodes — one label per type, filtered with --filter-by-type / --exclude-by-type:
| Label | Key properties |
|---|---|
:User |
id, display_name, user_principal_name, scope_level |
:ServicePrincipal |
id, display_name, app_id, sp_type, scope_level |
:ManagedIdentityServicePrincipal |
id, display_name, app_id, scope_level |
:AppRegistration |
id, display_name, app_id, scope_level |
The scope_level property reflects the direct privilege level of the entity (e.g. SERVICE_PRINCIPAL, USER, ALL, NONE_OR_DEFAULT) as computed by QAZPT's inheritance resolvers.
Permission nodes:
| Label | Key properties |
|---|---|
:EntraIDRole |
id, display_name, is_privileged, is_built_in |
:AppRole |
id, display_name, value, resource_display_name, resource_id |
:DelegatedPermission |
id, scope, resource_id, resource_display_name, principal_id, principal_display_name |
Relationships:
| Relationship | Meaning | Properties |
|---|---|---|
CONTROLS |
Concrete inheritance link (ownership, SP of app, FIC, …) | type, detail |
HAS_ENTRAID_ROLE |
Entity holds an EntraID role directly | resource_scope, directory_scope |
HAS_APP_ROLE |
Entity holds a Microsoft Graph application role | — |
HAS_DELEGATED_PERMISSION |
Entity holds a delegated OAuth2 permission | consent_type |
Search by name (case-insensitive):
MATCH (e) WHERE toLower(e.display_name) CONTAINS toLower('alice') RETURN eAll entities with a specific EntraID role (direct holders):
MATCH (e)-[:HAS_ENTRAID_ROLE]->(r:EntraIDRole {display_name: 'Global Administrator'})
RETURN e.display_name, e.entity_typeAll entities that lead to Global Administrator (direct + inherited via concrete links + via virtual scope):
// Direct holders
MATCH p=(e)-[:HAS_ENTRAID_ROLE]->(r:EntraIDRole {display_name: 'Global Administrator'})
RETURN p
UNION
// Via concrete inheritance chain
MATCH p=(e)-[:CONTROLS*1..]->(target)-[:HAS_ENTRAID_ROLE]->(:EntraIDRole {display_name: 'Global Administrator'})
RETURN pShortest paths to Global Administrator:
MATCH (target)-[:HAS_ENTRAID_ROLE]->(r:EntraIDRole {display_name: "Global Administrator"})
MATCH (e) WHERE NOT (e)-[:HAS_ENTRAID_ROLE]->(r) AND e <> target
WITH e, r, target
MATCH p = allShortestPaths((e)-[:CONTROLS*1..6]->(target))
WHERE NONE(n IN nodes(p)[1..-1] WHERE (n)-[:HAS_ENTRAID_ROLE]->(r))
MATCH q = (target)-[:HAS_ENTRAID_ROLE]->(r)
RETURN p, qEntities with an elevated direct scope level:
MATCH (e) WHERE e.scope_level IN ['USER', 'APPLICATION', 'ALL']
RETURN e.display_name, e.entity_type, e.scope_level ORDER BY e.scope_levelShortest paths to entities with elevated direct scope level:
MATCH q = (target {scope_level: "ALL"})-[:HAS_APP_ROLE|:HAS_ENTRAID_ROLE]->(r)
MATCH (e) WHERE NOT e.scope_level = "ALL" AND e <> target
WITH e, target, q
MATCH p = allShortestPaths((e)-[:CONTROLS*1..6]->(target))
WHERE NONE(n IN nodes(p)[1..-1] WHERE n.scope_level = "ALL")
RETURN p, qIn order to properly analyze inheritance, QAZPT requires sufficient permissions to read the entities and their permissions within the target EntraID environment.
Minimal Microsoft Graph required permissions include:
User.Read.AllApplication.Read.AllRoleManagement.Read.Directory
Directory.Read.All can also be used to cover every required read permissions.
Note that every member user of the directory can read most of the directory and delegate this permissions successfully. Using a guest account or a service principal will require above permissions to be explicitly granted.
The inheritance is computed thanks to resolvers (InheritanceResolverRule) located in qazpt/core/graph/resolvers/.
These resolvers are responsible for determining the relationships between entities and how permissions are inherited.
Currently, the resolvers execute one pass to establish inheritance relationships before computing the effective permissions for each entity. While this approach ensures accurate tree views, it does not account for potential new inheritance paths that could emerge from EntraID or Microsoft Graph permissions after the initial computation.
The use of Tarjan's algorithm addresses this issue by identifying and condensing cycles in the graph into single nodes. This ensures that transitive permissions are computed exhaustively, even in graphs with cyclic inheritance. By treating cycles as unified entities, the algorithm enables a more precise and complete calculation of effective permissions.
Currently, 5 resolvers are used in order to find inheritance between entities:
An entity is set as the owner of another one (e.g a User is the owner of an AppRegistration)
ServicePrincipals are living instances of AppRegistration. Owning an AppRegistration means you control its corresponding ServicePrincipal.
An Application provided with a Federated Identity Credential (FIC) allows the configured, potential external identity provider to request tokens for the application. Therefore, any identity able to authenticate against the configured identity provider can potentially gain access to the application and its permissions.
⚠️ Caveat: Only INTERNAL identities are currently supported. External identity providers (e.g., GitHub, Google) are not yet handled. A warning level log is generated when an unsupported identity type is encountered.
Several EntraID roles enable to control different kind of entities:
⚠️ Caveat: The list below is incomplete and covers only a subset of highly privileged roles commonly used in assessments. Additional roles with security implications exist but are not yet included.
- Global Administrator: All entities
- Application Administrator: All AppRegistration and ServicePrincipals (And consent to applications roles different than Microsoft Graph)
- Cloud Application Administrator: Same as above but not for App Proxy
Several Microsoft Graph application roles enable to control different kind of entities.
-
RoleManagement.ReadWrite.Directory: All entities
- This permissions enables to assign EntraID roles, such as Global Administrator and modify PIM configurations.
-
AppRoleAssignment.ReadWrite.All: All entities
- This permissions enables to assign any other application role to any other entity, without needing an admiministrator to consent. It can be used to assign
RoleManagement.ReadWrite.Directory, which can be used to assign General Administrator role.
- This permissions enables to assign any other application role to any other entity, without needing an admiministrator to consent. It can be used to assign
-
User-PasswordProfile.ReadWrite.All: Users by changing their password. True only if no MFA is enforced.
-
User.ReadWrite.All: Some users. Can be used to edit user profile that are less privileged. (Unclear)
-
UserAuthMethod-Passkey.ReadWrite.All: All users.
- This permissions can be used to add a passkey as authentication way for any users. Passkeys enable to authenticate without password and 2FA.
-
UserAuthenticationMethod.ReadWrite.All: Same as above, but include every other way to authenticate.
-
Application.ReadWrite.All: All applications and servicePrincipals.
-
Application.ReadUpdate.All: Same as above. Difference is that this one doesn't allow to create new applications.
-
ServicePrincipalEndpoint.ReadWrite.All: All servicePrincipals.
After nodes have been linked thanks to the different resolvers, the graph is either analyzed in order to compute the effective permissions of each entities using a DFS, cycle-safe approach (--legacy-dfs), or the graph is condensed leveraging Tarjan's algorithm to obtain a Directed Acyclic Graph (DAG) and then a bottom-up traversal is performed to compute inherited permissions.
Each node inherits the permissions of its childs nodes, except if the node is already highly privileged (e.g has an EntraID role or a Microsoft Graph permission that enable it to control every other entities).
- Groups are not yet supported. However, transitive EntraID roles obtained through groups are taken into account.
- Support for Azure Resource Manager (ARM) permissions and inheritance is not yet implemented, and therefore inheritance through ARM RBAC is not computed for managed identities and user-assigned managed identities.
- EntraID roles scopes are not yet supported (EntraID v1/v2).
- Administrative Units are not yet supported (EntraID v1/v2).
- PIM (Privileged Identity Management) is not yet supported (EntraID v1/v2).
- Federated Identity Credentials (FIC): Only INTERNAL identities are currently supported. External identity providers (e.g., GitHub, Google) are not yet handled.
- EntraID roles coverage: The tool covers only a subset of highly privileged roles commonly used in assessments. Additional roles with security implications exist but are not yet included.
You may wonder why QAZPT when there are already some tools that can analyze permissions in EntraID environments, such as Bloodhound (With AzureHound), ROADtools, ScoutSuite, etc..
First, QAZPT is not meant to be a replacement for these tools, but rather a complementary tool that focuses on a specific aspect of EntraID security: permission inheritance. While other tools may provide a broader range of features and analyses, QAZPT aims to provide a deeper and more comprehensive analysis of permission inheritance, which is a critical aspect of EntraID security that can often be overlooked.
It seems that no other tool currently provides an in-depth analysis of permission inheritance in EntraID environments, especially when it comes to the different inheritance paths (ownership, FIC, EntraID roles, Microsoft Graph permissions abuses) and the effective permissions gained through inheritance. QAZPT fills this gap by providing a dedicated tool for analyzing and understanding permission inheritance in EntraID environments.