Query Details

Enriched Microsoft Graph Activity

Query

id: 8bbdc1c2-9f3d-4ac1-9aae-ae7cf6d1f9bc
Function:
    Title: "Function to get enrichment for GraphAPIAuditEvents with critical assets from Exposure Management, EntraOps Graph API permissions and action classification."
    Version: "1.1.0"
    LastUpdated: "2025-07-30"
Category: Microsoft Defender XDR Function
FunctionName: EnrichedMicrosoftGraphActivity
FunctionAlias: EnrichedMicrosoftGraphActivity
FunctionQuery: |
    let EnrichedMicrosoftGraphActivity = (CallerObjectId:string="", CallerIpAddress:string="", ScopeEamClassification:string="", GraphRequestId:string="", UniqueTokenId:string="") {
        let XspmCriticalAadObjectIds = ExposureGraphNodes
        | mv-expand EntityIds
        | extend EntityType = parse_json(EntityIds)
        | where EntityType["type"] == "AadObjectId"
        | mv-expand CriticalityData = parse_json(NodeProperties)["rawData"]["criticalityLevel"]["ruleNames"]
        | extend CriticalityLevel = toint(parse_json(NodeProperties)["rawData"]["criticalityLevel"]["criticalityLevel"])
        | extend RuleName = tostring(CriticalityData)
        | extend ObjectId = iff(EntityType["type"] == "AadObjectId", tolower(tostring(extract("objectid=([\\w-]+)", 1, tostring(parse_json(EntityIds)["id"])))), tolower(tostring(EntityType["id"])))
        | where isnotempty(CriticalityLevel)
        | extend CriticalAssetDetail = bag_pack_columns(CriticalityLevel, RuleName)
        // Summarize criticality and asset details by object and node information
        | summarize 
            CriticalityLevel = min(CriticalityLevel), 
            CriticalAssetDetails = array_sort_asc(make_set(CriticalAssetDetail)) 
        by 
            ObjectId, 
            ObjectIdType = tostring(EntityType["type"]), 
            NodeId, 
            NodeName
        ;
        // Minimal Value of Critical Asset which will be classify as sensitive caller or target (default: 2 or lower)
        let MinCriticalAssetValue = 2;
        let SensitiveMsGraphScopes = externaldata(ScopeDisplayName: string, ScopeId: string, AppId: string, EAMTierLevelName: string, Category: string)
            ["https://raw.githubusercontent.com/Cloud-Architekt/AzurePrivilegedIAM/refs/heads/main/Classification/Classification_MsGraphScopes.json"] with(format='multijson');
        let ControlPlaneScopes = SensitiveMsGraphScopes | where EAMTierLevelName == "ControlPlane" | summarize by ScopeDisplayName;
        let ManagementPlaneScopes = SensitiveMsGraphScopes | where EAMTierLevelName == "ManagementPlane" | summarize by ScopeDisplayName;
        let PrivilegedGraphOperationsUri = (externaldata(Uri:string)
            [@"https://raw.githubusercontent.com/Cloud-Architekt/AzurePrivilegedIAM/refs/heads/main/PrivilegedOperations/GraphApiRequest.csv"] with (format="csv", ignoreFirstRecord=true)
        );
        let PrivilegedGraphOperationsExcludeMatches = @"(getMember|checkMember|[0-9a-fA-F-]{36}/(threads|drive|messages))";
        let PrivilegedGraphOperations = dynamic([
            'PATCH',
            'POST',
            'DELETE'
        ]);
        GraphAPIAuditEvents
        | where (CallerObjectId == "" or AccountObjectId == CallerObjectId)
        | where (CallerIpAddress == "" or IpAddress == CallerIpAddress)    
        | where (GraphRequestId == "" or ClientRequestId == GraphRequestId)
        | where (UniqueTokenId == "" or UniqueTokenIdentifier == UniqueTokenId)
        // Build schema similiar to MicrosoftGraphActivityLogs
        | extend UserId = iff(EntityType == "user",AccountObjectId,"")
        | extend ServicePrincipalId = iff(EntityType == "app",AccountObjectId,"")
        | extend ResponseStatusCode = toint(ResponseStatusCode)
        | extend RequestDuration = toint(RequestDuration)
        | extend SignInActivityId = tostring(UniqueTokenIdentifier)
        | extend RequestId = OperationId
        | project-rename AppId = ApplicationId, DurationMs = RequestDuration, IPAddress = IpAddress
        // Remove columns which exists in GraphAPIAuditEvents but does not exists MicrosoftGraphActivityLogs
        //| project-away AccountObjectId, EntityType, ReportId, UniqueTokenIdentifier, IdentityProvider
        // Sort columns in similar order than MicrosoftGraphActivityLogs
        | project-reorder 
            TimeGenerated, 
            Location, 
            RequestId, 
            OperationId, 
            ClientRequestId, 
            ApiVersion, 
            RequestMethod, 
            ResponseStatusCode, 
            IPAddress, 
            RequestUri, 
            DurationMs, 
            SignInActivityId, 
            AppId, 
            UserId, 
            ServicePrincipalId, 
            Scopes, 
            Type
        // Parsing Uri for Enrichment (by Fabian Bader: https://cloudbrothers.info/en/detect-threats-microsoft-graph-logs-part-1/)
        | extend ParsedUri = parse_url(RequestUri)
        | extend NormalizedRequestUri = tostring(ParsedUri.Path)
        | extend NormalizedRequestUri = replace_string(NormalizedRequestUri, '//', '/')
        | extend RequestTargetObjectId = extract(@'[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}', 0, NormalizedRequestUri)
        | extend NormalizedRequestUri = replace_regex(NormalizedRequestUri, @'[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}', @'<UUID>'), ParsedUri
        // Enrichment with Critical Asset Management
        | extend CallerObjectId = coalesce(UserId, ServicePrincipalId)
        | extend CallerEntityType = iff(isnotempty(UserId), "User", "ServicePrincipal")
        | join kind = leftouter ( XspmCriticalAadObjectIds | project CallerObjectId = ObjectId, CallerCriticalityLevel = CriticalityLevel, CallerCriticalAssetDetails = CriticalAssetDetails ) on CallerObjectId
        | join kind = leftouter ( XspmCriticalAadObjectIds | project RequestTargetObjectId = ObjectId, TargetCriticalityLevel = CriticalityLevel, TargetCriticalAssetDetails = CriticalAssetDetails ) on RequestTargetObjectId
        | extend IsSensitiveCaller = iff(CallerCriticalityLevel <(MinCriticalAssetValue), true, false)
        | extend IsSensitiveTarget = iff(TargetCriticalityLevel <(MinCriticalAssetValue), true, false)
        // Enrichment with EntraOps App Role Classification
        | extend ScopesArray = split(Scopes, ' ')
        | extend ScopeClassification = case(
            isempty(Scopes), "Unknown",
            ScopesArray has_any (ControlPlaneScopes), "ControlPlane",
            ScopesArray has_any (ManagementPlaneScopes), "ManagementPlane",
            "UserAccess"
        )
        | where (ScopeEamClassification == "" or ScopeClassification == ScopeEamClassification)
        | extend IsHighSensitiveScope = iff(ScopeClassification == "ControlPlane", true, false)
        | extend IsSensitiveAction = iff((
            NormalizedRequestUri has_any(PrivilegedGraphOperationsUri)
            and not(RequestUri matches regex "(getMemberGroups|checkMemberGroups|checkMemberObjects|getMemberObjects|estimateAccess|checkAccess)$")
            and not(NormalizedRequestUri matches regex "/(drive|messages|threads|teamwork|onlinemeetings|onlineMeetings|events)")
            and RequestMethod in~ (PrivilegedGraphOperations)) == true, true, false
        )
        | project-away ParsedUri, NormalizedRequestUri, RequestTargetObjectId, CallerObjectId1, RequestTargetObjectId, RequestTargetObjectId1, ScopesArray
        | project-reorder Timestamp, Type, ClientRequestId, OperationId, CallerObjectId, CallerEntityType, CallerCriticalityLevel, CallerCriticalAssetDetails, IsSensitiveCaller, Scopes, ScopeClassification, IsHighSensitiveScope, RequestUri, RequestMethod, ResponseStatusCode, IsSensitiveAction, IsSensitiveTarget, TargetCriticalityLevel, TargetCriticalAssetDetails
    };
    EnrichedMicrosoftGraphActivity(CallerObjectId,CallerIpAddress,ScopeEamClassification,GraphRequestId,UniqueTokenId)

