Microsoft Sentinel Analytic Rules
cloudbrothers.infoAzure Sentinel RepoToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeBack to homepage

New External User Granted Admin Role

Back
Idd7424fd9-abb3-4ded-a723-eebe023aaa0b
RulenameNew External User Granted Admin Role
DescriptionThis query will detect instances where a newly invited external user is granted an administrative role.

By default this query will alert on any granted administrative role, however this can be modified using the roles variable if false positives occur in your environment. The maximum delta between invite and escalation to admin is 60 minues, this can be configured using the deltaBetweenInviteEscalation variable.
SeverityMedium
TacticsPersistence
TechniquesT1098.001
Required data connectorsAzureActiveDirectory
KindScheduled
Query frequency1d
Query period1d
Trigger threshold0
Trigger operatorgt
Source Urihttps://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Cloud Identity Threat Protection Essentials/Analytic Rules/NewExtUserGrantedAdmin.yaml
Version1.0.3
Arm templated7424fd9-abb3-4ded-a723-eebe023aaa0b.json
Deploy To Azure
// Administrative roles to look for, default is all admin roles
let roles = dynamic(["Administrator", "Admin"]);
// The maximum distances between and invite and acceptance
let maxTimeBetweenInviteAccept = 30min;
// The delta (minutes) between the invite being sent and the account being escalated
let deltaBetweenInviteEscalation = 60;
// Collect external user invitations
let invite = AuditLogs
| where Category =~ "UserManagement"
| where OperationName =~ "Invite external user"
| extend Target = tostring(TargetResources[0].["userPrincipalName"])
| extend InviteInitiator = tostring(InitiatedBy.["user"].["userPrincipalName"])
| where isnotempty(InviteInitiator);
// Collect redeem events
let redeem = AuditLogs
| where Category =~ "UserManagement"
| where OperationName =~ "Redeem external user invite"
| where Result =~ "success"
| extend Target = tostring(TargetResources[0].["displayName"]) | extend Target = tostring(extract(@"UPN\:\s(.+)\,\sEmail",1,Target))
| where isnotempty(Target);
// Union the inivtation and redeem data then run the sequence_detect kusto plugin
invite
| union redeem
| order by TimeGenerated
| project TimeGenerated, Target, InviteInitiator, OperationName, TenantId
| evaluate sequence_detect(TimeGenerated, maxTimeBetweenInviteAccept, maxTimeBetweenInviteAccept, invite=(OperationName has "Invite external user"), redeem=(OperationName has "Redeem external user invite"), Target)
| join kind=innerunique (
AuditLogs
| where Category =~ "RoleManagement"
| where AADOperationType in~ ("Assign", "AssignEligibleRole")
| where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role")
| mv-expand TargetResources
// Limit to external accounts
| where TargetResources.userPrincipalName has "EXT"
| mv-expand TargetResources.modifiedProperties
| extend displayName_ = tostring(TargetResources_modifiedProperties.displayName)
| where displayName_ =~ "Role.DisplayName"
| extend RoleName = tostring(parse_json(tostring(TargetResources_modifiedProperties.newValue)))
// Perform check for admin roles
| where RoleName has_any(roles)
| extend InitiatingApp = tostring(parse_json(tostring(InitiatedBy.app)).displayName)
| extend Initiator = iif(isnotempty(InitiatingApp), InitiatingApp, tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName))
| where Initiator != "MS-PIM"
| extend Target = tostring(TargetResources.userPrincipalName)
| summarize by TimeGenerated, OperationName,  RoleName, Target, Initiator, Result
) on Target
// Calculate delta between the invite and the account escalation
| extend delta = datetime_diff("minute", TimeGenerated, invite_TimeGenerated)
| where delta <= deltaBetweenInviteEscalation
| project InvitationTime=invite_TimeGenerated, RedeemTime=redeem_TimeGenerated, GrantTime=TimeGenerated, ExternalUser=Target, RoleGranted=RoleName, AdminInitiator=Initiator, MinsBetweenInviteAndEscalation=delta
| extend ExternalUserName = tostring(split(ExternalUser, '@', 0)[0]), ExternalUserUPNSuffix = tostring(split(ExternalUser, '@', 1)[0])
| extend AdminInitiatorName = tostring(split(AdminInitiator, '@', 0)[0]), AdminInitiatorUPNSuffix = tostring(split(AdminInitiator, '@', 1)[0])
relevantTechniques:
- T1098.001
name: New External User Granted Admin Role
requiredDataConnectors:
- dataTypes:
  - AuditLogs
  connectorId: AzureActiveDirectory
entityMappings:
- fieldMappings:
  - identifier: Name
    columnName: ExternalUserName
  - identifier: UPNSuffix
    columnName: ExternalUserUPNSuffix
  entityType: Account
- fieldMappings:
  - identifier: Name
    columnName: AdminInitiatorName
  - identifier: UPNSuffix
    columnName: AdminInitiatorUPNSuffix
  entityType: Account
