Azure DevOps Service Connection Addition/Abuse - Historic allow list
Id | 5efb0cfd-063d-417a-803b-562eae5b0301 |
Rulename | 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 |
Tactics | Persistence Impact |
Techniques | T1098 T1496 |
Kind | Scheduled |
Query frequency | 6h |
Query period | 14d |
Trigger threshold | 0 |
Trigger operator | gt |
Source Uri | https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/AzureDevOpsAuditing/Analytic Rules/AzDOHistoricServiceConnectionAdds.yaml |
Version | 1.0.2 |
Arm template | 5efb0cfd-063d-417a-803b-562eae5b0301.json |
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 = AzureDevOpsAuditing
| 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;
AzureDevOpsAuditing
| 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, AccountCustomEntity = ActorUPN
requiredDataConnectors: []
triggerOperator: gt
queryFrequency: 6h
name: Azure DevOps Service Connection Addition/Abuse - Historic allow list
status: Available
queryPeriod: 14d
id: 5efb0cfd-063d-417a-803b-562eae5b0301
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
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 = AzureDevOpsAuditing
| 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;
AzureDevOpsAuditing
| 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, AccountCustomEntity = ActorUPN
version: 1.0.2
kind: Scheduled
OriginalUri: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/AzureDevOpsAuditing/Analytic Rules/AzDOHistoricServiceConnectionAdds.yaml
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AccountCustomEntity
relevantTechniques:
- T1098
- T1496
tactics:
- Persistence
- Impact
triggerThreshold: 0
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"workspace": {
"type": "String"
}
},
"resources": [
{
"id": "[concat(resourceId('Microsoft.OperationalInsights/workspaces/providers', parameters('workspace'), 'Microsoft.SecurityInsights'),'/alertRules/5efb0cfd-063d-417a-803b-562eae5b0301')]",
"name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/5efb0cfd-063d-417a-803b-562eae5b0301')]",
"type": "Microsoft.OperationalInsights/workspaces/providers/alertRules",
"kind": "Scheduled",
"apiVersion": "2022-11-01-preview",
"properties": {
"displayName": "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\nnot 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.'\n",
"severity": "Medium",
"enabled": true,
"query": "let starttime = 14d;\nlet endtime = 6h;\n// Ignore Build/Releases with less/equal this number\nlet ServiceConnectionThreshold = 3;\n// New Connections need to exhibit execution of more \"new\" connections than this number.\nlet NewConnectionThreshold = 1;\n// List of Builds/Releases to ignore in your space\nlet BypassDefIds = datatable(DefId:string, Type:string, ProjectName:string)\n[\n//\"103\", \"Release\", \"ProjectA\",\n//\"42\", \"Release\", \"ProjectB\",\n//\"122\", \"Build\", \"ProjectB\"\n];\nlet HistoricDefs = AzureDevOpsAuditing\n| where TimeGenerated between (ago(starttime) .. ago(endtime))\n| where OperationName == \"Library.ServiceConnectionExecuted\"\n| extend DefId = tostring(Data.DefinitionId), Type = tostring(Data.PlanType), ConnectionId = tostring(Data.ConnectionId)\n| summarize HistoricCount = dcount(tostring(ConnectionId)), ConnectionNames = make_set(tostring(Data.ConnectionName))\n by DefId = tostring(DefId), Type = tostring(Type), ProjectId, ProjectName, ActorUPN;\nAzureDevOpsAuditing\n| where TimeGenerated >= ago(endtime)\n| where OperationName == \"Library.ServiceConnectionExecuted\"\n| extend DefId = tostring(Data.DefinitionId), Type = tostring(Data.PlanType), ConnectionId = tostring(Data.ConnectionId)\n| parse ScopeDisplayName with OrganizationName ' (Organization)'\n| summarize CurrentCount = dcount(tostring(ConnectionId)), ConnectionNames = make_set(tostring(Data.ConnectionName)), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated)\n by OrganizationName, DefId = tostring(DefId), Type = tostring(Type), ProjectId, ProjectName, ActorUPN\n| where CurrentCount > ServiceConnectionThreshold\n| join (HistoricDefs) on ProjectId, DefId, Type, ActorUPN\n| join kind=anti BypassDefIds on $left.DefId==$right.DefId and $left.Type == $right.Type and $left.ProjectName == $right.ProjectName\n| extend link = iff(\nType == \"Build\", strcat('https://dev.azure.com/', OrganizationName, '/', ProjectName, '/_build?definitionId=', DefId),\nstrcat('https://dev.azure.com/', OrganizationName, '/', ProjectName, '/_release?_a=releases&view=mine&definitionId=', DefId))\n| where CurrentCount >= HistoricCount + NewConnectionThreshold\n| project StartTime, OrganizationName, ProjectName, DefId, link, RecentDistinctServiceConnections = CurrentCount, HistoricDistinctServiceConnections = HistoricCount,\n RecentConnections = ConnectionNames, HistoricConnections = ConnectionNames1, ActorUPN\n| extend timestamp = StartTime, AccountCustomEntity = ActorUPN\n",
"queryFrequency": "PT6H",
"queryPeriod": "P14D",
"triggerOperator": "GreaterThan",
"triggerThreshold": 0,
"suppressionDuration": "PT1H",
"suppressionEnabled": false,
"tactics": [
"Persistence",
"Impact"
],
"techniques": [
"T1098",
"T1496"
],
"alertRuleTemplateName": "5efb0cfd-063d-417a-803b-562eae5b0301",
"customDetails": null,
"entityMappings": [
{
"fieldMappings": [
{
"identifier": "FullName",
"columnName": "AccountCustomEntity"
}
],
"entityType": "Account"
}
],
"templateVersion": "1.0.2",
"OriginalUri": "https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/AzureDevOpsAuditing/Analytic Rules/AzDOHistoricServiceConnectionAdds.yaml",
"status": "Available"
}
}
]
}