Detection rules › Kusto
Azure DevOps Service Connection Addition/Abuse - Historic allow list
'This detection builds an allow list of historic service connection use by Builds and Releases and compares to recent history, flagging growth of service connection use which are not manually included in the allow list and not historically included in the allow list Build/Release runs. 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
| Tactic | Techniques |
|---|---|
| Persistence | |
| Impact |
Rule body
id: 5efb0cfd-063d-417a-803b-562eae5b0301
name: Azure DevOps Service Connection Addition/Abuse - Historic allow list
description: |
'This detection builds an allow list of historic service connection use by Builds and Releases and compares to recent history, flagging growth of service connection use which are not manually included in the allow list and not historically included in the allow list Build/Release runs.
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: 6h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
- Impact
relevantTechniques:
- T1098
- T1496
query: |
let starttime = 14d;
let endtime = 6h;
// Ignore Build/Releases with less/equal this number
let ServiceConnectionThreshold = 3;
// New Connections need to exhibit execution of more "new" connections than this number.
let NewConnectionThreshold = 1;
// List of Builds/Releases to ignore in your space
let BypassDefIds = datatable(DefId:string, Type:string, ProjectName:string)
[
//"103", "Release", "ProjectA",
//"42", "Release", "ProjectB",
//"122", "Build", "ProjectB"
];
let HistoricDefs = ADOAuditLogs
| where TimeGenerated between (ago(starttime) .. ago(endtime))
| where OperationName == "Library.ServiceConnectionExecuted"
| extend DefId = tostring(Data.DefinitionId), Type = tostring(Data.PlanType), ConnectionId = tostring(Data.ConnectionId)
| summarize HistoricCount = dcount(tostring(ConnectionId)), ConnectionNames = make_set(tostring(Data.ConnectionName))
by DefId = tostring(DefId), Type = tostring(Type), ProjectId, ProjectName, ActorUPN;
ADOAuditLogs
| where TimeGenerated >= ago(endtime)
| 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, DefId = tostring(DefId), Type = tostring(Type), ProjectId, ProjectName, ActorUPN
| where CurrentCount > ServiceConnectionThreshold
| join (HistoricDefs) on ProjectId, DefId, Type, ActorUPN
| join kind=anti BypassDefIds on $left.DefId==$right.DefId and $left.Type == $right.Type and $left.ProjectName == $right.ProjectName
| extend link = iff(
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))
| where CurrentCount >= HistoricCount + NewConnectionThreshold
| project StartTime, OrganizationName, ProjectName, DefId, link, RecentDistinctServiceConnections = CurrentCount, HistoricDistinctServiceConnections = HistoricCount,
RecentConnections = ConnectionNames, HistoricConnections = ConnectionNames1, ActorUPN
| 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
version: 1.0.6
kind: Scheduled
Stages and Predicates
Parameters
let starttime = 14d;
let endtime = 6h;
let ServiceConnectionThreshold = 3;
let NewConnectionThreshold = 1;
Let binding: BypassDefIds
let BypassDefIds = datatable(DefId:string, Type:string, ProjectName:string)
[
];
Let binding: HistoricDefs
let HistoricDefs = ADOAuditLogs
| where TimeGenerated between (ago(starttime) .. ago(endtime))
| where OperationName == "Library.ServiceConnectionExecuted"
| extend DefId = tostring(Data.DefinitionId), Type = tostring(Data.PlanType), ConnectionId = tostring(Data.ConnectionId)
| summarize HistoricCount = dcount(tostring(ConnectionId)), ConnectionNames = make_set(tostring(Data.ConnectionName))
by DefId = tostring(DefId), Type = tostring(Type), ProjectId, ProjectName, ActorUPN;
Stage 1: source
ADOAuditLogs
Stage 2: where
| where TimeGenerated >= ago(endtime)
Stage 3: where
| where OperationName == "Library.ServiceConnectionExecuted"
Stage 4: extend
| extend DefId = tostring(Data.DefinitionId), Type = tostring(Data.PlanType), ConnectionId = tostring(Data.ConnectionId)
Stage 5: parse
| parse ScopeDisplayName with OrganizationName ' (Organization)'
Stage 6: summarize
| summarize CurrentCount = dcount(tostring(ConnectionId)), ConnectionNames = make_set(tostring(Data.ConnectionName)), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated)
by OrganizationName, DefId = tostring(DefId), Type = tostring(Type), ProjectId, ProjectName, ActorUPN
Stage 7: where
| where CurrentCount > ServiceConnectionThreshold
Stage 8: join
| join (HistoricDefs) on ProjectId, DefId, Type, ActorUPN
Stage 9: join (negated)
| join kind=anti BypassDefIds on $left.DefId==$right.DefId and $left.Type == $right.Type and $left.ProjectName == $right.ProjectName
Stage 10: extend
| extend link = iff(
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))
link =if
Type == "Build"strcat('https://dev.azure.com/', OrganizationName, '/', ProjectName, '/_build?definitionId=', DefId)else
strcat('https://dev.azure.com/', OrganizationName, '/', ProjectName, '/_release?_a=releases&view=mine&definitionId=', DefId)Stage 11: where
| where CurrentCount >= HistoricCount + NewConnectionThreshold
Stage 12: project
| project StartTime, OrganizationName, ProjectName, DefId, link, RecentDistinctServiceConnections = CurrentCount, HistoricDistinctServiceConnections = HistoricCount,
RecentConnections = ConnectionNames, HistoricConnections = ConnectionNames1, ActorUPN
Stage 13: extend
| extend timestamp = StartTime
Stage 14: extend
| extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
CurrentCount | gt |
| field:"CurrentCount" kind:gt value:"3" |
OperationName | eq |
| field:"OperationName" kind:eq value:"Library.ServiceConnectionExecuted" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActorUPN | project |
DefId | project |
HistoricConnections | project |
HistoricDistinctServiceConnections | project |
OrganizationName | project |
ProjectName | project |
RecentConnections | project |
RecentDistinctServiceConnections | project |
StartTime | project |
link | project |
timestamp | extend |
AccountName | extend |
AccountUPNSuffix | extend |