Query Details

Summary Of First Party Service Principals Without Tenant Specific Data

Query

// Overview of all First Party Apps enriched with Sign-in events, activities in Microsoft Graph and Entra ID Audit Logs and enriched with WorkloadIdentityInfo
// Exclude Tenant specific values such as Correlation ID, IP Addresses for comparing
// Include also Microsoft apps without AppId from the Audit Logs (AadAuditActivityByUnknown)
// Requires AuditLogs, MicrosoftGraphActivityLogs and deployment of WorkloadIdentityInfo
// More details on deploying WorkloadIdentityInfo: https://www.cloud-architekt.net/entra-workload-id-advanced-detection-enrichment/#publish-watchlist-workloadidentityinfo-with-sentinelenrichment
let Lookback = 90d;
// Get list of TenantIds and Classified First Party Apps from WorkloadIdentityInfo
let FirstPartyAppOwnerTenantId = dynamic(['f8cdef31-a31e-4b4a-93e4-5f571e91255a', '72f988bf-86f1-41af-91ab-2d7cd011db47']);
let FirstPartyApps = _GetWatchlist('WorkloadIdentityInfo')
    | where IsFirstPartyApp == "true" or AppOwnerTenantId in~ (FirstPartyAppOwnerTenantId) or AppDisplayName contains "entraops"
    | extend Identity = tostring(ServicePrincipalObjectId)
    | extend AppId = tostring(AppId);
// Get list of signins from First Party Apps
let SignInEvents = FirstPartyApps 
    | join kind=inner (
        AADServicePrincipalSignInLogs
        | where TimeGenerated >ago(Lookback)
    ) on AppId
    | summarize UniqueTokenIdentifiers = make_set(UniqueTokenIdentifier), Locations = make_set(Location), Application = make_set(AppDisplayName), Resource = make_set(ResourceDisplayName) by AppId;
