M365 Setup for dfe-fetcher
What a cloud administrator configures so dfe-fetcher can read security and audit data from Microsoft 365 (read-only, pull-mode).
Overview
dfe-fetcher reads M365 over two parallel surfaces, authenticating to each via
the OAuth2 client_credentials flow against
https://login.microsoftonline.com/<tenant>/oauth2/v2.0/token:
- Office 365 Management Activity API (
manage.office.com, scopehttps://manage.office.com/.default) - raw per-event audit records, fetched per content type. Backsaudit_log,dlp, andexchange_audit. - Microsoft Graph Security (
graph.microsoft.com, scopehttps://graph.microsoft.com/.default) - curated, post-classification alerts fromsecurity/alerts_v2. Backsalertsonly.
Services and their behaviour:
| Service | Surface | What it pulls |
|---|---|---|
audit_log | OMAP | Iterates content types. Default set: Audit.General, Audit.AzureActiveDirectory, Audit.Exchange, Audit.SharePoint, DLP.All. Emits one result per content type, tagged m365.audit_log.<content_type>. Override with service config content_types: [...]. |
exchange_audit | OMAP | Audit.Exchange only - per-message audit records. Tagged m365.exchange_audit. |
dlp | OMAP | DLP.All only - raw DLP rule-match events. Tagged m365.dlp. |
alerts | Graph | GET /v1.0/security/alerts_v2. Tagged m365.alerts. |
The source is the shipped m365 profile
(crates/fetcher/profiles/m365.yaml); the sources.m365 block below maps
onto an instance of it at load. On the OMAP path every tick starts each
content type's subscription blind (the API answers an already-enabled feed
with a 400 the profile ignores), asks for the content list in steps of at most
a day, follows NextPageUri, and fetches each listed blob in list order; every
row carries its blob as a checkpoint item, committed once the row is
acknowledged. One token per audience is minted per instance and shared by the
units that need it. A 429 or 5xx is retried with backoff (honouring
Retry-After), a 401, 403 or 404 ends the tick with the API's
error.message, and a tick that fails does not advance the fetch window. A
one-time manual subscription start is still the recommended bootstrap (see
Source-Side Setup).
Prerequisites
- An Entra ID (Azure AD) tenant with M365 audit logging enabled in the Purview / compliance portal (the unified audit log must be turned on for OMAP to return content).
- An Entra app registration (
client_id) with a client secret, in the target tenant (tenant_id). - A Global Administrator (or Application Administrator) to grant admin consent.
azCLI logged into the target tenant, or OpenTofu withaz login.
Required Permissions
All permissions are application (app-only, not delegated) and read-only.
Resource App IDs: Office 365 Management API = c5393580-f805-4401-95e8-94b7a6ef2fc2,
Microsoft Graph = 00000003-0000-0000-c000-000000000000.
| Used by | API | Permission | Permission ID | Type |
|---|---|---|---|---|
audit_log, exchange_audit | Office 365 Management | ActivityFeed.Read | 594c1fb6-4f81-4475-ae41-0c394909246c | Application |
dlp | Office 365 Management | ActivityFeed.ReadDlp | 4807a72c-ad38-4250-94c9-4eabfe26cd55 | Application |
alerts | Microsoft Graph | SecurityAlert.Read.All | 472e4a4d-bb4a-4026-98d1-0b0d74cb74a5 | Application |
Notes:
ActivityFeed.Readcovers theAudit.*content types.DLP.Allcontent (sensitive-data detections) requiresActivityFeed.ReadDlp. Grant both if you fetchdlpor the defaultaudit_logset (which includesDLP.All).472e4a4d-bb4a-4026-98d1-0b0d74cb74a5is the application ID forSecurityAlert.Read.All(the delegated variant is a different GUID). This is the only Graph permission the current code needs - it readsalerts_v2.SecurityEvents.Read.All(thealerts_v1permission) andReports.Read.Allare provisioned by the test module for convenience but are not required by the live code path.
Source-Side Setup
OpenTofu (Automated)
The test infrastructure lives in
infra/test/main.tf (M365 section). It creates an
app registration with the permission IDs above, a service principal, and a
client secret.
cd infra/test
cp terraform.tfvars.example terraform.tfvars
az login # must target the M365 tenant
tofu init
tofu apply
tofu output -json | python3 gen-env.py > ../../.env
The module cannot grant admin consent. After apply (requires Global Admin):
az ad app permission admin-consent --id <m365_client_id>
Manual Setup (az CLI)
-
Create the app registration
az ad app create --display-name "dfe-fetcher-m365" -
Add the Office 365 Management API permissions
# ActivityFeed.Readaz ad app permission add --id <app_id> \--api c5393580-f805-4401-95e8-94b7a6ef2fc2 \--api-permissions 594c1fb6-4f81-4475-ae41-0c394909246c=Role# ActivityFeed.ReadDlp (for the dlp service / DLP.All content)az ad app permission add --id <app_id> \--api c5393580-f805-4401-95e8-94b7a6ef2fc2 \--api-permissions 4807a72c-ad38-4250-94c9-4eabfe26cd55=Role -
Add the Microsoft Graph permission
az ad app permission add --id <app_id> \--api 00000003-0000-0000-c000-000000000000 \--api-permissions 472e4a4d-bb4a-4026-98d1-0b0d74cb74a5=Role -
Grant admin consent (Global Admin)
az ad app permission admin-consent --id <app_id> -
Create a client secret
az ad app credential reset --id <app_id> --years 1 -
Create the service principal (if not auto-created)
az ad sp create --id <app_id> -
Activate the OMAP audit subscriptions. The Office 365 Management Activity API will not return content until a subscription is started per content type. dfe-fetcher starts each subscription on every tick, but do this once to bootstrap so the very first fetch has data:
TENANT_ID="your-tenant-uuid"TOKEN=$(az account get-access-token \--resource https://manage.office.com --query accessToken -o tsv)for CT in Audit.General Audit.AzureActiveDirectory Audit.Exchange Audit.SharePoint DLP.All; docurl -s -X POST \"https://manage.office.com/api/v1.0/${TENANT_ID}/activity/feed/subscriptions/start?contentType=${CT}" \-H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" -d '{}'doneSubscriptions persist until explicitly stopped. Verify consent later with:
az ad app permission list-grants --id <app_id>
dfe-fetcher Configuration
Config File
sources:
m365:
enabled: true
tenant_id: "your-tenant-uuid"
client_id: "your-client-uuid"
client_secret: "your-client-secret"
# publisher_identifier: "your-own-guid" # keys this deployment's OMAP rate budget
services:
- name: audit_log
# config:
# content_types: ["Audit.General", "Audit.SharePoint"] # opt out of feeds
- name: exchange_audit
- name: dlp
- name: alerts
topic: "m365"
# filter: 'Workload != "Exchange"' # CEL, hot-reloaded
Recognised top-level fields: enabled, tenant_id, client_id,
client_secret, credential_secret, publisher_identifier,
interval_secs, services, connections, topic, filter,
management_url_override, graph_url_override, token_url_override.
Service names are audit_log, exchange_audit, dlp and alerts; the OMAP
Audit.Exchange feed is exchange_audit. A missing tenant or secret, an
unknown service name, or a content_types entry outside the five feeds is
refused at load, naming sources.m365.
publisher_identifier sets the OMAP PublisherIdentifier on every
Management Activity call, including the subscription start. It keys the
per-publisher rate budget, so set your own GUID per deployment rather than
sharing the default; it is a config key, not an environment variable.
Environment Variables
Pattern: DFE_FETCHER_SOURCES__M365__<FIELD> (double underscores).
DFE_FETCHER_SOURCES__M365__ENABLED="true"
DFE_FETCHER_SOURCES__M365__TENANT_ID="your-tenant-uuid"
DFE_FETCHER_SOURCES__M365__CLIENT_ID="your-client-uuid"
DFE_FETCHER_SOURCES__M365__CLIENT_SECRET="your-client-secret"
Secrets Manager
Set credential_secret to a secret-store spec. It supplies the client SECRET
only and resolves to a single string, so keep client_id as the literal
application id beside it. A JSON blob is not parsed.
sources:
m365:
enabled: true
tenant_id: "your-tenant-uuid"
client_id: "your-client-uuid"
credential_secret: "vault:kv/data/dfe/m365:client_secret"
services:
- name: audit_log
- name: alerts
topic: "m365"
For multiple tenants (MSP scenario), run one dfe-fetcher instance per tenant -
each with its own app registration, admin consent, activated subscriptions, and
a distinct instance_id.
Verification
-
Health check. The health check is the management token exchange. A healthy result confirms OMAP-backed services will authenticate; Graph
alertsneeds the separateSecurityAlert.Read.Allgrant, which the health check does not exercise. -
Env-gated smoke tests. Live tests live in
crates/fetcher/tests/e2e/smoke_remote.rs, all#[ignore]'d. They readM365_TENANT_ID,M365_CLIENT_IDandM365_CLIENT_SECRETfrom.env-cloud, falling back to theAZURE_*equivalents when an M365 variable is unset.cargo test -p dfe-fetcher --test e2e m365_ -- --ignored -
Common failures.
403 Forbiddenon every call: admin consent not granted.403onalertsonly: missingSecurityAlert.Read.All.audit_log/dlpreturns nothing: unified audit log not enabled in Purview, or the subscription was never started (the bootstrap step). A404on the content list fails that unit's tick; the next tick's subscription start re-arms it and the window has not moved.- DLP content missing while
Audit.*works: missingActivityFeed.ReadDlp. - HTTP 429: OMAP throttling - dfe-fetcher retries with backoff and
Retry-Afterup to the profile's policy, then fails the tick without advancing; set your ownpublisher_identifierso the deployment has its own rate budget.
Cost
dfe-fetcher only reads. Entra ID app registrations, Office 365 Management Activity API reads, and Microsoft Graph Security API reads are not expected to carry an additional charge, though this can depend on your M365 licensing. Confirm any cost implications against your own Microsoft agreement.
References
- Office 365 Management Activity API reference: https://learn.microsoft.com/en-us/office/office-365-management-api/office-365-management-activity-api-reference
- Get started with Office 365 Management APIs (permissions, ActivityFeed.Read/ReadDlp): https://learn.microsoft.com/en-us/office/office-365-management-api/get-started-with-office-365-management-apis
- Microsoft Graph permissions reference: https://learn.microsoft.com/en-us/graph/permissions-reference
- Graph security alerts_v2 (list): https://learn.microsoft.com/en-us/graph/api/security-list-alerts_v2?view=graph-rest-1.0
- OAuth2 client credentials (Microsoft identity platform): https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow