Query Details

Analytics Azure RBAC Role Assignments

Query

// This query can help you to detect Azure RBAC role assignments.
//
// Click "Save as function", in Parameters write in the fields:
// "timespan" "query_frequency" "1d"
//
// If you name the function "AzureRBACRoleAssignments", you can check the function with queries like the following:
//
// AzureRBACRoleAssignments()
//
// AzureRBACRoleAssignments(1h)
//
//let query_frequency = 1d;
//let Function = (query_frequency:timespan = 1d){
let query_period = 14d;
let _PIM_ids = toscalar(
    _GetWatchlist("Activity-ExpectedSignificantActivity")
    | where Activity == "PrivilegedIdentityManagement" and Notes has "[App]"
    | summarize make_list(ActorId)
);
let _PrivAzureRBACRoles = toscalar(
    _GetWatchlist("RegEx-PrivAzureRBACRoles")
    | summarize RegEx = make_list(RegEx)
    | extend RegEx = strcat(@'^(', strcat_array(RegEx, '|'), @')$')
);
let _Users =
    IdentityInfo
    | where TimeGenerated > ago(query_period)
    | summarize arg_max(TimeGenerated, *) by AccountObjectId
    | project-away TimeGenerated
;
let _AADApps = _GetWatchlist("UUID-AADApps");
let _Groups = _GetWatchlist("UUID-AADGroups");
let _AzureSubscriptions = _GetWatchlist("UUID-AzureSubscriptions");
let _AzureRBACRoles = _GetWatchlist("UUID-AzureRBACRoles");
let _ResourceManagementPIM = materialize(
    AuditLogs
    | where TimeGenerated > ago(query_frequency)
    | where Category == "ResourceManagement" and LoggedByService == "PIM" and OperationName has "member" //and AADOperationType matches regex "Role" and not(AADOperationType == "CreateRoleUpdateRequest")
    // Microsoft performed changes on 11/04 to "AADOperationType" and "Id" columns format, and changed type "managementgroup" to "Other" and the available information worsened... TargetManagementGroup columns will be empty
    | mv-apply AdditionalDetail = AdditionalDetails on (
        summarize ParsedAdditionalDetails = make_bag(pack(tostring(AdditionalDetail["key"]), tostring(AdditionalDetail["value"])))
    )
    // | mv-apply TargetResource = TargetResources on (
    //     summarize TargetResourcesTypes = make_list(tostring(TargetResource["type"]))
    // )
    | mv-apply TargetResource = TargetResources on (
        summarize TargetResource = make_list(TargetResource) by TargetResourceType = tostring(TargetResource["type"])
        | extend TargetResource = iff(array_length(TargetResource) == 1, TargetResource[0], TargetResource)
        | summarize ParsedTargetResources = make_bag(pack(TargetResourceType, TargetResource))
    )
    | project
        TimeGenerated,
        LoggedByService,
        Category,
        AADOperationType,
        OperationName,
        Result,
        ResultReason,
        ActivateOperation = OperationName has_any ("activation", "deactivate"),
        EligibleOperation = OperationName has "eligible",
        RemoveOperation = OperationName has "remove",
        PermanentOperation = OperationName has "permanent",
        // Information about the actor
        ActorIdentity = Identity,
        // Information about the actor, if it was a user
        ActorUserId = tostring(InitiatedBy["user"]["id"]),
        ActorUserPrincipalName = tolower(tostring(InitiatedBy["user"]["userPrincipalName"])),
        ActorUserIPAddress = tostring(InitiatedBy["user"]["ipAddress"]),
        ActorUserRoles = tostring(InitiatedBy["user"]["roles"]),
        // Information about the actor, if it was an application
        ActorAppName = tostring(InitiatedBy["app"]["displayName"]),
        ActorAppId = tostring(InitiatedBy["app"]["appId"]),
        ActorAppServicePrincipalName = tostring(InitiatedBy["app"]["servicePrincipalName"]),
        ActorAppServicePrincipalId = tostring(InitiatedBy["app"]["servicePrincipalId"]),
        // Information about the role
        RoleProvider = tostring(ParsedTargetResources["Provider"]["displayName"]),
        //RoleDefinitionOriginType = tostring(ParsedAdditionalDetails["RoleDefinitionOriginType"]),
        TargetRoleName = tostring(TargetResources[0]["displayName"]),
        TargetRoleTemplateId = tostring(ParsedAdditionalDetails["TemplateId"]),
        // Information about the targets
        TargetManagementGroupId = tostring(ParsedTargetResources["managementgroup"]["id"]),
        TargetManagementGroupName = tostring(ParsedTargetResources["managementgroup"]["displayName"]),
        TargetSubscriptionId = extract(@"\/subscriptions\/([^\/]+)\/", 1, tostring(ParsedAdditionalDetails["RoleDefinitionOriginId"])),
        TargetSubscriptionName = tostring(ParsedTargetResources["subscription"]["displayName"]),
        TargetGroupName = tostring(ParsedTargetResources["Group"]["displayName"]),
        TargetGroupId = tostring(ParsedTargetResources["Group"]["id"]),
        TargetUserPrincipalName = tostring(ParsedTargetResources["User"]["userPrincipalName"]),
        TargetUserId = tostring(ParsedTargetResources["User"]["id"]),
        // JSONs containers
        InitiatedBy,
        AdditionalDetails,
        TargetResources,
        RequestId = iff(isempty(array_length(ParsedTargetResources["Request"])), tostring(ParsedTargetResources["Request"]["id"]), tostring(ParsedTargetResources["Request"][0]["id"])),
        RoleScheduleId = iff(isempty(array_length(ParsedTargetResources["Request"])), tostring(ParsedTargetResources["RoleSchedule"]["id"]), tostring(ParsedTargetResources["Request"][1]["id"])),
        CorrelationId,
        Type
    | extend
        Caller = coalesce(ActorUserPrincipalName, ActorUserId, ActorAppServicePrincipalId),
        CallerDisplayName = ActorIdentity,
        CallerIpAddress = coalesce(ActorUserIPAddress, ""),
        TargetId = coalesce(TargetUserId, TargetGroupId),
        Target = coalesce(TargetUserPrincipalName, TargetGroupName),
        SourceSystem = LoggedByService
    | summarize
        StartTime = min(TimeGenerated),
        EndTime = max(TimeGenerated),
        RequestedTimeGenerated = make_list_if(TimeGenerated, OperationName has "requested"),
        CompletedTimeGenerated = make_list_if(TimeGenerated, not(OperationName has "requested") and Result == "success"),
        RequestedCount = countif(OperationName has "requested"),
        CompletedCount = countif(not(OperationName has "requested") and Result == "success"),
        arg_max(TimeGenerated, *)
        by CorrelationId, EligibleOperation, RemoveOperation, PermanentOperation, TargetManagementGroupId, TargetSubscriptionId, TargetRoleTemplateId, TargetGroupName, TargetUserPrincipalName, TargetUserId
    | extend
        RequestedTimeGenerated = iff(array_length(RequestedTimeGenerated) == 0, dynamic(null), RequestedTimeGenerated),
        RequestedCount = iff(RequestedCount == 0, int(null), RequestedCount)
);
let _RoleAssignmentsAzure = materialize(
    AzureActivity
    | where TimeGenerated > ago(query_frequency)
    | where ResourceProviderValue =~ "Microsoft.Authorization" and OperationNameValue has_any ("roleAssignments")
    | extend PreferenceInteger = case(
        ResourceProviderValue == "Microsoft.Authorization", 1,
        ResourceProviderValue == "MICROSOFT.AUTHORIZATION", 0,
        -1
        )
    // Group together Start, Accept, Success... operations
    | summarize hint.shufflekey=CorrelationId
        StartTime = min(TimeGenerated),
        EndTime = max(TimeGenerated),
        PropertiesDynamic = make_bag(pack(ActivityStatusValue, iff(PreferenceInteger == 1, todynamic(Properties), Properties_d))),
        EventDataId = array_sort_asc(make_list(EventDataId)),
        arg_max(TimeGenerated, *)
        by CorrelationId, ResourceProviderValue, OperationNameValue, _ResourceId
    | project-away EventDataId1
    // Group together two kinds of logs (where ResourceProviderValue is all caps or title - MICROSOFT.AUTHORIZATION or Microsoft.Authorization)
    | summarize hint.shufflekey=CorrelationId
        PropertiesDynamic = make_bag(pack(ResourceProviderValue, PropertiesDynamic)),
        EventDataId = make_bag(pack(ResourceProviderValue, EventDataId)),
        take_any(TenantId, SourceSystem, CategoryValue, SubscriptionId, Type),
        arg_min(PreferenceInteger, CallerIpAddress, Authorization, Authorization_d, Claims_d, Properties_d, EventSubmissionTimestamp, Hierarchy),
        arg_max(PreferenceInteger, Level, OperationNameValue, Caller, HTTPRequest, OperationId, ResourceGroup, ResourceProviderValue, ActivityStatusValue, ActivitySubstatusValue, OperationName, ActivityStatus, ActivitySubstatus, Category, ResourceId, ResourceProvider, Resource)
        by CorrelationId, TimeGenerated, StartTime, EndTime, _ResourceId
    | project-away PreferenceInteger*
    // Add origin events of operations (if a resource group is deleted, it might trigger a role assignment delete operation)
    | join kind=leftouter (
        AzureActivity
        | where TimeGenerated > ago(query_frequency)
        | where OperationNameValue has "/delete" and not(OperationNameValue has_any ("roleAssignments")) and ActivityStatusValue in ("Start", "Started")
        | summarize take_any(*) by EventDataId
        | extend StartOperationNameValue = coalesce(tostring(Properties_d["message"]), OperationNameValue), Start_HTTPRequest = HTTPRequest
        | summarize arg_min(TimeGenerated, StartOperationNameValue, Start_HTTPRequest) by CorrelationId, Auxiliar = tolower(StartOperationNameValue)
        | project-away TimeGenerated, Auxiliar
    ) on CorrelationId
    | project-away CorrelationId1
    | join kind=leftouter (
        AzureActivity
        | where TimeGenerated > ago(query_frequency)
        | where OperationNameValue has "/delete" and not(OperationNameValue has_any ("roleAssignments")) and not(ActivityStatusValue in ("Start", "Started"))
        | summarize take_any(*) by EventDataId
        | extend GroupOperationNameValue = coalesce(tostring(Properties_d["message"]), OperationNameValue), Group_ResourceId = _ResourceId, Group_HTTPRequest = HTTPRequest
        | summarize arg_min(TimeGenerated, GroupOperationNameValue, Group_HTTPRequest) by CorrelationId, Auxiliar = tolower(GroupOperationNameValue), Group_ResourceId
        | project-away TimeGenerated, Auxiliar
    ) on CorrelationId
    | project-away CorrelationId1
    | where not(isnotempty(GroupOperationNameValue) and (not(_ResourceId contains Group_ResourceId) or _ResourceId == Group_ResourceId))
    | extend OriginOperationNameValue = coalesce(StartOperationNameValue, GroupOperationNameValue, tostring(PropertiesDynamic["MICROSOFT.AUTHORIZATION"]["Start"]["message"]), tostring(PropertiesDynamic["Microsoft.Authorization"]["Start"]["message"]))
    | extend OriginOperationNameValue = iff(OperationNameValue =~ OriginOperationNameValue, "", OriginOperationNameValue)
    | project-away StartOperationNameValue, GroupOperationNameValue, Group_ResourceId
    // Start coalescing fields
    | extend
        Caller = tolower(Caller),
        AllCapsPropertiesDynamic = coalesce(PropertiesDynamic["MICROSOFT.AUTHORIZATION"]["Start"], PropertiesDynamic["MICROSOFT.AUTHORIZATION"]["Success"]),
        TitlePropertiesDynamic = coalesce(PropertiesDynamic["Microsoft.Authorization"]["Started"], PropertiesDynamic["Microsoft.Authorization"]["Succeeded"])
    | extend
        ResourceId = coalesce(ResourceId, tostring(AllCapsPropertiesDynamic["entity"])),
        Category = coalesce(Category, tostring(AllCapsPropertiesDynamic["eventCategory"])),
        OperationNameValue = coalesce(tostring(AllCapsPropertiesDynamic["message"]), OperationNameValue),
        ResourceProvider = coalesce(ResourceProvider, tostring(split(tostring(AllCapsPropertiesDynamic["message"]), "/")[0])),
        Resource = coalesce(
            Resource,
            replace_regex(tostring(AllCapsPropertiesDynamic["resource"]), @"([a-f0-9]{8})([a-f0-9]{4})([a-f0-9]{4})([a-f0-9]{4})([a-f0-9]{12})", @"\1-\2-\3-\4-\5")
            ),
        TargetResourceGroup = coalesce(tostring(split(split(tostring(AllCapsPropertiesDynamic["entity"]), "/resourceGroups/")[1], "/")[0]), ResourceGroup),
        HTTPRequest = coalesce(HTTPRequest, tostring(AllCapsPropertiesDynamic["httpRequest"]), Start_HTTPRequest, Group_HTTPRequest),
        Hierarchy = coalesce(Hierarchy, tostring(TitlePropertiesDynamic["hierarchy"])),
        AppId = tostring(Claims_d["appid"]),
        CallerType = case(
            tostring(Claims_d["http://schemas.microsoft.com/identity/claims/scope"]) == "user_impersonation", "User",
            tostring(Claims_d["idtyp"]) == "app", "App",
            ""
            ),
        CallerRole = tostring(todynamic(Authorization)["evidence"]["role"]),
        CallerRoleType = tostring(todynamic(Authorization)["evidence"]["principalType"]),
        Body = todynamic(dynamic_to_json(coalesce(
            PropertiesDynamic["MICROSOFT.AUTHORIZATION"]["Start"]["requestbody"],
            PropertiesDynamic["Microsoft.Authorization"]["Started"]["requestbody"],
            PropertiesDynamic["MICROSOFT.AUTHORIZATION"]["Success"]["responseBody"],
            PropertiesDynamic["Microsoft.Authorization"]["Succeeded"]["responseBody"]
            ))),
        CoalescedProperties = coalesce(
            PropertiesDynamic["MICROSOFT.AUTHORIZATION"]["Success"],
            PropertiesDynamic["Microsoft.Authorization"]["Succeeded"]
        )
    | extend
    //     BodyKeys = bag_keys(Body),
    //     PropertiesKeys = bag_keys(CoalescedProperties),
        CallerIpAddress = coalesce(CallerIpAddress, tostring(todynamic(HTTPRequest)["clientIpAddress"])),
        BodyPropertiesKeys = bag_keys(coalesce(Body["properties"], Body["Properties"]))
    | extend
        TargetId = coalesce(
            tostring(Body["properties"]["principalId"]), 
            tostring(Body["Properties"]["PrincipalId"]),
            replace_regex(tostring(CoalescedProperties["principalId"]), @"([a-f0-9]{8})([a-f0-9]{4})([a-f0-9]{4})([a-f0-9]{4})([a-f0-9]{12})", @"\1-\2-\3-\4-\5")
            ),
        TargetRoleDefinitionId = coalesce(tostring(Body["properties"]["roleDefinitionId"]), tostring(Body["Properties"]["RoleDefinitionId"])),
        TargetType = coalesce(tostring(Body["properties"]["principalType"]), tostring(Body["Properties"]["PrincipalType"]), iff(BodyPropertiesKeys has "EmailAddress", "User", ""))//,
        // TargetCondition = coalesce(tostring(Body["Properties"]["Condition"]), ""),
        // TargetConditionVersion = coalesce(tostring(Body["Properties"]["ConditionVersion"]), "")
    | extend
        TargetRoleTemplateId = coalesce(
            tostring(split(TargetRoleDefinitionId, "/roleDefinitions/")[1]),
            replace_regex(tostring(CoalescedProperties["roleDefinitionId"]), @"([a-f0-9]{8})([a-f0-9]{4})([a-f0-9]{4})([a-f0-9]{4})([a-f0-9]{12})", @"\1-\2-\3-\4-\5")
            ),
        RemoveOperation = case(
            OperationNameValue =~ "Microsoft.Authorization/roleAssignments/delete", true,
            OperationNameValue =~ "Microsoft.Authorization/roleAssignments/write", false,
            false
        )
    | project-away Body, CoalescedProperties, BodyPropertiesKeys, Start_HTTPRequest, Group_HTTPRequest, AllCapsPropertiesDynamic, TitlePropertiesDynamic//, TargetRoleDefinitionId
    | lookup kind=leftouter (
        union
            (
            _AADApps
            | project CallerType = "App", Caller = tostring(ObjectId), CallerDisplayName = AppDisplayName
            ),
            (
            AuditLogs
            | where TimeGenerated > ago(query_period)
            | where Category == "ApplicationManagement" and OperationName has "service principal" and not(AADOperationType in ("Assign", "Unassign"))
            | distinct Caller = tostring(TargetResources[0]["id"]), CallerDisplayName = tostring(TargetResources[0]["displayName"])//, TargetType = "ServicePrincipal"
            | extend CallerType = "App"
            )
            | where isnotempty(Caller)
        ) on Caller//, CallerType
    | extend CallerDisplayName = coalesce(CallerDisplayName, tostring(Claims_d["name"]))
    | extend PermanentOperation = iff(CallerDisplayName == "MS-PIM" and Caller in (_PIM_ids), bool(null), true)
    | lookup kind=leftouter (
        _AADApps
        | project AppId = tostring(AppId), AppDisplayName
        | where isnotempty(AppId)
        ) on AppId
    | project-rename TargetSubscriptionId = SubscriptionId
    | lookup kind=leftouter (
        _AzureSubscriptions
        | project TargetSubscriptionId = tostring(SubscriptionId), TargetSubscriptionName = tostring(SubscriptionName)
        ) on TargetSubscriptionId
    | lookup kind=leftouter (
        union
            (
            _AADApps
            | project TargetId = tostring(ObjectId), Target = tostring(AppDisplayName)//, TargetType = "ServicePrincipal"
            ),
            (
            AuditLogs
            | where TimeGenerated > ago(query_period)
            | where Category == "ApplicationManagement" and OperationName has "service principal" and not(AADOperationType in ("Assign", "Unassign"))
            | distinct TargetId = tostring(TargetResources[0]["id"]), Target = tostring(TargetResources[0]["displayName"])//, TargetType = "ServicePrincipal"
            ),
            (
            _Users
            | where isnotempty(AccountObjectId) and isnotempty(AccountUPN)
            | project TargetId = AccountObjectId, Target = tostring(AccountUPN)//, TargetType = "User"
            ),
            (
            _Groups
            | where isnotempty(ObjectId) and isnotempty(DisplayName)
            | project TargetId = tostring(ObjectId), Target = tostring(DisplayName)//, TargetType = "Group"
            )
        | where isnotempty(TargetId)
        | distinct *
        ) on TargetId//, TargetType
    | lookup kind=leftouter (
        _AzureRBACRoles
        | project TargetRoleTemplateId = tostring(RoleId), TargetRoleName = tostring(RoleName)
        | where isnotempty(TargetRoleTemplateId)
        ) on TargetRoleTemplateId
    | project
        TimeGenerated,
        StartTime,
        EndTime,
        ResourceProvider,
        Category,
        CategoryValue,
        ResourceProviderValue,
        Level,
        CallerIpAddress,
        CallerType,
        Caller,
        CallerDisplayName,
        CallerRole,
        CallerRoleType,
        AppId,
        AppDisplayName,
        OperationName,
        OperationNameValue,
        OriginOperationNameValue,
        ActivityStatusValue,
        //ActivityStatus,
        ActivitySubstatusValue,
        ActivitySubstatus,
        TargetSubscriptionId,
        TargetSubscriptionName,
        TargetResourceGroup,
        TargetType,
        TargetId,
        Target,
        TargetRoleTemplateId,
        TargetRoleName,
        //TargetRoleDefinitionId,
        // TargetCondition,
        // TargetConditionVersion,
        Authorization,
        PropertiesDynamic,
        HTTPRequest,
        Authorization_d,
        Properties_d,
        Claims_d,
        Resource,
        ResourceId,
        _ResourceId,
        Hierarchy,
        OperationId,
        CorrelationId,
        RemoveOperation,
        PermanentOperation,
        EventSubmissionTimestamp,
        EventDataId,
        //TenantId,
        SourceSystem,
        Type
);
union
    (
    union
        (
        _ResourceManagementPIM
        | where not(EligibleOperation)
        | lookup kind=leftouter (
            _RoleAssignmentsAzure
            | where CallerDisplayName == "MS-PIM" and Caller in (_PIM_ids) and isnotempty(TargetSubscriptionId) and isnotempty(TargetRoleTemplateId) and isnotempty(TargetId) and isnotempty(RemoveOperation)
            | project-rename Azure_EndTime = EndTime, AzureActivity_CorrelationId = CorrelationId, Azure_CallerIpAddress = CallerIpAddress
            | project-away TimeGenerated, StartTime, Category, Caller, CallerDisplayName, OperationName, TargetSubscriptionName, TargetRoleName, Target, SourceSystem, Type, PermanentOperation
            ) on TargetSubscriptionId, TargetRoleTemplateId, TargetId, RemoveOperation
        | where not(isnotempty(Azure_EndTime) and Azure_EndTime > EndTime)
        | summarize arg_max(Azure_EndTime, *) by TimeGenerated, CorrelationId, TargetSubscriptionId, TargetRoleTemplateId, TargetId, RemoveOperation
        | extend CallerIpAddress = coalesce(CallerIpAddress, Azure_CallerIpAddress)
        | project-away Azure_EndTime, Azure_CallerIpAddress
        ),
        (
        _ResourceManagementPIM
        | where EligibleOperation
        )
    | project-rename AuditLogs_CorrelationId = CorrelationId
    | project-away LoggedByService
    ),
    (
    _RoleAssignmentsAzure
    | where not(CallerDisplayName == "MS-PIM" and Caller in (_PIM_ids) and isnotempty(TargetSubscriptionId) and isnotempty(TargetRoleTemplateId) and isnotempty(TargetId)) and isnotempty(RemoveOperation)
    | project-rename AzureActivity_CorrelationId = CorrelationId
    )
| where not(SourceSystem == "PIM" and isnotempty(CompletedCount) and CompletedCount == 0)
| extend
    PrivilegedRole = isempty(TargetRoleName) or TargetRoleName matches regex _PrivAzureRBACRoles,
    WorkingTime = IsWorkingTime(TimeGenerated),
    AlertName = strcat(
        iff(RemoveOperation, "Remove", "Add"),
        " ",
        iff(PermanentOperation, "permanent", iff(not(ActivateOperation), "temporary", "activated")),
        " ",
        iff(EligibleOperation, "eligible ", ""),
        "member ",
        iff(RemoveOperation, "from", "to"),
        " Azure RBAC role"),
    AlertDescription = strcat(
        'This rule detects operations with Azure RBAC roles.\n\nThe ',
        case(TargetType == "User", 'user', TargetType == "ServicePrincipal", 'service principal', TargetType == "Group", 'group', 'member'),
        strcat(' "', coalesce(Target, TargetId), '"'),
        ' was ',
        iff(RemoveOperation, 'removed from', 'added to'),
        ' role "',
        coalesce(TargetRoleName, TargetRoleTemplateId),
        '" in ',
        iff(isnotempty(TargetResourceGroup), strcat('resource group "', TargetResourceGroup,'" from '), ''),
        case(
            isnotempty(TargetSubscriptionId), 'subscription "',
            isnotempty(TargetManagementGroupId), 'management group "',
            ''),
        case(
            isnotempty(TargetSubscriptionId), coalesce(TargetSubscriptionName, TargetSubscriptionId),
            isnotempty(TargetManagementGroupId), coalesce(TargetManagementGroupName, TargetManagementGroupId),
            ''),
        '" by "',
        iff(Caller matches regex @"[a-f0-9]+\-[a-f0-9]+\-[a-f0-9]+\-[a-f0-9]+\-[a-f0-9]+", coalesce(CallerDisplayName, Caller), Caller),
        '".\n'
    )
| extend AlertSeverity = case(
    // Remove operations by Azure AD PIM (assumedly automatic)
    RemoveOperation and CallerDisplayName == "Azure AD PIM" and AppDisplayName == "MS-PIM" and Caller in (_PIM_ids), "Informational",
    // Remove operations originated by resource group deletion
    RemoveOperation and SourceSystem == "Azure" and isnotempty(OriginOperationNameValue), "Informational",
    // Direct Azure operations
    not(SourceSystem == "PIM") and PrivilegedRole, "High",
    not(SourceSystem == "PIM") and not(PrivilegedRole), "Medium",
    // Voluntary deactivation operations
    AADOperationType == "DeactivateRole", "Informational",
    // Activation
    AADOperationType == "ActivateRole" and not(WorkingTime), "Medium",
    AADOperationType == "ActivateRole" and WorkingTime, "Informational",
    // Non-user target
    not(TargetGroupName == Target or TargetType in ("User", "Group")), "High",
    // Non-working hours
    not(WorkingTime), "High",
    // Working hours
    WorkingTime and PrivilegedRole and PermanentOperation, "Medium",
    WorkingTime and PrivilegedRole and not(PermanentOperation), "Medium",
    WorkingTime and not(PrivilegedRole) and PermanentOperation, "Low",
    WorkingTime and not(PrivilegedRole) and not(PermanentOperation), "Informational",
    "High"
    )
