<!-- Copyright (c) 2026 defconxt. All rights reserved. -->
<!-- Licensed under AGPL-3.0 — see LICENSE file for details. -->
---
name: auditing-azure-active-directory
description: >-
  Audit Azure Active Directory (Entra ID) configurations for security gaps including
  risky sign-in policies, excessive privileged role assignments, stale accounts,
  conditional access misconfigurations, and app registration vulnerabilities.
domain: cybersecurity
subdomain: cloud-security
tags:
  - azure
  - entra-id
  - active-directory
  - conditional-access
  - pim
  - app-registration
  - mfa
  - identity-security
version: "1.0"
author: defconxt
license: AGPL-3.0
metadata:
  mitre-attack: ["T1078.004"]
---

# Auditing Azure Active Directory

## Overview

Azure Active Directory (now Entra ID) is the identity backbone for Azure and Microsoft
365 environments. Misconfigurations in Entra ID — such as excessive Global Admin
assignments, missing conditional access policies, stale accounts, and overprivileged
app registrations — are among the most exploited attack vectors in cloud breaches.
This skill provides systematic audit procedures using Azure CLI and Microsoft Graph.

Mode: `[MODE: BLUE]` — Identity security posture assessment.

## Prerequisites

- Azure CLI (`az`) authenticated with sufficient privileges
- Permissions: `Directory.Read.All`, `RoleManagement.Read.Directory`, `Policy.Read.All`,
  `AuditLog.Read.All`, `UserAuthenticationMethod.Read.All`
- Python 3.10+ with `azure-identity`, `azure-mgmt-authorization`, `msgraph-sdk`
- Optional: Azure AD Privileged Identity Management (PIM) license

## Key Concepts

### Privileged Role Assignments

Excessive permanent Global Admin assignments increase blast radius:

```bash
# List all Global Administrator assignments
az role assignment list --role "Global Administrator" --all --output table

# List all privileged role assignments (PIM eligible vs active)
az rest --method GET \
  --url "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments" \
  --query "value[].{PrincipalId:principalId,RoleId:roleDefinitionId,DirectoryScope:directoryScopeId}"

# Count users per privileged role
az ad user list --query "[?assignedPlans[?servicePlanId!=null]]" --output table
```

### Conditional Access Policies

```bash
# List all conditional access policies
az rest --method GET \
  --url "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" \
  --query "value[].{Name:displayName,State:state,Conditions:conditions.users}"

# Find policies in report-only mode (not enforced)
az rest --method GET \
  --url "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" \
  --query "value[?state=='enabledForReportingButNotEnforced'].{Name:displayName,State:state}"
```

### Stale and Risky Accounts

```bash
# Find users who haven't signed in for 90+ days
az ad user list --query "[].{UPN:userPrincipalName,DisplayName:displayName,LastSignIn:signInActivity.lastSignInDateTime}" \
  --output table

# List users without MFA registered
az rest --method GET \
  --url "https://graph.microsoft.com/v1.0/reports/authenticationMethods/userRegistrationDetails" \
  --query "value[?!isMfaRegistered].{UPN:userPrincipalName,MFA:isMfaRegistered}"

# Check for risky users
az rest --method GET \
  --url "https://graph.microsoft.com/v1.0/identityProtection/riskyUsers" \
  --query "value[?riskLevel=='high'].{UPN:userDisplayName,RiskLevel:riskLevel,RiskState:riskState}"
```

### App Registration Security

```bash
# List app registrations with secrets/certificates
az ad app list --all --query "[].{AppId:appId,DisplayName:displayName,PasswordCredentials:passwordCredentials}" \
  --output json | jq '.[] | select(.PasswordCredentials | length > 0)'

# Find apps with broad API permissions
az ad app list --all --query "[].{AppId:appId,Name:displayName,RequiredResourceAccess:requiredResourceAccess}" \
  --output json | jq '.[] | select(.RequiredResourceAccess[]?.resourceAccess[]?.type == "Role")'

# Find expired or soon-expiring credentials
az ad app list --all --output json | jq '
  .[] | select(.passwordCredentials[]?.endDateTime < (now | strftime("%Y-%m-%dT%H:%M:%SZ")))
  | {appId, displayName, expiredCreds: [.passwordCredentials[] | select(.endDateTime < (now | strftime("%Y-%m-%dT%H:%M:%SZ")))]}'
```