// Get list of Graph Activity from 1st Party Apps
let GraphActivity = FirstPartyApps
| join kind=inner (
MicrosoftGraphActivityLogs
    | where TimeGenerated > ago(Lookback)
    // Filter out GET operations
    | where RequestMethod != "GET"
    | extend Roles = split(Roles, ' ')
    | extend Identity = ServicePrincipalId
    | extend ParsedUri = parse_url(RequestUri)
    | extend NormalizedRequestUri = tostring(ParsedUri.Path)
    | extend NormalizedRequestUri = replace_string(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>')
    | extend Operations = bag_pack_columns(
                        RequestMethod,
                        NormalizedRequestUri
                    )
    | summarize
        GraphOperations = make_set(Operations)
        by Identity
    ) on Identity
| project AppDisplayName, AppId, AppOwnerTenantId, VerifiedPublisher, GraphOperations, CreatedDateTime, AssignedRoles;
// Get list of empty initator in Microsoft Entra Audit log which are identifier for other backend jobs
let AadAuditActivityByUnknown = AuditLogs
    | where TimeGenerated >ago(Lookback) and InitiatedBy == "{}"
    | extend OperationId = Id
    | extend AppDisplayName = Identity
    | extend AadOperation = bag_pack_columns(
        ActivityDisplayName,
        OperationName
      )
    | summarize AadOperations = make_set( AadOperation ) by AppDisplayName
    | extend OperationsActivity = iff(isnotempty(AadOperations), true, false);
// Get list of Microsoft Entra Audit log from 1st Party Apps
let AadAuditActivity = _GetWatchlist('WorkloadIdentityInfo')
    | where IsFirstPartyApp == "true" or AppOwnerTenantId in~ (FirstPartyAppOwnerTenantId)
    | extend Identity = tostring(AppDisplayName)
    | join kind=inner ( AuditLogs
        | extend OperationId = Id
        | where TimeGenerated >ago(Lookback)
    ) on Identity
    | extend AadOperation = bag_pack_columns(
        ActivityDisplayName,
        OperationName
      )
    | summarize AadOperations = make_set( AadOperation ) by tostring(AppId);
// Get list of operations to issue credential on 1st Party Apps
let CredentialOperations = AuditLogs
    | where TimeGenerated >ago(Lookback)
    // Captures "Add service principal", "Add service principal credentials", and "Update application - Certificates and secrets management" events
    | where OperationName has_any ("Add service principal", "Certificates and secrets management", "Update application")
    | where Result =~ "success"
    | mv-apply TargetResource = TargetResources on 
        (
        where TargetResource.type =~ "Application" or TargetResource.type =~ "ServicePrincipal"
        | extend
            TargetName = tostring(TargetResource.displayName),
            ResourceId = tostring(TargetResource.id),
            WorkloadIdentityObjectType = tostring(TargetResource.type),
            keyEvents = TargetResource.modifiedProperties
        )
    | mv-apply Property = keyEvents on 
        (
        where Property.displayName =~ "KeyDescription" or Property.displayName =~ "FederatedIdentityCredentials"
        | extend
            new_value_set = parse_json(tostring(Property.newValue)),
            old_value_set = parse_json(tostring(Property.oldValue))
        )
    | extend diff = set_difference(new_value_set, old_value_set)
    | where isnotempty(diff)
    | parse diff with * "KeyIdentifier=" keyIdentifier: string ",KeyType=" keyType: string ",KeyUsage=" keyUsage: string ",DisplayName=" keyDisplayName: string "]" *
    | where keyUsage =~ "Verify" or isnotempty(parse_json(tostring(diff[0].Audiences))[0])
    | mv-apply AdditionalDetail = AdditionalDetails on 
        (
        where AdditionalDetail.key =~ "User-Agent"
        | extend UserAgent = tostring(AdditionalDetail.value)
        )
    | mv-apply AdditionalDetail = AdditionalDetails on 
        (
        where AdditionalDetail.key =~ "AppId"
        | extend AppId = tostring(AdditionalDetail.value)
        )
    | join kind=inner ( FirstPartyApps ) on AppId
    | extend CredentialName = iff(isnotempty(keyDisplayName), keyDisplayName, diff[0].Name)
    | extend CredentialIdentifier = iff(isnotempty(keyIdentifier), keyIdentifier, diff[0].Subject)
    | extend CredentialType = iff(isnotempty(keyType), keyType, keyEvents[0].displayName)
    | extend CredentialUsage = iff(isnotempty(keyUsage), keyUsage, tostring(diff[0].Audiences))
    | extend CredentialOperation = bag_pack_columns(
        TimeGenerated,
        OperationName,
        CredentialName,
        CredentialType,
        CredentialUsage,
        UserAgent
        )
    | summarize CredentialOperations = make_set(CredentialOperation) by AppId;
// Merge data from different queries of known Service Principals
let KnownServicePrincipals = FirstPartyApps
    | join kind=leftouter ( SignInEvents ) on AppId
    | join kind=leftouter ( AadAuditActivity ) on AppId
    | join kind=leftouter ( GraphActivity ) on AppId
    | join kind=leftouter ( CredentialOperations ) on AppId
    | extend AddedCredential = iff(isnotempty(CredentialOperations), true, false)
    | extend SignInActivity = iff(isnotempty(UniqueTokenIdentifiers), true, false)
    | extend OperationsActivity = iff(isnotempty(GraphOperations) or isnotempty(AadOperations), true, false)
    | project AppId, AppDisplayName, AppOwnerTenantId, CreatedDateTime, SignInActivity, OperationsActivity, AddedCredential, VerifiedPublisher, Locations, Application, Resource, GraphOperations, AadOperations, CredentialOperations;
union KnownServicePrincipals, AadAuditActivityByUnknown
    | sort by tostring(AppDisplayName) asc

Explanation

This KQL query provides a comprehensive overview of First Party Apps by combining data from various sources such as Sign-in events, Microsoft Graph activities, and Entra ID Audit Logs. Here's a simplified breakdown:

  1. Lookback Period: The query looks back over the last 90 days.

  2. First Party Apps Identification:

    • It retrieves a list of First Party Apps from a watchlist named WorkloadIdentityInfo.
    • It filters apps based on specific criteria (e.g., IsFirstPartyApp is true, or the app belongs to certain tenant IDs).
  3. Sign-In Events:

    • It joins the list of First Party Apps with Azure Active Directory (AAD) Service Principal Sign-In Logs.
    • It summarizes unique token identifiers, locations, applications, and resources associated with each app.
  4. Microsoft Graph Activity:

    • It joins the list of First Party Apps with Microsoft Graph Activity Logs.
    • It filters out GET operations and normalizes request URIs.
    • It summarizes the operations performed by each app.
  5. Entra ID Audit Logs:

    • It identifies audit log entries with empty initiators, which indicate backend jobs.
    • It summarizes operations from these logs.
  6. Credential Operations:

    • It identifies operations related to adding or updating credentials for First Party Apps.
    • It extracts details such as key identifiers, types, usage, and user agents.
  7. Combining Data:

    • It merges data from the above queries to create a comprehensive view of known service principals.
    • It includes details like sign-in activity, operations activity, added credentials, and more.
  8. Union with Unknown Activities:

    • It combines the known service principals with audit activities that have unknown initiators.
    • The final result is sorted by the app display name.

In summary, this query aggregates and enriches data from multiple sources to provide a detailed view of First Party Apps' activities, including sign-ins, operations, and credential changes, while excluding tenant-specific values for comparison purposes.

Details

Thomas Naunheim profile picture

Thomas Naunheim

Released: August 9, 2024

Tables

AADServicePrincipalSignInLogsMicrosoftGraphActivityLogsAuditLogsKnownServicePrincipalsAadAuditActivityByUnknown

Keywords

AuditLogsMicrosoftGraphActivityLogsWorkloadIdentityInfoAADServicePrincipalSignInLogsFirstPartyAppsSignInEventsGraphActivityAadAuditActivityByUnknownAadAuditActivityCredentialOperationsKnownServicePrincipals

Operators

letdynamic_GetWatchlistwhereorin~containsextendtostringjoinkindinneronsummarizemake_setbyprojectagosplitparse_urlreplace_stringreplace_regexbag_pack_columnsiffisnotemptymv-applyparseset_differencehas_any=~sortunionasc.

Actions