Detection rules › Kusto

Azure DevOps Administrator Group Monitoring

Status
available
Severity
medium
Time window
4h
Source
github.com/Azure/Azure-Sentinel

'This detection monitors for additions to projects or project collection administration groups in an Azure DevOps Organization.'

MITRE ATT&CK coverage

TacticTechniques
PersistenceT1098 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.

FieldKindValues
Areaeq
  • Group transforms: cased
Detailsends_with
  • \\Project Administrators
  • \\Project Collection Administrators
Detailsmatch
  • Administrators transforms: term
  • was added as a member of group transforms: term
EntityNamein
  • <project_X> transforms: cased
  • <project_Y> transforms: cased
Leveleq
  • Organization transforms: cased
OperationNameeq
  • Group.UpdateGroupMembership.Add transforms: cased

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
ActorAuthTypeproject
ActorIpAddressproject
ActorUserAgentproject
AddedIdentityproject
AddedIdentityIdproject
Adderproject
AlertDetailsproject
EntityNameproject
GroupNameproject
Levelproject
RawDetailsproject
Severityproject
TimeGeneratedproject
timestampextend
AccountNameextend
AccountUPNSuffixextend