Azure RBAC compliance monitoring — detect permission drift and governance violations
Project description
Azure Permissions Watch
Azure RBAC as Code — drift detection and guardrails.
What it does
Two complementary commands in one tool:
scan— RBAC as Code (affirmative): declare your desired RBAC state in YAML, detect drift from that stateaudit— Policy as Code (negative): define forbidden patterns (guardrails), detect violationsscan --orphans-only— detect assignments referencing deleted principals (orphaned identities)
Both commands share the same RBAC scanner — the differentiating value is that neither OPA nor Azure Policy can natively scan RBAC assignments.
Installation
# Recommended — isolated install for CLI tools
pipx install az-rbac-watch
# Or with pip
pip install az-rbac-watch
Requires Python ≥ 3.12 and Azure CLI for authentication.
Shell completion
Enable tab completion for bash, zsh, or fish:
az-rbac-watch --install-completion
Restart your shell after installation.
Quick start
1. Authenticate
az login
2. Discover existing assignments
# Single subscription
az-rbac-watch discover -t <tenant-id> -s <subscription-id> -o my_policy.yaml
# All accessible scopes
az-rbac-watch discover -t <tenant-id> -o my_policy.yaml
3. Scan for drift (RBAC as Code)
The scan command compares actual RBAC assignments against your baseline rules. Any assignment not covered by a baseline rule is reported as DRIFT.
# Console output
az-rbac-watch scan -p my_policy.yaml
# HTML report
az-rbac-watch scan -p my_policy.yaml -o report.html
# JSON (CI/CD)
az-rbac-watch scan -p my_policy.yaml --format json
4. Audit guardrails (Policy as Code)
The audit command checks governance rules (forbidden patterns). Any assignment matching a governance rule is reported as a violation.
# Console output
az-rbac-watch audit -p my_policy.yaml
# HTML report
az-rbac-watch audit -p my_policy.yaml -o report.html
# JSON (CI/CD)
az-rbac-watch audit -p my_policy.yaml --format json
A starter kit with common governance rules is available in examples/deny_rules_starter.yaml.
5. Validate policy syntax (offline)
az-rbac-watch validate -p my_policy.yaml
6. Capture RBAC snapshots
# From a policy file
az-rbac-watch snapshot -p my_policy.yaml -o snapshot_2026-03-09.json
# From explicit scopes
az-rbac-watch snapshot -t <tenant-id> -s <subscription-id> -o snapshot.json
7. Compare snapshots (change tracking)
# Console output
az-rbac-watch diff snapshot_old.json snapshot_new.json
# JSON (CI/CD)
az-rbac-watch diff snapshot_old.json snapshot_new.json --format json -o changes.json
Two axes, one tool
| Axis | Command | Rule type | Finding | Question answered |
|---|---|---|---|---|
| RBAC as Code | scan |
baseline |
DRIFT |
"Is there something I didn't declare?" |
| Policy as Code | audit |
governance |
GOVERNANCE_VIOLATION |
"Is there something forbidden?" |
| Change tracking | snapshot + diff |
n/a | Added / Removed / Modified | "What changed since last time?" |
You can use both in the same policy file. Each command focuses on its rule type and ignores the other.
Scope modes
The policy model supports two scope modes:
scope: explicit(default) — only scans subscriptions and management groups listed in the YAMLscope: all— auto-discovers all accessible scopes at scan time, with optional exclusions
scope: all
exclude_subscriptions:
- "22222222-2222-2222-2222-222222222222"
exclude_management_groups:
- "mg-sandbox"
CLI exclusions (--exclude-subscription, --exclude-management-group) apply on top of YAML exclusions.
CLI reference
az-rbac-watch scan
Detects RBAC drift — compares actual state against baseline rules.
| Option | Description |
|---|---|
-p, --policy PATH |
Policy model YAML (required) |
-t, --tenant-id ID |
Tenant ID (ad-hoc mode) |
-s, --subscription ID |
Subscription to scan, repeatable (ad-hoc mode) |
-m, --management-group ID |
Management group to scan, repeatable (ad-hoc mode) |
-o, --output PATH |
HTML report output path |
-f, --format FORMAT |
console (default) or json |
--orphans-only |
Scan only for orphaned assignments (requires --tenant-id) |
--dry-run |
Show scan plan without making API calls |
--exclude-subscription ID |
Exclude subscription (repeatable) |
--exclude-management-group ID |
Exclude management group (repeatable) |
-v, --verbose |
Debug logging |
--debug |
Show full traceback on error |
az-rbac-watch audit
Checks governance guardrails — evaluates governance rules against actual state.
| Option | Description |
|---|---|
-p, --policy PATH |
Policy model YAML (required) |
-t, --tenant-id ID |
Tenant ID (ad-hoc mode) |
-s, --subscription ID |
Subscription to scan, repeatable (ad-hoc mode) |
-m, --management-group ID |
Management group to scan, repeatable (ad-hoc mode) |
-o, --output PATH |
HTML report output path |
-f, --format FORMAT |
console (default) or json |
--dry-run |
Show scan plan without making API calls |
--exclude-subscription ID |
Exclude subscription (repeatable) |
--exclude-management-group ID |
Exclude management group (repeatable) |
-v, --verbose |
Debug logging |
--debug |
Show full traceback on error |
az-rbac-watch discover
| Option | Description |
|---|---|
-t, --tenant-id ID |
Tenant ID |
-s, --subscription ID |
Subscription to scan (repeatable) |
-m, --management-group ID |
Management group to scan (repeatable) |
-o, --output PATH |
Output YAML (default: discovered_policy.yaml) |
az-rbac-watch validate
| Option | Description |
|---|---|
-p, --policy PATH |
Policy model YAML to validate (required) |
az-rbac-watch snapshot
Captures a full RBAC snapshot (assignments + role definitions) as JSON.
| Option | Description |
|---|---|
-p, --policy PATH |
Policy model YAML (uses its scopes) |
-t, --tenant-id ID |
Tenant ID |
-s, --subscription ID |
Subscription to scan (repeatable) |
-m, --management-group ID |
Management group to scan (repeatable) |
--exclude-subscription ID |
Exclude subscription (repeatable) |
--exclude-management-group ID |
Exclude management group (repeatable) |
-o, --output PATH |
Output JSON file (required) |
-v, --verbose |
Debug logging |
--debug |
Show full traceback on error |
az-rbac-watch diff
Compares two snapshots and shows RBAC changes (added, removed, modified assignments).
| Option | Description |
|---|---|
OLD_SNAPSHOT |
Path to the older snapshot JSON file (required) |
NEW_SNAPSHOT |
Path to the newer snapshot JSON file (required) |
-f, --format FORMAT |
console (default) or json |
-o, --output PATH |
Output file path |
Exit codes
| Code | Meaning |
|---|---|
0 |
Compliant — no findings detected |
1 |
Non-compliant — findings detected |
2 |
Error — authentication failure, API error, invalid YAML |
The diff command returns 0 for no changes, 1 for changes detected.
Example CI/CD usage:
# Check drift
az-rbac-watch scan -p policy.yaml --format json -o drift.json
scan_exit=$?
# Check guardrails
az-rbac-watch audit -p policy.yaml --format json -o audit.json
audit_exit=$?
if [ "$scan_exit" -eq 1 ] || [ "$audit_exit" -eq 1 ]; then
echo "Non-compliant — review reports"
elif [ "$scan_exit" -eq 2 ] || [ "$audit_exit" -eq 2 ]; then
echo "Scan error — check credentials and permissions"
fi
Troubleshooting
Authentication error
- Run
az loginto refresh your credentials - Verify
DefaultAzureCredentialcan authenticate (checkAZURE_*env vars or managed identity)
Access denied
- The scanning principal needs
Microsoft.Authorization/roleAssignments/readon each scoped subscription/MG - Check with:
az role assignment list --scope /subscriptions/<id> --assignee <principal-id>
Names not resolved (UUIDs shown)
- The App Registration (or user) needs
Directory.Read.Allpermission on Microsoft Graph - Run with
--verboseto see Graph API errors in the logs
Throttling
- Azure ARM API rate-limits parallel requests
- Reduce parallelism: the default is 4 workers (configurable in code via
max_workers) - Wait a few minutes and retry
Full traceback
- Use
--debugto see the complete Python traceback on any error
Rule match operators
All comparisons are case-insensitive. Conditions are combined with AND logic.
| Operator | Type | Description |
|---|---|---|
scope |
str |
Exact scope match |
scope_prefix |
str |
Scope starts with value |
role |
str |
Exact role name |
role_in |
list |
Role is in list |
role_not_in |
list |
Role is NOT in list |
role_type |
str |
BuiltInRole or CustomRole |
principal_type |
str |
User, Group, or ServicePrincipal |
principal_type_in |
list |
Principal type is in list |
principal_id |
str |
Exact principal ID |
principal_name_prefix |
str |
Display name starts with |
principal_name_not_prefix |
str |
Display name does NOT start with |
principal_name_contains |
str |
Display name contains |
principal_name_not_contains |
str |
Display name does NOT contain |
Name-based operators require Graph API access. If unavailable, they evaluate to false (no false positives).
Required Azure permissions
Microsoft.Authorization/roleAssignments/readon scanned scopesDirectory.Read.All(Graph API, for display name resolution)
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file az_rbac_watch-0.4.2.tar.gz.
File metadata
- Download URL: az_rbac_watch-0.4.2.tar.gz
- Upload date:
- Size: 196.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ae53bddf340fa38fa04c984e32389dc09fc3aeffee00188642f711dac33ce0a8
|
|
| MD5 |
f21481baf17d1cc2bb7a1bc7255b259e
|
|
| BLAKE2b-256 |
198fdeeca881e10d62c4f46ae3287b2028aad0a5114f43040f1f3b7e9e1fcbd0
|
Provenance
The following attestation bundles were made for az_rbac_watch-0.4.2.tar.gz:
Publisher:
publish.yml on maxvanp/az-rbac-watch
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
az_rbac_watch-0.4.2.tar.gz -
Subject digest:
ae53bddf340fa38fa04c984e32389dc09fc3aeffee00188642f711dac33ce0a8 - Sigstore transparency entry: 1066023800
- Sigstore integration time:
-
Permalink:
maxvanp/az-rbac-watch@0833ead4957b89543ca8b534288a06e9881fbe19 -
Branch / Tag:
refs/tags/v0.4.1 - Owner: https://github.com/maxvanp
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0833ead4957b89543ca8b534288a06e9881fbe19 -
Trigger Event:
push
-
Statement type:
File details
Details for the file az_rbac_watch-0.4.2-py3-none-any.whl.
File metadata
- Download URL: az_rbac_watch-0.4.2-py3-none-any.whl
- Upload date:
- Size: 45.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4170422171af5b080b95abe6ad69a52e661b3dd946d7919e9270b79e094a947b
|
|
| MD5 |
ae3988ae81bcdc7bfe07c4c903803854
|
|
| BLAKE2b-256 |
7cdbd14f9dab3333cc4d48668734a21e3c264da56b202dc68d88bb58d43957e5
|
Provenance
The following attestation bundles were made for az_rbac_watch-0.4.2-py3-none-any.whl:
Publisher:
publish.yml on maxvanp/az-rbac-watch
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
az_rbac_watch-0.4.2-py3-none-any.whl -
Subject digest:
4170422171af5b080b95abe6ad69a52e661b3dd946d7919e9270b79e094a947b - Sigstore transparency entry: 1066023862
- Sigstore integration time:
-
Permalink:
maxvanp/az-rbac-watch@0833ead4957b89543ca8b534288a06e9881fbe19 -
Branch / Tag:
refs/tags/v0.4.1 - Owner: https://github.com/maxvanp
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0833ead4957b89543ca8b534288a06e9881fbe19 -
Trigger Event:
push
-
Statement type: