Detection rules › Kusto

Azure DevOps Service Connection Abuse

Status
available
Severity
medium
Time window
14d
Group by
ActorUPN, IpAddress, OrganizationName, ProjectId, ProjectName
Source
github.com/Azure/Azure-Sentinel

'Flags builds/releases that use a large number of service connections if they aren't manually in the allow list. This is to determine if someone is hijacking a build/release and adding many service connections in order to abuse or dump credentials from service connections.'

MITRE ATT&CK coverage

Rule body

id: d564ff12-8f53-41b8-8649-44f76b37b99f
name: Azure DevOps Service Connection Abuse
description: |
  'Flags builds/releases that use a large number of service connections if they aren't manually in the allow list.
  This is to determine if someone is hijacking a build/release and adding many service connections in order to abuse or dump credentials from service connections.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
  - Impact
relevantTechniques:
  - T1098
  - T1496
query: |
  // How many greater than Service Connections you want to view per build/release
  let ServiceConnectionThreshold = 4;
  let BypassDefIds = datatable(DefId:string, Type:string, ProjectName:string)
  [
  //"103", "Release", "ProjectA",
  //"42", "Release", "ProjectB",
  //"122", "Build", "ProjectB"
  ];
  ADOAuditLogs
  | where OperationName == "Library.ServiceConnectionExecuted"
  | extend DefId = tostring(Data.DefinitionId), Type = tostring(Data.PlanType), ConnectionId = tostring(Data.ConnectionId)
  | parse ScopeDisplayName with OrganizationName ' (Organization)'
  | summarize CurrentCount = dcount(tostring(ConnectionId)), ConnectionNames = make_set(tostring(Data.ConnectionName)), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated)
    by OrganizationName, tostring(DefId), tostring(Type), ProjectId, ProjectName, ActorUPN, IpAddress
  | where CurrentCount > ServiceConnectionThreshold
  | join kind=anti BypassDefIds on $left.DefId==$right.DefId and $left.Type == $right.Type and $left.ProjectName == $right.ProjectName
  | extend link = iif(
    Type == "Build", strcat('https://dev.azure.com/', OrganizationName, '/', ProjectName, '/_build?definitionId=', DefId),
    strcat('https://dev.azure.com/', OrganizationName, '/', ProjectName, '/_release?_a=releases&view=mine&definitionId=', DefId))
  | extend timestamp = StartTime
  | extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ActorUPN
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IpAddress
version: 1.0.6
kind: Scheduled

Stages and Predicates

Parameters

let ServiceConnectionThreshold = 4;

Let binding: BypassDefIds used in Stage 7

let BypassDefIds = datatable(DefId:string, Type:string, ProjectName:string)
[
];

Stage 1: source

ADOAuditLogs

Stage 2: where

| where OperationName == "Library.ServiceConnectionExecuted"

Stage 3: extend

| extend DefId = tostring(Data.DefinitionId), Type = tostring(Data.PlanType), ConnectionId = tostring(Data.ConnectionId)

Stage 4: parse

| parse ScopeDisplayName with OrganizationName ' (Organization)'

Stage 5: summarize

| summarize CurrentCount = dcount(tostring(ConnectionId)), ConnectionNames = make_set(tostring(Data.ConnectionName)), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated)
  by OrganizationName, tostring(DefId), tostring(Type), ProjectId, ProjectName, ActorUPN, IpAddress

Stage 6: where

| where CurrentCount > ServiceConnectionThreshold

Stage 7: join (negated)

| join kind=anti BypassDefIds on $left.DefId==$right.DefId and $left.Type == $right.Type and $left.ProjectName == $right.ProjectName

Stage 8: extend (3 consecutive steps)

| extend link = iif(
  Type == "Build", strcat('https://dev.azure.com/', OrganizationName, '/', ProjectName, '/_build?definitionId=', DefId),
  strcat('https://dev.azure.com/', OrganizationName, '/', ProjectName, '/_release?_a=releases&view=mine&definitionId=', DefId))
| extend timestamp = StartTime
| extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
ActorUPNsummarize
ConnectionNamessummarize
CurrentCountsummarize
EndTimesummarize
IpAddresssummarize
OrganizationNamesummarize
ProjectIdsummarize
ProjectNamesummarize
StartTimesummarize
linkextend
timestampextend
AccountNameextend
AccountUPNSuffixextend