Explanation

This KQL query is a function designed to enrich Microsoft Graph API audit events with additional information about critical assets, permissions, and action classifications. Here's a simplified breakdown of what it does:

  1. Input Parameters: The function takes several optional parameters, such as CallerObjectId, CallerIpAddress, ScopeEamClassification, GraphRequestId, and UniqueTokenId. These can be used to filter the audit events.

  2. Critical Asset Identification: It identifies critical Azure Active Directory (AAD) objects by expanding and parsing data from ExposureGraphNodes. It determines the criticality level and associated rule names for these objects.

  3. Sensitive Scope Classification: The function retrieves sensitive Microsoft Graph API scopes from external JSON data, categorizing them into "ControlPlane" and "ManagementPlane" scopes.

  4. Privileged Operations: It identifies privileged operations by checking against a list of specific HTTP methods (PATCH, POST, DELETE) and excluding certain patterns.

  5. Audit Event Filtering: The function filters GraphAPIAuditEvents based on the input parameters and enriches them with additional details like user ID, service principal ID, response status code, and request duration.

  6. URI Parsing and Normalization: It parses and normalizes the request URI to extract target object IDs and replace UUIDs with a placeholder.

  7. Enrichment with Critical Asset Management: The function joins the audit events with critical asset data to determine if the caller or target is sensitive based on a minimum criticality level.

  8. Scope and Action Classification: It classifies the scope of the API call and determines if it's high-sensitive or if the action is sensitive based on predefined criteria.

  9. Output: The function projects and reorders the columns to provide a structured output, highlighting key details like caller information, criticality levels, scope classification, and sensitivity of actions and targets.

Overall, this query is used to enhance the visibility and analysis of Microsoft Graph API audit events by integrating critical asset information and classifying actions and permissions.

Details

Thomas Naunheim profile picture

Thomas Naunheim

Released: June 11, 2026

Tables

ExposureGraphNodesGraphAPIAuditEvents

Keywords

DevicesExposureManagementEntraOpsGraphAPIPermissionsActionClassificationMicrosoftGraphActivityLogsUserServicePrincipalCriticalAssetManagementScopesControlPlaneManagementPlaneSensitiveAction

Operators

letmv-expandextendparse_jsonwhereisnotemptyifftolowertostringextractbag_pack_columnssummarizeminarray_sort_ascmake_setbyexternaldatawithformatproject-renametointproject-reorderparse_urlreplace_stringreplace_regexcoalescejoinkindonsplitcaseisemptyhas_anymatchesregexin~project-away

Actions