This document details the steps needed to create a new Azure Data Explorer table for ingested logs from Azure Active Directory.
All ingested logs from AAD are written to a table in ADX named MyAADLogs
, this table is overwritten over and over thus the need to create a parsing function which is used to filter the new ingested logs by their category
and construct new records out of it to then write them to their corresponding tables.
First step is to query the MyAADLogs
table filtering by the record.category
property and expanding those properties of interest from each record. We can query the same logs using Log Analytics for comparison. For example, for NonInteractiveUserSignInLogs
:
MyAADLogs
| mv-expand record = data.records
| where tostring(record.category) == 'NonInteractiveUserSignInLogs'
| extend LocationDetails = todynamic(record.properties.['location'])
| project
TimeGenerated = todatetime(record.['time']),
OperationName = record.['operationName'],
OperationVersion = record.['operationVersion'],
Category = record.['category'],
ResultType = record.['resultType'],
ResultSignature = record.['resultSignature'],
ResultDescription = record.['resultDescription'],
DurationMs = record.['durationMs'],
CorrelationId = record.['correlationId'],
ResourceGroup = split(record.['resourceId'], '/')[-1],
Identity = record.['identity'],
Location = LocationDetails['countryOrRegion'],
AppDisplayName = record.properties.['appDisplayName'],
AppId = record.properties.['appId'],
AuthenticationContextClassReferences = record.properties['authenticationContextClassReferences'],
AuthenticationDetails = record.properties['authenticationDetails'],
AuthenticationProcessingDetails = record.properties.['authenticationProcessingDetails'],
AuthenticationProtocol = record.properties.['authenticationProtocol'],
AuthenticationRequirement = record.properties.['authenticationRequirement'],
AuthenticationRequirementPolicies = record.properties.['authenticationRequirementPolicies'],
AutonomousSystemNumber = record.properties.['autonomousSystemNumber'],
ClientAppUsed = record.properties.['clientAppUsed'],
CreatedDateTime = todatetime(record.properties.['createdDateTime']),
CrossTenantAccessType = record.properties.['crossTenantAccessType'],
DeviceDetail = record.properties.['deviceDetail'],
HomeTenantId = record.properties['homeTenantId'],
Id = record.properties.['id'],
IPAddress = record.callerIpAddress,
IsInteractive = record.properties.['isInteractive'],
LocationDetails,
MfaDetail = record.properties.['mfaDetail'],
NetworkLocationDetails = record.properties.['networkLocationDetails'],
OriginalRequestId = record.properties.['originalRequestId'],
ProcessingTimeInMs = record.properties.['processingTimeInMilliseconds'],
ResourceDisplayName = record.properties.['resourceDisplayName'],
ResourceIdentity = record.properties.['resourceId'],
ResourceServicePrincipalId = record.properties.['resourceServicePrincipalId'],
ResourceTenantId = record.properties.['resourceTenantId'],
RiskDetail = record.properties.['riskDetail'],
RiskEventTypes = record.properties.['riskEventTypes'],
RiskLevelAggregated = record.properties.['riskLevelAggregated'],
RiskState = record.properties.['riskState'],
SessionLifetimePolicies = record.properties.['sessionLifetimePolicies'],
Status = record.properties.['status'],
TokenIssuerType = record.properties.['tokenIssuerType'],
UniqueTokenIdentifier = record.properties.['uniqueTokenIdentifier'],
UserAgent = record.properties.['userAgent'],
UserDisplayName = record.properties.['userDisplayName'],
UserId = record.properties.['userId'],
UserPrincipalName = record.properties.['userPrincipalName'],
UserType = record.properties.['userType'],
Type = 'AADNonInteractiveUserSignInLogs'
Once we have the new query we can use the .create-or-alter function
command to create a new Azure Data Explorer function. This command can also be used to update an existing query.
.create-or-alter function AADNonInteractiveUserSignInLogsParsing() {
AADLogs
| mv-expand record = data.records
| where tostring(record.category) == 'NonInteractiveUserSignInLogs'
| project
...
...
}
After running the control command we should see the new function being created in the Functions
table. Now we can re-use this function:
AADNonInteractiveUserSignInLogsParsing
| take 1
Next step is to create the table where the new logs are going to be written to. For this we can use the .set
control command:
.set AADNonInteractiveUserSignInLogs <|
AADNonInteractiveUserSignInLogsParsing()
| take 0
The | take 0
suffix is meant to make sure the command actually appends no records to the target table. We just need to create the schema for this new table.
Last set is to change the table update policy, here we define which query is going to be used to update the table and the source table. For this we can use the .alter table
policy update command:
.alter table AADNonInteractiveUserSignInLogs policy update
```
[
{
"IsEnabled": true,
"Source": "MyAADLogs",
"Query": "AADNonInteractiveUserSignInLogsParsing",
"IsTransactional": true,
"PropagateIngestionProperties": false
}
]
```