Detection rules › Kusto
Azure DevOps Administrator Group Monitoring
'This detection monitors for additions to projects or project collection administration groups in an Azure DevOps Organization.'
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | T1098 Account Manipulation |
Rule body kusto
id: 89e6adbd-612c-4fbe-bc3d-32f81baf3b6c
name: Azure DevOps Administrator Group Monitoring
description: |
'This detection monitors for additions to projects or project collection administration groups in an Azure DevOps Organization.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 4h
queryPeriod: 4h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
relevantTechniques:
- T1098
query: |
// Change to true to monitor for Project Administrator adds to *any* project
let MonitorAllProjects = false;
// If MonitorAllProjects is false, trigger only on Project Administrator add for the following projects
let ProjectsToMonitor = dynamic(['<project_X>','<project_Y>']);
ADOAuditLogs
| where Area == "Group" and OperationName == "Group.UpdateGroupMembership.Add"
| where Details has 'Administrators'
| where Details has "was added as a member of group" and (Details endswith '\\Project Administrators' or Details endswith '\\Project Collection Administrators')
| parse Details with AddedIdentity ' was added as a member of group [' EntityName ']\\' GroupName
| extend Level = iif(GroupName == 'Project Collection Administrators', 'Organization', 'Project'), AddedIdentityId = Data.MemberId
| extend Severity = iif(Level == 'Organization', 'High', 'Medium'), AlertDetails = strcat('At ', TimeGenerated, ' UTC ', ActorUPN, '/', ActorDisplayName, ' added ', AddedIdentity, ' to the ', EntityName, ' ', Level)
| where MonitorAllProjects == true or EntityName in (ProjectsToMonitor) or Level == 'Organization'
| project TimeGenerated, Severity, Adder = ActorUPN, AddedIdentity, AddedIdentityId, AlertDetails, Level, EntityName, GroupName, ActorAuthType = AuthenticationMechanism,
ActorIpAddress = IpAddress, ActorUserAgent = UserAgent, RawDetails = Details
| extend timestamp = TimeGenerated
| extend AccountName = tostring(split(Adder, "@")[0]), AccountUPNSuffix = tostring(split(Adder, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Adder
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ActorIpAddress
version: 1.0.5
kind: Scheduled
Stages and Predicates
Parameters
let MonitorAllProjects = false;
let ProjectsToMonitor = dynamic(['<project_X>','<project_Y>']);
Stage 1: source
ADOAuditLogs
Stage 2: where
| where Area == "Group" and OperationName == "Group.UpdateGroupMembership.Add"
Stage 3: where
| where Details has 'Administrators'
Stage 4: where
| where Details has "was added as a member of group" and (Details endswith '\\Project Administrators' or Details endswith '\\Project Collection Administrators')
Stage 5: parse
| parse Details with AddedIdentity ' was added as a member of group [' EntityName ']\\' GroupName
Stage 6: extend
| extend Level = iif(GroupName == 'Project Collection Administrators', 'Organization', 'Project'), AddedIdentityId = Data.MemberId
Stage 7: extend
| extend Severity = iif(Level == 'Organization', 'High', 'Medium'), AlertDetails = strcat('At ', TimeGenerated, ' UTC ', ActorUPN, '/', ActorDisplayName, ' added ', AddedIdentity, ' to the ', EntityName, ' ', Level)
Stage 8: where
| where MonitorAllProjects == true or EntityName in (ProjectsToMonitor) or Level == 'Organization'
Stage 9: project
| project TimeGenerated, Severity, Adder = ActorUPN, AddedIdentity, AddedIdentityId, AlertDetails, Level, EntityName, GroupName, ActorAuthType = AuthenticationMechanism,
ActorIpAddress = IpAddress, ActorUserAgent = UserAgent, RawDetails = Details
Stage 10: extend
| extend timestamp = TimeGenerated
Stage 11: extend
| extend AccountName = tostring(split(Adder, "@")[0]), AccountUPNSuffix = tostring(split(Adder, "@")[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 |
|---|---|---|
Area | eq |
|
Details | ends_with |
|
Details | match |
|
EntityName | in |
|
Level | eq |
|
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 |
|---|---|
ActorAuthType | project |
ActorIpAddress | project |
ActorUserAgent | project |
AddedIdentity | project |
AddedIdentityId | project |
Adder | project |
AlertDetails | project |
EntityName | project |
GroupName | project |
Level | project |
RawDetails | project |
Severity | project |
TimeGenerated | project |
timestamp | extend |
AccountName | extend |
AccountUPNSuffix | extend |