Microsoft Entra ID
Overview
Microsoft Entra ID (formerly Azure Active Directory) is a pull-based source that fetches your tenant's directory from Microsoft Graph as identity context. You choose which kinds of objects to collect:
| Data | Microsoft Graph API | Default | Google SecOps log type |
|---|---|---|---|
| Users: every user's profile, account status, department, on-premises identifiers and manager | GET /beta/users?$expand=manager | On | AZURE_AD_CONTEXT |
| Groups: every group's name, type and settings (without members) | GET /beta/groups | Off | none (see below) |
| Devices: every registered device, with its owners | GET /beta/devices?$expand=registeredOwners | Off | none (see below) |
| Admin roles: every active admin role assignment, meaning who (user, group or app) holds which Entra role, such as Global Administrator | GET /beta/roleManagement/directory/roleAssignments?$expand=principal | Off | none (see below) |
The user call is the same one the Google SecOps native Azure AD Organizational Context feed makes. Every object is sent exactly as Graph returns it, so the Google SecOps AZURE_AD_CONTEXT parser receives the complete user record. This is not an event stream: it is a periodic snapshot of the directory that Google SecOps uses to enrich other logs with who a user is.
Groups, devices and admin roles are not understood by Google SecOps today. The
AZURE_AD_CONTEXTparser maps users only, and there is no Google SecOps log type for Entra ID groups, devices or role assignments. Collect them for other destinations or for search, and never label themAZURE_AD_CONTEXT(see Sending to Google SecOps).
Admin roles use Microsoft's recommended role-management API rather than
/directoryRoles, which lists only role names, not who holds them. Only active assignments are included; PIM eligible assignments (people who can activate a role) are not.
Sign-in logs (
AZURE_AD) are not collected by this source. Use the Azure Event Hub source with an Entra ID diagnostic setting that exportsSignInLogs(andNonInteractiveUserSignInLogs,ServicePrincipalSignInLogs,ManagedIdentitySignInLogsas needed).
Supported types: Logs
Supported platforms: Linux, Windows, macOS
Minimum collector version: 0.5.62
Authentication
Attach an Azure Auth (azureauth) credential, the shared Azure identity credential also used by the Azure Event Hub source and the Microsoft Sentinel destination. The node holds no secret.
The identity behind the credential needs one Microsoft Graph application permission per kind of data you collect:
| Permission | Type | Needed for |
|---|---|---|
User.Read.All | Application | Users (on by default) |
Group.Read.All | Application | Groups |
Device.Read.All | Application | Devices |
RoleManagement.Read.Directory | Application | Admin roles |
Set up an app registration (service principal)
- In the Microsoft Entra admin center, go to Identity → Applications → App registrations → New registration. Choose Accounts in this organizational directory only and leave the redirect URI empty.
- Copy the Application (client) ID and Directory (tenant) ID.
- Under API permissions, choose Add a permission → Microsoft Graph → Application permissions, then add
User.Read.All, plusGroup.Read.All,Device.Read.Alland/orRoleManagement.Read.Directoryif you collect groups, devices or admin roles. - Click Grant admin consent for your organization. This needs the Global Administrator or Privileged Role Administrator role. The status column must show Granted for every permission.
- Under Certificates & secrets, create a client secret and copy its Value.
- In Praxis, create an
azureauthcredential with Auth Typeservice_principaland the tenant ID, client ID and secret.
They must be application permissions, not delegated ones. The collector signs in as the app itself, so a delegated permission grants nothing and every request fails with
403 Authorization_RequestDenied.
Set up a managed identity (collector running in Azure)
A managed identity stores no secret, but works only when the collector runs on an Azure resource (VM, VM scale set, AKS node, Container Apps) that the identity is assigned to.
azureauth Auth Type | Use when | Credential fields |
|---|---|---|
system_managed_identity | The VM/scale set has its own identity (Identity → System assigned → On) | none |
user_managed_identity | You created a Managed Identity resource and assigned it to the VM/scale set | the identity's Client ID |
workload_identity | The collector runs in AKS with workload identity | tenant ID, client ID, federated token file (usually /var/run/secrets/azure/tokens/azure-identity-token) |
The identity needs the same Microsoft Graph application permissions as an app registration, but the Entra admin center cannot grant them to a managed identity. An administrator with Privileged Role Administrator or Global Administrator assigns them with Microsoft Graph PowerShell, using the identity's Object (principal) ID. That ID is shown on the VM's Identity page for system-assigned identities, and on the Managed Identity resource's Overview page for user-assigned ones.
Connect-MgGraph -Scopes "Application.Read.All","AppRoleAssignment.ReadWrite.All"
$miObjectId = "<managed identity object (principal) ID>"
$graph = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
# Keep only the permissions for the data you collect.
foreach ($perm in "User.Read.All","Group.Read.All","Device.Read.All","RoleManagement.Read.Directory") {
$role = $graph.AppRoles | Where-Object { $_.Value -eq $perm -and $_.AllowedMemberTypes -contains "Application" }
New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $miObjectId -PrincipalId $miObjectId -ResourceId $graph.Id -AppRoleId $role.Id
}
To check the result, open Enterprise applications, set the Application type filter to Managed Identities, select the identity, and open Permissions.
To test from the Azure VM itself before deploying (add &client_id=<client ID> to the URL for a user-assigned identity):
TOKEN=$(curl -s -H Metadata:true "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://graph.microsoft.com/" | python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])')
curl -s -H "Authorization: Bearer $TOKEN" 'https://graph.microsoft.com/beta/users?$top=1' | head -c 400
Newly granted permissions can take several hours to reach a managed identity. Azure caches managed identity tokens. A
403straight after granting a permission usually clears on its own; the source keeps retrying.
Configuration
Data to collect
| Parameter | Type | Default | Description |
|---|---|---|---|
users.enabled | bool | true | Collect users. |
groups.enabled | bool | false | Collect groups. Needs Group.Read.All. |
devices.enabled | bool | false | Collect devices. Needs Device.Read.All. |
roles.enabled | bool | false | Collect admin role assignments. Needs RoleManagement.Read.Directory. |
At least one must be on. Each kind is fetched and tracked independently: if, say, the app lacks Group.Read.All, groups fail and retry on their own while users keep flowing.
Schedule
One schedule applies to every kind you collect.
| Parameter | Type | Default | Description |
|---|---|---|---|
schedule.mode | scheduled | manual | scheduled | scheduled: fetch everything every schedule.interval. manual: fetch only with Fetch Now. |
schedule.interval | integer (seconds) | 86400 | Time between snapshots in scheduled mode. Minimum 900 (15 minutes). |
schedule.sync_request | integer | 0 | Fetch Now: the on-demand trigger, shown and used in manual mode only. See below. |
Options
| Parameter | Type | Default | Description |
|---|---|---|---|
users.expand_manager | bool | true | Embed each user's manager. Google SecOps maps it to the user's managers. |
devices.expand_owners | bool | true | Embed each device's registered owners (up to 20), linking a device to its users. |
persist_offsets | bool | true | Persist Fetch State. Remember when each type was last fetched and which Fetch Now was completed, so restarts don't fetch everything again. See Fetch state. |
advanced
| Parameter | Type | Default | Description |
|---|---|---|---|
users.select | list of strings | none | Graph user properties to fetch ($select). Leave empty to get Graph's default set, which is what Google SecOps expects. When set, Graph returns only these properties. Use it to add directory extension attributes (extension_<appId>_<name>). Properties Graph only returns for a single user (aboutMe, birthday, hireDate, interests, mailboxSettings, mySite, pastProjects, preferredName, responsibilities, schools, skills) and signInActivity are rejected when you save. |
api_version | beta | v1.0 | beta | Graph API version. beta matches the Google SecOps native feed. |
page_size | integer | 999 | Users requested per Graph call (1–999). Graph may return fewer per page when expanding managers; the source always follows the next-page link. |
timeout | integer (seconds) | 60 | HTTP timeout for requests to Microsoft Graph. |
tls.insecure_skip_verify | bool | false | Disable TLS server certificate verification. Not recommended in production. |
tls.ca_file | string | none | Custom CA bundle, for example for a TLS-inspecting proxy. |
In the pipeline editor, User Properties (users.select) appears inside the Advanced group, but it is a key of the users block.
Fetching on demand
Fetch Now appears only when Fetch Mode is manual, and in that mode it is the only way data is fetched.
- Set Fetch Mode to
manual. - In Fetch Now, enter a number higher than the current one (for example
1→2) and deploy. Every kind you collect is fetched once.
Fetch state
With Persist Fetch State on (the default), the collector stores two small values per type of data (users, groups, devices, admin roles) under <collector home>/metadata/entra_id_offsets/<node name>:
| Stored value | Example | Used for |
|---|---|---|
| Time the last complete fetch finished | 2026-10-09T07:15:00Z | After a restart, wait until that time plus the interval instead of fetching now. |
| Last Fetch Now value completed | 5 | After a restart, do not repeat a Fetch Now that already ran. |
No records and no position in the directory data are stored. Every fetch reads the whole directory from the first page, so the order in which Microsoft Graph returns objects does not matter. If a fetch fails part-way, the page to continue from is kept in memory only and is dropped on restart, when Graph rejects it, or after 3 failed attempts; the next attempt then starts from the first page.
Turn Persist Fetch State off only on collectors that cannot write to disk (read-only filesystem, ephemeral container without a volume). Without it, every collector restart (config push, upgrade, reboot) fetches the whole directory again, and in manual mode repeats the last Fetch Now. If the stored files are deleted, the next restart simply fetches once.
Sending to Google SecOps
The source does not set a Google SecOps log type itself. Add a Google SecOps Standardization processor between this source and the Google SecOps destination with:
| Field | Value |
|---|---|
| Log Type | AZURE_AD_CONTEXT |
| Condition | resource.attributes["entraid.source"] == "users" |
The condition is required whenever groups, devices or admin roles are on: without it, those records would be labeled AZURE_AD_CONTEXT and fed to a parser that only understands users. If you also send to Google SecOps, either drop non-user records before the SecOps destination (for example with a filter on resource.attributes["entraid.source"] != "users") or route them to a different destination.
Output mapping
Every object becomes one log record. All records carry:
| Source | OTel destination |
|---|---|
| (whole object JSON, as Graph returned it) | LogRecord.Body (string) |
| (snapshot start time) | LogRecord.Timestamp: the record describes the object as of that snapshot |
| (constant) | Resource.Attributes["vendor"] = "microsoft" |
| (kind of object) | Resource.Attributes["entraid.source"] = users, groups, devices or roles |
| (Graph host) | Resource.Attributes["entraid.graph_host"] |
Promoted attributes, each only when present:
| Kind | Graph field → attribute |
|---|---|
| Users | id → entraid.user.id, userPrincipalName → entraid.user.principal_name, userType → entraid.user.type, accountEnabled → entraid.user.account_enabled (bool) |
| Groups | id → entraid.group.id, displayName → entraid.group.display_name, securityEnabled → entraid.group.security_enabled (bool), mailEnabled → entraid.group.mail_enabled (bool) |
| Devices | id → entraid.device.id, deviceId → entraid.device.device_id, displayName → entraid.device.display_name, operatingSystem → entraid.device.operating_system, accountEnabled → entraid.device.account_enabled (bool) |
| Admin roles | id → entraid.role_assignment.id, roleDefinitionId → entraid.role.definition_id, (role name, looked up) → entraid.role.name, directoryScopeId → entraid.role.directory_scope_id, principalId → entraid.principal.id, principal type → entraid.principal.type (user, group, servicePrincipal), principal.displayName → entraid.principal.display_name, principal.userPrincipalName → entraid.principal.user_principal_name |
The promoted attributes exist for routing and filtering. The body is untouched, so a downstream parser still sees the complete record.
Example configuration
{
"users": { "enabled": true, "expand_manager": true }, // default: on
"groups": { "enabled": true }, // default: off
"devices": { "enabled": false, "expand_owners": true }, // default: off
"roles": { "enabled": true }, // default: off
"schedule": {
"mode": "scheduled", // default: scheduled
"interval": 86400, // default: 86400, minimum 900
"sync_request": 0 // manual mode: raise and deploy to fetch everything now
},
"persist_offsets": true, // default: true
"advanced": {
"api_version": "beta", // default: beta
"page_size": 999, // default: 999
"timeout": 60 // default: 60
}
}
Operational notes
- Volume. Each snapshot sends one record per object: a daily snapshot of a tenant with 50,000 users, 3,000 groups, 20,000 devices and 200 admin role assignments is about 73,200 records a day with everything on.
- Admin role names. Each admin-roles snapshot first reads the role definitions (one or two extra calls) to add the role name to each record.
- Switching tenants. Fetch state is stored per node name, not per tenant. If you point an existing node's credential at a different tenant, its first fetch can wait up to one interval. Switch to
manualand raise Fetch Now to fetch the new tenant immediately. - Partial snapshots. Pages are sent downstream as they arrive. If a snapshot fails part-way (for example Graph keeps returning errors), the next attempt resumes at the page that failed, so objects already sent are not sent again. Only if the collector restarts in between does the snapshot start over.
- Retries. Throttling (
429) and server errors (5xx) are retried up to 5 times, honoring Graph'sRetry-After. A failed snapshot is retried after 5 minutes, then with doubling waits up to 1 hour. - Graph throttling budget. Each list call costs 1–3 resource units per page against a per-app, per-tenant budget of 3,500–8,000 units per 10 seconds, so even large tenants snapshot everything well inside the limit.
- Group membership is not collected. Listing each group's members costs one extra Graph call per group.
- Conditional Access. If a Conditional Access policy restricts workload identities by location, exclude the collector's egress IPs (or this app's service principal) from it.
- Sovereign clouds (US Government, China) are not supported yet.
Troubleshooting
| Symptom in the collector log | Cause | Fix |
|---|---|---|
entra id <kind> snapshot failed … graph returned 403 Authorization_RequestDenied, with a hint naming a permission | The app lacks that kind's application permission (User.Read.All, Group.Read.All, Device.Read.All or RoleManagement.Read.Directory), or admin consent was not granted. | Add the application permission and click Grant admin consent. |
get Entra ID token: … (for example AADSTS7000215: Invalid client secret), with a hint to check the credential | Service principal: wrong tenant ID or client ID, the secret's ID was entered instead of its Value, or the secret expired. | Fix or rotate the azureauth credential. |
get Entra ID token: … ManagedIdentityCredential: … | Managed identity: the collector is not running on an Azure resource, the identity is not assigned to that resource, or (user-assigned) the client ID is wrong. | Assign the identity to the VM / scale set / container, or fix the client ID. |
graph returned 401 InvalidAuthenticationToken | A token was obtained but Graph rejected it, typically a token for the wrong tenant. | Check the credential's tenant ID. |
No records and no errors in manual mode | Fetch Now was not raised above the value of the last completed fetch. | Enter a higher number and deploy. |
Metrics
All kinds report under the same node.
| Metric Name | Description |
|---|---|
prx_src_records_received_total{signal="logs"} | Log records emitted, one per user, group, device or role assignment. |
prx_src_bytes_received_total | Response bytes read from Microsoft Graph. |
prx_src_pipeline_latency_seconds | Arrival to downstream handoff, one sample per page. |
prx_src_errors_total{reason="io"} | Graph request failures, including permission errors and exhausted retries. |
prx_src_parse_errors_total{parser="json"} | A Graph response that was not a list, or objects whose fields had unexpected types (those objects are still sent). |
prx_src_records_dropped_total{reason="backpressure"} | Records dropped because the downstream pipeline refused them. |