Detection rules › Kusto

New PA, PCA, or PCAS added to Azure DevOps

Status
available
Severity
medium
Time window
1d
Group by
ActorUPN, AddingUser, IpAddress, PermissionGrantDetails, TimeAdded, UserAgent
Source
github.com/Azure/Azure-Sentinel

'In order for an attacker to be able to conduct many potential attacks against Azure DevOps they will need to gain elevated permissions. This detection looks for users being granted key administrative permissions. If the principal of least privilege is applied, the number of users granted these permissions should be small. Note that permissions can also be granted via Microsoft Entra ID Protection groups and monitoring of these should also be conducted.'

MITRE ATT&CK coverage

TacticTechniques
Initial AccessT1078.004 Valid Accounts: Cloud Accounts

Rule body kusto

id: 35ce9aff-1708-45b8-a295-5e9a307f5f17
name: New PA, PCA, or PCAS added to Azure DevOps
description: |
  'In order for an attacker to be able to conduct many potential attacks against Azure DevOps they will need to gain elevated permissions. 
  This detection looks for users being granted key administrative permissions. If the principal of least privilege is applied, the number of users granted these permissions should be small. Note that permissions can also be granted via Microsoft Entra ID Protection groups and monitoring of these should also be conducted.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078.004
query: |
  ADOAuditLogs
  | where OperationName =~ "Group.UpdateGroupMembership.Add"
  | where Details has_any ("Project Administrators", "Project Collection Administrators", "Project Collection Service Accounts", "Build Administrator")
  | project-reorder TimeGenerated, Details, ActorUPN, IpAddress, UserAgent, AuthenticationMechanism, ScopeDisplayName
  | extend timekey = bin(TimeGenerated, 1h)
  | extend ActorUserId = tostring(Data.MemberId)
  | project timekey, ActorUserId, AddingUser=ActorUPN, TimeAdded=TimeGenerated, PermissionGrantDetails = Details
  // Get details of operations conducted by user soon after elevation of permissions
  | join (ADOAuditLogs
  | extend ActorUserId = tostring(Data.MemberId)
  | extend timekey = bin(TimeGenerated, 1h)) on timekey, ActorUserId
  | summarize ActionsWhenAdded = make_set(OperationName) by ActorUPN, AddingUser, TimeAdded, PermissionGrantDetails, IpAddress, UserAgent
  | extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])
  | extend AddingUserAccountName = tostring(split(AddingUser, "@")[0]), AddingUserAccountUPNSuffix = tostring(split(AddingUser, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ActorUPN
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AddingUser
      - identifier: Name
        columnName: AddingUserAccountName
      - identifier: UPNSuffix
        columnName: AddingUserAccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IpAddress
version: 1.0.6
kind: Scheduled

Stages and Predicates

Stage 1: source

ADOAuditLogs

Stage 2: where

| where OperationName =~ "Group.UpdateGroupMembership.Add"

Stage 3: where

| where Details has_any ("Project Administrators", "Project Collection Administrators", "Project Collection Service Accounts", "Build Administrator")

Stage 4: project-reorder

| project-reorder TimeGenerated, Details, ActorUPN, IpAddress, UserAgent, AuthenticationMechanism, ScopeDisplayName

Stage 5: extend

| extend timekey = bin(TimeGenerated, 1h)

Stage 6: extend

| extend ActorUserId = tostring(Data.MemberId)

Stage 7: project

| project timekey, ActorUserId, AddingUser=ActorUPN, TimeAdded=TimeGenerated, PermissionGrantDetails = Details

Stage 8: join

| join (ADOAuditLogs
| extend ActorUserId = tostring(Data.MemberId)
| extend timekey = bin(TimeGenerated, 1h)) on timekey, ActorUserId

Stage 9: summarize

| summarize ActionsWhenAdded = make_set(OperationName) by ActorUPN, AddingUser, TimeAdded, PermissionGrantDetails, IpAddress, UserAgent

Stage 10: extend

| extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])

Stage 11: extend

| extend AddingUserAccountName = tostring(split(AddingUser, "@")[0]), AddingUserAccountUPNSuffix = tostring(split(AddingUser, "@")[1])

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
Detailsmatch
  • Build Administrator
  • Project Administrators
  • Project Collection Administrators
  • Project Collection Service Accounts
OperationNameeq
  • Group.UpdateGroupMembership.Add

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
ActionsWhenAddedsummarize
ActorUPNsummarize
AddingUsersummarize
IpAddresssummarize
PermissionGrantDetailssummarize
TimeAddedsummarize
UserAgentsummarize
AccountNameextend
AccountUPNSuffixextend
AddingUserAccountNameextend
AddingUserAccountUPNSuffixextend