| project
    Type,
    SourceSystem,
    StartTime,
    EndTime,
    ResourceProviderValue,
    Category,
    CallerIpAddress,
    Caller,
    CallerDisplayName,
    AppDisplayName,
    OriginOperationNameValue,
    OperationNameValue,
    ActivityStatusValue,
    ActivitySubstatusValue,
    OperationName,
    Result,
    ResultReason,
    TargetManagementGroupName,
    TargetSubscriptionName,
    TargetResourceGroup,
    TargetRoleName,
    TargetGroupName,
	//PrivilegedRole,
    Target,
    TargetType,
    TargetManagementGroupId,
    TargetSubscriptionId,
    TargetRoleTemplateId,
    TargetId,
    AzureActivity_CorrelationId,
    ResourceProvider,
    CategoryValue,
    Level,
    CallerType,
    CallerRoleType,
    CallerRole,
    AppId,
    ActivitySubstatus,
    Authorization,
    PropertiesDynamic,
    HTTPRequest,
    Authorization_d,
    Properties_d,
    Claims_d,
    Resource,
    ResourceId,
    _ResourceId,
    Hierarchy,
    OperationId,
    AuditLogs_CorrelationId,
    AADOperationType,
    // WorkingTime,
    // ActivateOperation,
    // EligibleOperation,
    // RemoveOperation,
    // PermanentOperation,
    ActorIdentity,
    ActorUserId,
    ActorUserPrincipalName,
    ActorUserIPAddress,
    ActorUserRoles,
    ActorAppId,
    ActorAppName,
    ActorAppServicePrincipalId,
    ActorAppServicePrincipalName,
    //RoleProvider,
	TargetGroupId,
    TargetUserId,
    TargetUserPrincipalName,
    RequestId,
    RoleScheduleId,
    InitiatedBy,
    AdditionalDetails,
    TargetResources,
    AlertName,
    AlertSeverity,
    AlertDescription