### Guest User Audit

```bash
# List all guest users
az ad user list --filter "userType eq 'Guest'" \
  --query "[].{UPN:userPrincipalName,DisplayName:displayName,CreatedDateTime:createdDateTime}" \
  --output table

# Find guests with privileged role assignments
az rest --method GET \
  --url "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments?\$expand=principal" \
  --query "value[?principal.userType=='Guest'].{Principal:principal.displayName,RoleId:roleDefinitionId}"
```

## Workflow

### Step 1: Enumerate Privileged Roles

```bash
# Count permanent vs eligible admin assignments
az rest --method GET \
  --url "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments" | \
  jq '[.value[] | .roleDefinitionId] | group_by(.) | map({role: .[0], count: length}) | sort_by(-.count)'
```

### Step 2: Audit Conditional Access Coverage

```bash
# Verify MFA enforcement for admins
az rest --method GET \
  --url "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" | \
  jq '.value[] | select(.conditions.users.includeRoles != null) | {name: .displayName, state: .state, roles: .conditions.users.includeRoles}'
```

### Step 3: Run Agent Script

```bash
node scripts/agent.js --tenant-id $TENANT_ID --output /tmp/aad_audit.json
```

## Detection

```yaml
title: Auditing Azure Active Directory Detection
id: c4b3595f-ca8f-4f88-947c-06c00af8d558
status: experimental
description: Detects suspicious activity related to auditing azure active directory techniques in cloud security context
logsource:
  product: aws
  service: cloudtrail
detection:
  selection:
    eventName: "*Unauthorized*"
  condition: selection
level: medium
tags:
  - attack.t1078.004
  - attack.credential_access
falsepositives:
  - Automated cloud infrastructure provisioning by approved CI/CD pipelines
```

## Common Findings

| Finding | Severity | CIS Control | Remediation |
|---|---|---|---|
| Global Admin assigned to >5 accounts | Critical | CIS 1.1.1 | Reduce to 2-4 break-glass accounts; use PIM |
| No MFA on privileged roles | Critical | CIS 1.1.3 | Enforce MFA via Conditional Access policy |
| Stale privileged accounts (>90d inactive) | High | CIS 1.1.20 | Disable or delete via Access Reviews |
| Legacy authentication not blocked | High | CIS 1.1.4 | Create CA policy blocking legacy auth protocols |
| PIM not enabled for privileged roles | High | CIS 1.1.15 | Enable PIM with approval workflow |
| App registrations with owner-only oversight | Medium | CIS 1.3.1 | Implement periodic app registration review |
| Guest user external collaboration unrestricted | Medium | CIS 1.1.21 | Scope guest invitations to admins only |
| Sign-in risk policies in report-only mode | Medium | — | Switch Identity Protection policies to enforce |

## Verification

- [ ] Global Admin assignments limited to 2-4 break-glass accounts
- [ ] PIM enabled with time-limited privileged role activation
- [ ] Conditional access enforces MFA for all privileged roles
- [ ] No stale accounts (>90 days inactive) with active privileges
- [ ] All app registrations reviewed for excessive API permissions
- [ ] Guest user access scoped and regularly reviewed
- [ ] Sign-in risk policies configured and enforced (not report-only)
- [ ] Legacy authentication protocols blocked via conditional access

## References

- [CIS Microsoft Azure Foundations Benchmark — Identity](https://www.cisecurity.org/benchmark/azure)
- [Microsoft Entra ID Security Operations Guide](https://learn.microsoft.com/en-us/entra/architecture/security-operations-introduction)
- [MITRE ATT&CK T1078.004 — Cloud Accounts](https://attack.mitre.org/techniques/T1078/004/)
- [Azure AD Conditional Access Documentation](https://learn.microsoft.com/en-us/entra/identity/conditional-access/)
