Detection rules › Kusto
New PA, PCA, or PCAS added to Azure DevOps
'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
| Tactic | Techniques |
|---|---|
| Initial Access | T1078.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.
| Field | Kind | Values |
|---|---|---|
Details | match |
|
OperationName | eq |
|
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.
| Field | Source |
|---|---|
ActionsWhenAdded | summarize |
ActorUPN | summarize |
AddingUser | summarize |
IpAddress | summarize |
PermissionGrantDetails | summarize |
TimeAdded | summarize |
UserAgent | summarize |
AccountName | extend |
AccountUPNSuffix | extend |
AddingUserAccountName | extend |
AddingUserAccountUPNSuffix | extend |