//};
//Function(query_frequency)

Explanation

This KQL (Kusto Query Language) script is designed to detect and analyze Azure Role-Based Access Control (RBAC) role assignments, particularly focusing on changes made through Azure Privileged Identity Management (PIM) and other Azure activities. Here's a simplified breakdown of what the query does:

  1. Parameters and Setup:

    • The query is set up to run over a default period of 1 day (query_frequency) and looks back over a 14-day period (query_period).
    • It uses watchlists to gather specific data, such as expected significant activities, privileged Azure RBAC roles, Azure Active Directory (AAD) applications, groups, subscriptions, and roles.
  2. Data Collection:

    • It collects data from AuditLogs for activities logged by Azure PIM, focusing on role membership changes.
    • It also gathers data from AzureActivity logs to capture role assignment operations performed directly through Azure.
  3. Data Processing:

    • The query processes and summarizes the collected data to identify role assignment changes, including additions and removals of roles.
    • It distinguishes between operations performed by users and applications, and whether these operations were permanent or temporary.
  4. Role and Activity Analysis:

    • It identifies whether the roles involved are privileged roles using regular expressions.
    • The query checks if the operations occurred during working hours and evaluates the severity of the operations based on several criteria, such as whether the operation was a removal, if it was performed by Azure AD PIM, and if it involved privileged roles.
  5. Alert Generation:

    • Based on the analysis, the query generates alerts with names, descriptions, and severity levels. These alerts describe the nature of the role assignment change, who performed it, and the target of the change.
  6. Output:

    • The final output includes detailed information about each detected role assignment change, such as the type of operation, the actor involved, the target, and the alert details.

This query can be saved as a function named AzureRBACRoleAssignments, allowing users to easily execute it with different time spans to monitor and audit Azure RBAC role assignments effectively.