triggerThreshold: 0
id: d7424fd9-abb3-4ded-a723-eebe023aaa0b
tactics:
- Persistence
version: 1.0.3
OriginalUri: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Cloud Identity Threat Protection Essentials/Analytic Rules/NewExtUserGrantedAdmin.yaml
queryPeriod: 1d
kind: Scheduled
queryFrequency: 1d
severity: Medium
status: Available
description: |
  'This query will detect instances where a newly invited external user is granted an administrative role.
  By default this query will alert on any granted administrative role, however this can be modified using the roles variable if false positives occur in your environment. The maximum delta between invite and escalation to admin is 60 minues, this can be configured using the deltaBetweenInviteEscalation variable.'  
query: |
  // Administrative roles to look for, default is all admin roles
  let roles = dynamic(["Administrator", "Admin"]);
  // The maximum distances between and invite and acceptance
  let maxTimeBetweenInviteAccept = 30min;
  // The delta (minutes) between the invite being sent and the account being escalated
  let deltaBetweenInviteEscalation = 60;
  // Collect external user invitations
  let invite = AuditLogs
  | where Category =~ "UserManagement"
  | where OperationName =~ "Invite external user"
  | extend Target = tostring(TargetResources[0].["userPrincipalName"])
  | extend InviteInitiator = tostring(InitiatedBy.["user"].["userPrincipalName"])
  | where isnotempty(InviteInitiator);
  // Collect redeem events
  let redeem = AuditLogs
  | where Category =~ "UserManagement"
  | where OperationName =~ "Redeem external user invite"
  | where Result =~ "success"
  | extend Target = tostring(TargetResources[0].["displayName"]) | extend Target = tostring(extract(@"UPN\:\s(.+)\,\sEmail",1,Target))
  | where isnotempty(Target);
  // Union the inivtation and redeem data then run the sequence_detect kusto plugin
  invite
  | union redeem
  | order by TimeGenerated
  | project TimeGenerated, Target, InviteInitiator, OperationName, TenantId
  | evaluate sequence_detect(TimeGenerated, maxTimeBetweenInviteAccept, maxTimeBetweenInviteAccept, invite=(OperationName has "Invite external user"), redeem=(OperationName has "Redeem external user invite"), Target)
  | join kind=innerunique (
  AuditLogs
  | where Category =~ "RoleManagement"
  | where AADOperationType in~ ("Assign", "AssignEligibleRole")
  | where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role")
  | mv-expand TargetResources
  // Limit to external accounts
  | where TargetResources.userPrincipalName has "EXT"
  | mv-expand TargetResources.modifiedProperties
  | extend displayName_ = tostring(TargetResources_modifiedProperties.displayName)
  | where displayName_ =~ "Role.DisplayName"
  | extend RoleName = tostring(parse_json(tostring(TargetResources_modifiedProperties.newValue)))
  // Perform check for admin roles
  | where RoleName has_any(roles)
  | extend InitiatingApp = tostring(parse_json(tostring(InitiatedBy.app)).displayName)
  | extend Initiator = iif(isnotempty(InitiatingApp), InitiatingApp, tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName))
  | where Initiator != "MS-PIM"
  | extend Target = tostring(TargetResources.userPrincipalName)
  | summarize by TimeGenerated, OperationName,  RoleName, Target, Initiator, Result
  ) on Target
  // Calculate delta between the invite and the account escalation
  | extend delta = datetime_diff("minute", TimeGenerated, invite_TimeGenerated)
  | where delta <= deltaBetweenInviteEscalation
  | project InvitationTime=invite_TimeGenerated, RedeemTime=redeem_TimeGenerated, GrantTime=TimeGenerated, ExternalUser=Target, RoleGranted=RoleName, AdminInitiator=Initiator, MinsBetweenInviteAndEscalation=delta
  | extend ExternalUserName = tostring(split(ExternalUser, '@', 0)[0]), ExternalUserUPNSuffix = tostring(split(ExternalUser, '@', 1)[0])
  | extend AdminInitiatorName = tostring(split(AdminInitiator, '@', 0)[0]), AdminInitiatorUPNSuffix = tostring(split(AdminInitiator, '@', 1)[0])  
