Skip to main content

HTTP + MS Graph

The simplest approach: poll the Microsoft Graph API directly using the HTTP scraper. No log forwarding infrastructure needed.

When to use

  • Simplest setup — just a ScrapeConfig and credentials
  • You need full control over $filter, $select, $orderby
  • No existing log forwarding infrastructure
  • Works with any Entra ID P1/P2 tenant immediately

When NOT to use

  • You need sub-minute latency (use Event Hub instead)
  • You have >999 sign-ins per polling interval (no auto-pagination)
  • Logs are already forwarded to a backend (use Logs Scraper)
  • You're hitting Graph API rate limits across multiple scrapers

Prerequisites

  • Connection to Entra ID with OAuth2 client credentials
  • AuditLog.Read.All — Application permission, admin-consented
  • Entra ID P1 or P2 license on the tenant

Endpoints

Graph API EndpointDescription
/auditLogs/signInsInteractive and non-interactive sign-in events
/auditLogs/directoryAuditsDirectory changes (user/group/app modifications)

Field Mapping: Sign-In Logs

Graph API FieldMission Control FieldDescription
ididUnique sign-in event ID
createdDateTimecreated_atWhen the sign-in occurred
userPrincipalNameuser (alias)User who signed in
appDisplayNameconfig_nameApplication signed into
ipAddressipClient IP address
status.errorCodestatus0 = success, non-zero = failure
locationmetadata.locationCity/country from IP geolocation
conditionalAccessStatusmetadata.conditionalAccessConditional access policy result

Sign-In Logs

entra-signin-logs.yaml
apiVersion: configs.flanksource.com/v1
kind: ScrapeConfig
metadata:
name: entra-signin-logs
namespace: mc
spec:
schedule: "@every 15m"
full: true
http:
- url: "https://graph.microsoft.com/v1.0/auditLogs/signIns?$top=500&$orderby=createdDateTime desc"
oauth:
tokenURL: https://login.microsoftonline.com/{{.tenant_id}}/oauth2/v2.0/token
clientID:
valueFrom:
secretKeyRef:
name: azure-entra-credentials
key: AZURE_CLIENT_ID
clientSecret:
valueFrom:
secretKeyRef:
name: azure-entra-credentials
key: AZURE_CLIENT_SECRET
scope:
- https://graph.microsoft.com/.default
env:
- name: tenant_id
valueFrom:
secretKeyRef:
name: azure-entra-credentials
key: AZURE_TENANT_ID
transform:
expr: |
dyn(config).value.map(e, {
'config_type': 'Azure::SignIn',
'id': e.id,
'name': e.userPrincipalName + ' -> ' + e.appDisplayName,
'config': {
'id': e.id,
'userPrincipalName': e.userPrincipalName,
'userId': e.userId,
'appDisplayName': e.appDisplayName,
'appId': e.appId,
'ipAddress': e.ipAddress,
'status': e.status,
'createdDateTime': e.createdDateTime,
'location': e.?location.orValue({}),
'conditionalAccessStatus': e.?conditionalAccessStatus.orValue('')
},
'config_access': [{
'user': [e.userPrincipalName, e.userId],
'config_name': e.appDisplayName,
'config_type': 'Azure::EnterpriseApplication',
'status': e.status.errorCode == 0 ? 'success' : 'failure',
'ip': e.?ipAddress.orValue(''),
'created_at': e.createdDateTime
}]
}).toJSON()
Details
Example sign-in log output

After scraping, each sign-in event produces a config item and a config_access record:

{
"config_type": "Azure::SignIn",
"id": "66ea54eb-6301-4ee5-be62-ff5a759b0100",
"name": "jane.doe@contoso.com -> Payments API",
"config": {
"id": "66ea54eb-6301-4ee5-be62-ff5a759b0100",
"userPrincipalName": "jane.doe@contoso.com",
"userId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"appDisplayName": "Payments API",
"appId": "11112222-3333-4444-5555-666677778888",
"ipAddress": "203.0.113.42",
"status": { "errorCode": 0 },
"createdDateTime": "2025-01-15T14:32:00Z",
"location": { "city": "Sydney", "countryOrRegion": "AU" },
"conditionalAccessStatus": "success"
},
"config_access": [{
"user": ["jane.doe@contoso.com", "a1b2c3d4-e5f6-7890-abcd-ef1234567890"],
"config_name": "Payments API",
"config_type": "Azure::EnterpriseApplication",
"status": "success",
"ip": "203.0.113.42",
"created_at": "2025-01-15T14:32:00Z"
}]
}

Directory Audit Logs

entra-directory-audits.yaml
apiVersion: configs.flanksource.com/v1
kind: ScrapeConfig
metadata:
name: entra-directory-audits
namespace: mc
spec:
schedule: "@every 15m"
full: true
http:
- url: "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?$top=500&$orderby=activityDateTime desc"
oauth:
tokenURL: https://login.microsoftonline.com/{{.tenant_id}}/oauth2/v2.0/token
clientID:
valueFrom:
secretKeyRef:
name: azure-entra-credentials
key: AZURE_CLIENT_ID
clientSecret:
valueFrom:
secretKeyRef:
name: azure-entra-credentials
key: AZURE_CLIENT_SECRET
scope:
- https://graph.microsoft.com/.default
env:
- name: tenant_id
valueFrom:
secretKeyRef:
name: azure-entra-credentials
key: AZURE_TENANT_ID
transform:
expr: |
dyn(config).value.map(e, {
'config_type': 'Azure::DirectoryAudit',
'id': e.id,
'name': e.activityDisplayName,
'config': {
'id': e.id,
'activityDisplayName': e.activityDisplayName,
'activityDateTime': e.activityDateTime,
'category': e.category,
'result': e.result,
'initiatedBy': e.?initiatedBy.orValue({}),
'targetResources': e.?targetResources.orValue([])
}
}).toJSON()

For custom $filter queries and user attribute selection, see Microsoft Graph API for the full HTTP scraper pattern with CEL transforms.

Filtering High-Volume Sign-In Logs

Use $filter in the URL to reduce volume and focus on relevant events:

Failed sign-ins only:

/auditLogs/signIns?$filter=status/errorCode ne 0&$top=500&$orderby=createdDateTime desc

Sign-ins for a specific application:

/auditLogs/signIns?$filter=appDisplayName eq 'Payments API'&$top=500&$orderby=createdDateTime desc

Sign-ins without MFA:

/auditLogs/signIns?$filter=authenticationRequirement eq 'singleFactorAuthentication'&$top=500&$orderby=createdDateTime desc
warning

Not all OData $filter operators are supported on sign-in log endpoints. The eq, ne, startsWith, and ge/le operators work on most fields, but contains and endsWith are not supported. See the MS Graph signIn resource documentation for supported filter properties.