Detection rules › Kusto
Azure DevOps Pipeline Created and Deleted on the Same Day
'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
| Tactic | Techniques |
|---|---|
| Execution |
Rule body
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
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
OperationName | eq |
| field:"OperationName" kind:eq |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CreatingIP | project |
CreatingUser | project |
CreatingUserAgent | project |
Data | project |
DeletingIP | project |
DeletingUser | project |
DeletingUserAgent | project |
OperationName | project |
OperationName1 | project |
PipelineId | project |
PipelineName | project |
ProjectName | project |
ScopeDisplayName | project |
TimeCreated | project |
TimeDeleted | project |
timestamp | extend |
CreatingUserAccountName | extend |
CreatingUserAccountUPNSuffix | extend |
DeletingUserAccountName | extend |
DeletingUserAccountUPNSuffix | extend |