triggerOperator: gt
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "workspace": {
      "type": "String"
    }
  },
  "resources": [
    {
      "apiVersion": "2024-01-01-preview",
      "id": "[concat(resourceId('Microsoft.OperationalInsights/workspaces/providers', parameters('workspace'), 'Microsoft.SecurityInsights'),'/alertRules/d7424fd9-abb3-4ded-a723-eebe023aaa0b')]",
      "kind": "Scheduled",
      "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/d7424fd9-abb3-4ded-a723-eebe023aaa0b')]",
      "properties": {
        "alertRuleTemplateName": "d7424fd9-abb3-4ded-a723-eebe023aaa0b",
        "customDetails": null,
        "description": "'This query will detect instances where a newly invited external user is granted an administrative role.\nBy default this query will alert on any granted administrative role, however this can be modified using the roles variable if false positives occur in your environment. The maximum delta between invite and escalation to admin is 60 minues, this can be configured using the deltaBetweenInviteEscalation variable.'\n",
        "displayName": "New External User Granted Admin Role",
        "enabled": true,
        "entityMappings": [
          {
            "entityType": "Account",
            "fieldMappings": [
              {
                "columnName": "ExternalUserName",
                "identifier": "Name"
              },
              {
                "columnName": "ExternalUserUPNSuffix",
                "identifier": "UPNSuffix"
              }
            ]
          },
          {
            "entityType": "Account",
            "fieldMappings": [
              {
                "columnName": "AdminInitiatorName",
                "identifier": "Name"
              },
              {
                "columnName": "AdminInitiatorUPNSuffix",
                "identifier": "UPNSuffix"
              }
            ]
          }
        ],
        "OriginalUri": "https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Cloud Identity Threat Protection Essentials/Analytic Rules/NewExtUserGrantedAdmin.yaml",
        "query": "// Administrative roles to look for, default is all admin roles\nlet roles = dynamic([\"Administrator\", \"Admin\"]);\n// The maximum distances between and invite and acceptance\nlet maxTimeBetweenInviteAccept = 30min;\n// The delta (minutes) between the invite being sent and the account being escalated\nlet deltaBetweenInviteEscalation = 60;\n// Collect external user invitations\nlet invite = AuditLogs\n| where Category =~ \"UserManagement\"\n| where OperationName =~ \"Invite external user\"\n| extend Target = tostring(TargetResources[0].[\"userPrincipalName\"])\n| extend InviteInitiator = tostring(InitiatedBy.[\"user\"].[\"userPrincipalName\"])\n| where isnotempty(InviteInitiator);\n// Collect redeem events\nlet redeem = AuditLogs\n| where Category =~ \"UserManagement\"\n| where OperationName =~ \"Redeem external user invite\"\n| where Result =~ \"success\"\n| extend Target = tostring(TargetResources[0].[\"displayName\"]) | extend Target = tostring(extract(@\"UPN\\:\\s(.+)\\,\\sEmail\",1,Target))\n| where isnotempty(Target);\n// Union the inivtation and redeem data then run the sequence_detect kusto plugin\ninvite\n| union redeem\n| order by TimeGenerated\n| project TimeGenerated, Target, InviteInitiator, OperationName, TenantId\n| evaluate sequence_detect(TimeGenerated, maxTimeBetweenInviteAccept, maxTimeBetweenInviteAccept, invite=(OperationName has \"Invite external user\"), redeem=(OperationName has \"Redeem external user invite\"), Target)\n| join kind=innerunique (\nAuditLogs\n| where Category =~ \"RoleManagement\"\n| where AADOperationType in~ (\"Assign\", \"AssignEligibleRole\")\n| where ActivityDisplayName has_any (\"Add eligible member to role\", \"Add member to role\")\n| mv-expand TargetResources\n// Limit to external accounts\n| where TargetResources.userPrincipalName has \"EXT\"\n| mv-expand TargetResources.modifiedProperties\n| extend displayName_ = tostring(TargetResources_modifiedProperties.displayName)\n| where displayName_ =~ \"Role.DisplayName\"\n| extend RoleName = tostring(parse_json(tostring(TargetResources_modifiedProperties.newValue)))\n// Perform check for admin roles\n| where RoleName has_any(roles)\n| extend InitiatingApp = tostring(parse_json(tostring(InitiatedBy.app)).displayName)\n| extend Initiator = iif(isnotempty(InitiatingApp), InitiatingApp, tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName))\n| where Initiator != \"MS-PIM\"\n| extend Target = tostring(TargetResources.userPrincipalName)\n| summarize by TimeGenerated, OperationName,  RoleName, Target, Initiator, Result\n) on Target\n// Calculate delta between the invite and the account escalation\n| extend delta = datetime_diff(\"minute\", TimeGenerated, invite_TimeGenerated)\n| where delta <= deltaBetweenInviteEscalation\n| project InvitationTime=invite_TimeGenerated, RedeemTime=redeem_TimeGenerated, GrantTime=TimeGenerated, ExternalUser=Target, RoleGranted=RoleName, AdminInitiator=Initiator, MinsBetweenInviteAndEscalation=delta\n| extend ExternalUserName = tostring(split(ExternalUser, '@', 0)[0]), ExternalUserUPNSuffix = tostring(split(ExternalUser, '@', 1)[0])\n| extend AdminInitiatorName = tostring(split(AdminInitiator, '@', 0)[0]), AdminInitiatorUPNSuffix = tostring(split(AdminInitiator, '@', 1)[0])\n",
        "queryFrequency": "P1D",
        "queryPeriod": "P1D",
        "severity": "Medium",
        "status": "Available",
        "subTechniques": [
          "T1098.001"
        ],
        "suppressionDuration": "PT1H",
        "suppressionEnabled": false,
        "tactics": [
          "Persistence"
        ],
        "techniques": [
          "T1098"
        ],
        "templateVersion": "1.0.3",
        "triggerOperator": "GreaterThan",
        "triggerThreshold": 0
      },
      "type": "Microsoft.OperationalInsights/workspaces/providers/alertRules"
    }
  ]
}