Detection rules › Kusto

Azure DevOps Pipeline modified by a new user

Status
available
Severity
medium
Time window
14d
Group by
AadUserId, ActorUserId
Source
github.com/Azure/Azure-Sentinel

'There are several potential pipeline steps that could be modified by an attacker to inject malicious code into the build cycle. A likely attacker path is the modification to an existing pipeline that they have access to. This detection looks for users modifying a pipeline when they have not previously been observed modifying or creating that pipeline before. This query also joins events with data to Microsoft Entra ID Protection in order to show if the user conducting the action has any associated Microsoft Entra ID Protection alerts. You can also choose to filter this detection to only alert when the user also has Microsoft Entra ID Protection alerts associated with them.'

MITRE ATT&CK coverage

TacticTechniques
ExecutionT1569 System Services
Defense ImpairmentT1578 Modify Cloud Compute Infrastructure

Rule body kusto

id: 155e9134-d5ad-4a6f-88f3-99c220040b66
name: Azure DevOps Pipeline modified by a new user
description: |
  'There are several potential pipeline steps that could be modified by an attacker to inject malicious code into the build cycle. A likely attacker path is the modification to an existing pipeline that they have access to. 
  This detection looks for users modifying a pipeline when they have not previously been observed modifying or creating that pipeline before. This query also joins events with data to Microsoft Entra ID Protection in order to show if the user conducting the action has any associated Microsoft Entra ID Protection alerts. You can also choose to filter this detection to only alert when the user also has Microsoft Entra ID Protection alerts associated with them.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution 
  - DefenseEvasion
relevantTechniques:
  - T1578
  - T1569
query: |
  // Set the lookback to determine if user has created pipelines before
  let timeback = 14d;
  // Set the period for detections
  let timeframe = 1d;
  // Get a list of previous Release Pipeline creators to exclude
  let releaseusers = ADOAuditLogs
  | where TimeGenerated > ago(timeback) and TimeGenerated < ago(timeframe)
  | where OperationName in ("Release.ReleasePipelineCreated", "Release.ReleasePipelineModified")
  // We want to look for users performing actions in specific projects so we create this userscope object to match on
  | extend UserScope = strcat(ActorUserId, "-", ProjectName)
  | summarize by UserScope;
  // Get Release Pipeline creations by new users
  ADOAuditLogs
  | where TimeGenerated > ago(timeframe)
  | where OperationName =~ "Release.ReleasePipelineModified"
  | extend UserScope = strcat(ActorUserId, "-", ProjectName)
  | where UserScope !in (releaseusers)
  | extend ActorUPN = tolower(ActorUPN)
  | project-away Id, ActivityId, ActorCUID, ScopeId, ProjectId, TenantId, SourceSystem, UserScope
  // See if any of these users have Azure AD alerts associated with them in the same timeframe
  | join kind = leftouter (
  SecurityAlert
  | where TimeGenerated > ago(timeframe)
  | where ProviderName == "IPC"
  | extend AadUserId = tostring(parse_json(Entities)[0].AadUserId)
  | summarize Alerts=count() by AadUserId) on $left.ActorUserId == $right.AadUserId
  | extend Alerts = iif(isnotempty(Alerts), Alerts, 0)
  // Uncomment the line below to only show results where the user as AADIdP alerts
  //| where Alerts > 0
  | extend timestamp = TimeGenerated
  | 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
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IpAddress
version: 1.0.8
kind: Scheduled

Stages and Predicates

Parameters

let timeback = 14d;
let timeframe = 1d;

Let binding: releaseusers

let releaseusers = ADOAuditLogs
| where TimeGenerated > ago(timeback) and TimeGenerated < ago(timeframe)
| where OperationName in ("Release.ReleasePipelineCreated", "Release.ReleasePipelineModified")
| extend UserScope = strcat(ActorUserId, "-", ProjectName)
| summarize by UserScope;

Derived from timeback, timeframe.

Stage 1: source

ADOAuditLogs

Stage 2: where

| where TimeGenerated > ago(timeframe)

Stage 3: where

| where OperationName =~ "Release.ReleasePipelineModified"

Stage 4: extend

| extend UserScope = strcat(ActorUserId, "-", ProjectName)

Stage 5: where

| where UserScope !in (releaseusers)

References releaseusers (defined above).

Stage 6: extend

| extend ActorUPN = tolower(ActorUPN)

Stage 7: project-away

| project-away Id, ActivityId, ActorCUID, ScopeId, ProjectId, TenantId, SourceSystem, UserScope

Stage 8: join

| join kind = leftouter (
SecurityAlert
| where TimeGenerated > ago(timeframe)
| where ProviderName == "IPC"
| extend AadUserId = tostring(parse_json(Entities)[0].AadUserId)
| summarize Alerts=count() by AadUserId) on $left.ActorUserId == $right.AadUserId

Stage 9: extend (3 consecutive steps)

| extend Alerts = iif(isnotempty(Alerts), Alerts, 0)
| extend timestamp = TimeGenerated
| extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])

Stage 10: summarize

summarize by AadUserId

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

FieldKindExcluded values
UserScopeeqreleaseusers

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.ReleasePipelineModified
ProviderNameeq
  • IPC transforms: cased

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
AadUserIdsummarize