Detection rules › Kusto

Azure DevOps Pipeline Created and Deleted on the Same Day

Status
available
Severity
medium
Time window
3d
Source
github.com/Azure/Azure-Sentinel

'An attacker with access to Azure DevOps could create a pipeline to inject artifacts used by other pipelines, or to create a malicious software build that looks legitimate by using a pipeline that incorporates legitimate elements. An attacker would also likely want to cover their tracks once conducting such activity. This query looks for Pipelines created and deleted within the same day, this is unlikely to be legitimate user activity in the majority of cases.'

MITRE ATT&CK coverage

TacticTechniques
ExecutionT1072 Software Deployment Tools

Rule body kusto

id: 17f23fbe-bb73-4324-8ecf-a18545a5dc26
name: Azure DevOps Pipeline Created and Deleted on the Same Day
description: |
  'An attacker with access to Azure DevOps could create a pipeline to inject artifacts used by other pipelines, or to create a malicious software build that looks legitimate by using a pipeline that incorporates legitimate elements. 
  An attacker would also likely want to cover their tracks once conducting such activity. This query looks for Pipelines created and deleted within the same day, this is unlikely to be legitimate user activity in the majority of cases.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 3d
queryPeriod: 3d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
relevantTechniques:
  - T1072
query: |
  let timeframe = 3d;
  // Get Release Pipeline Creation Events and group by day
  ADOAuditLogs
  | where TimeGenerated > ago(timeframe)
  | where OperationName =~ "Release.ReleasePipelineCreated"
  // Group by day
  | extend timekey = bin(TimeGenerated, 1d)
  | extend PipelineId = tostring(Data.PipelineId)
  | extend PipelineName = tostring(Data.PipelineName)
  // Rename some columns to make output clearer
  | project-rename TimeCreated = TimeGenerated, CreatingUser = ActorUPN, CreatingUserAgent = UserAgent, CreatingIP = IpAddress
  // Join with Release Pipeline Deletions where Pipeline ID is the same and deletion occurred on same day as creation
  | join (ADOAuditLogs
  | where TimeGenerated > ago(timeframe)
  | where OperationName =~ "Release.ReleasePipelineDeleted"
  // Group by day
  | extend timekey = bin(TimeGenerated, 1d)
  | extend PipelineId = tostring(Data.PipelineId)
  | extend PipelineName = tostring(Data.PipelineName)
  // Rename some things to make the output clearer
  | project-rename TimeDeleted = TimeGenerated,DeletingUser = ActorUPN, DeletingUserAgent = UserAgent, DeletingIP = IpAddress) on PipelineId, timekey
  | project TimeCreated, TimeDeleted, PipelineName, PipelineId, CreatingUser, CreatingIP, CreatingUserAgent, DeletingUser, DeletingIP, DeletingUserAgent, ScopeDisplayName, ProjectName, Data, OperationName, OperationName1
  | extend timestamp = TimeCreated
  | extend CreatingUserAccountName = tostring(split(CreatingUser, "@")[0]), CreatingUserAccountUPNSuffix = tostring(split(CreatingUser, "@")[1])
  | extend DeletingUserAccountName = tostring(split(DeletingUser, "@")[0]), DeletingUserAccountUPNSuffix = tostring(split(DeletingUser, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: CreatingUser
      - identifier: Name
        columnName: CreatingUserAccountName
      - identifier: UPNSuffix
        columnName: CreatingUserAccountUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: DeletingUser
      - identifier: Name
        columnName: DeletingUserAccountName
      - identifier: UPNSuffix
        columnName: DeletingUserAccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CreatingIP
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: DeletingIP
version: 1.0.4
kind: Scheduled

Stages and Predicates

Parameters

let timeframe = 3d;

Stage 1: source

ADOAuditLogs

Stage 2: where

| where TimeGenerated > ago(timeframe)

Stage 3: where

| where OperationName =~ "Release.ReleasePipelineCreated"

Stage 4: extend (3 consecutive steps)

| extend timekey = bin(TimeGenerated, 1d)
| extend PipelineId = tostring(Data.PipelineId)
| extend PipelineName = tostring(Data.PipelineName)

Stage 5: project-rename

| project-rename TimeCreated = TimeGenerated, CreatingUser = ActorUPN, CreatingUserAgent = UserAgent, CreatingIP = IpAddress

Stage 6: join

| join (ADOAuditLogs
| where TimeGenerated > ago(timeframe)
| where OperationName =~ "Release.ReleasePipelineDeleted"
| extend timekey = bin(TimeGenerated, 1d)
| extend PipelineId = tostring(Data.PipelineId)
| extend PipelineName = tostring(Data.PipelineName)
| project-rename TimeDeleted = TimeGenerated,DeletingUser = ActorUPN, DeletingUserAgent = UserAgent, DeletingIP = IpAddress) on PipelineId, timekey

Stage 7: project

| project TimeCreated, TimeDeleted, PipelineName, PipelineId, CreatingUser, CreatingIP, CreatingUserAgent, DeletingUser, DeletingIP, DeletingUserAgent, ScopeDisplayName, ProjectName, Data, OperationName, OperationName1

Stage 8: extend (3 consecutive steps)

| extend timestamp = TimeCreated
| extend CreatingUserAccountName = tostring(split(CreatingUser, "@")[0]), CreatingUserAccountUPNSuffix = tostring(split(CreatingUser, "@")[1])
| extend DeletingUserAccountName = tostring(split(DeletingUser, "@")[0]), DeletingUserAccountUPNSuffix = tostring(split(DeletingUser, "@")[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
OperationNameeq
  • Release.ReleasePipelineCreated
  • Release.ReleasePipelineDeleted

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
CreatingIPproject
CreatingUserproject
CreatingUserAgentproject
Dataproject
DeletingIPproject
DeletingUserproject
DeletingUserAgentproject
OperationNameproject
OperationName1project
PipelineIdproject
PipelineNameproject
ProjectNameproject
ScopeDisplayNameproject
TimeCreatedproject
TimeDeletedproject
timestampextend
CreatingUserAccountNameextend
CreatingUserAccountUPNSuffixextend
DeletingUserAccountNameextend
DeletingUserAccountUPNSuffixextend