Microsoft Sentinel Analytic Rules
cloudbrothers.infoAzure Sentinel RepoToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeBack to homepage

Azure DevOps Service Connection Abuse

Back
Idd564ff12-8f53-41b8-8649-44f76b37b99f
RulenameAzure DevOps Service Connection Abuse
DescriptionFlags 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.
SeverityMedium
TacticsPersistence
Impact
TechniquesT1098
T1496
KindScheduled
Query frequency1d
Query period14d
Trigger threshold0
Trigger operatorgt
Source Urihttps://github.com/Azure/Azure-Sentinel/blob/master/Solutions/AzureDevOpsAuditing/Analytic Rules/AzDOServiceConnectionUsage.yaml
Version1.0.4
Arm templated564ff12-8f53-41b8-8649-44f76b37b99f.json
Deploy To Azure
// 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"
];
AzureDevOpsAuditing
| 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])
triggerOperator: gt
queryFrequency: 1d
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.'  
status: Available
kind: Scheduled
triggerThreshold: 0
requiredDataConnectors: []
version: 1.0.4
queryPeriod: 14d
name: Azure DevOps Service Connection Abuse
OriginalUri: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/AzureDevOpsAuditing/Analytic Rules/AzDOServiceConnectionUsage.yaml
id: d564ff12-8f53-41b8-8649-44f76b37b99f
tactics:
- Persistence
- Impact
relevantTechniques:
- T1098
- T1496
severity: Medium
entityMappings:
- fieldMappings:
  - identifier: FullName
    columnName: ActorUPN
  - identifier: Name
    columnName: AccountName
  - identifier: UPNSuffix
    columnName: AccountUPNSuffix
  entityType: Account
- fieldMappings:
  - identifier: Address
    columnName: IpAddress
  entityType: IP
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"
  ];
  AzureDevOpsAuditing
  | 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])  
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "workspace": {
      "type": "String"
    }
  },
  "resources": [
    {
      "apiVersion": "2023-02-01-preview",
      "id": "[concat(resourceId('Microsoft.OperationalInsights/workspaces/providers', parameters('workspace'), 'Microsoft.SecurityInsights'),'/alertRules/d564ff12-8f53-41b8-8649-44f76b37b99f')]",
      "kind": "Scheduled",
      "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/d564ff12-8f53-41b8-8649-44f76b37b99f')]",
      "properties": {
        "alertRuleTemplateName": "d564ff12-8f53-41b8-8649-44f76b37b99f",
        "customDetails": null,
        "description": "'Flags builds/releases that use a large number of service connections if they aren't manually in the allow list.\nThis is to determine if someone is hijacking a build/release and adding many service connections in order to abuse\nor dump credentials from service connections.'\n",
        "displayName": "Azure DevOps Service Connection Abuse",
        "enabled": true,
        "entityMappings": [
          {
            "entityType": "Account",
            "fieldMappings": [
              {
                "columnName": "ActorUPN",
                "identifier": "FullName"
              },
              {
                "columnName": "AccountName",
                "identifier": "Name"
              },
              {
                "columnName": "AccountUPNSuffix",
                "identifier": "UPNSuffix"
              }
            ]
          },
          {
            "entityType": "IP",
            "fieldMappings": [
              {
                "columnName": "IpAddress",
                "identifier": "Address"
              }
            ]
          }
        ],
        "OriginalUri": "https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/AzureDevOpsAuditing/Analytic Rules/AzDOServiceConnectionUsage.yaml",
        "query": "// How many greater than Service Connections you want to view per build/release\nlet ServiceConnectionThreshold = 4;\nlet BypassDefIds = datatable(DefId:string, Type:string, ProjectName:string)\n[\n//\"103\", \"Release\", \"ProjectA\",\n//\"42\", \"Release\", \"ProjectB\",\n//\"122\", \"Build\", \"ProjectB\"\n];\nAzureDevOpsAuditing\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, tostring(DefId), tostring(Type), ProjectId, ProjectName, ActorUPN, IpAddress\n| where CurrentCount > ServiceConnectionThreshold\n| join kind=anti BypassDefIds on $left.DefId==$right.DefId and $left.Type == $right.Type and $left.ProjectName == $right.ProjectName\n| extend link = iif(\n  Type == \"Build\", strcat('https://dev.azure.com/', OrganizationName, '/', ProjectName, '/_build?definitionId=', DefId),\n  strcat('https://dev.azure.com/', OrganizationName, '/', ProjectName, '/_release?_a=releases&view=mine&definitionId=', DefId))\n| extend timestamp = StartTime\n| extend AccountName = tostring(split(ActorUPN, \"@\")[0]), AccountUPNSuffix = tostring(split(ActorUPN, \"@\")[1])\n",
        "queryFrequency": "P1D",
        "queryPeriod": "P14D",
        "severity": "Medium",
        "status": "Available",
        "suppressionDuration": "PT1H",
        "suppressionEnabled": false,
        "tactics": [
          "Impact",
          "Persistence"
        ],
        "techniques": [
          "T1098",
          "T1496"
        ],
        "templateVersion": "1.0.4",
        "triggerOperator": "GreaterThan",
        "triggerThreshold": 0
      },
      "type": "Microsoft.OperationalInsights/workspaces/providers/alertRules"
    }
  ]
}