Detection rules › Kusto

Potential Build Process Compromise

Severity
medium
Time window
5m
Group by
BuildAccount, BuildCommand, BuildParentProcess, BuildProcess, BuildProcessTime, Computer, FileEditAccount, FileEditParentProcess, FileEditProcess, FileEditTime, FileEdited, timekey
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

The query looks for source code files being modified immediately after a build process is started. The purpose of this is to look for malicious code injection during the build process. More details: https://techcommunity.microsoft.com/t5/azure-sentinel/monitoring-the-software-supply-chain-with-azure-sentinel/ba-p/2176463

MITRE ATT&CK coverage

TacticTechniques
PersistenceT1554 Compromise Host Software Binary

Event coverage

Rule body kusto

id: 5ef06767-b37c-4818-b035-47de950d0046
name: Potential Build Process Compromise
description: |
  'The query looks for source code files being modified immediately after a build process is started. The purpose of this is to look for malicious code injection during the build process.
  More details: https://techcommunity.microsoft.com/t5/azure-sentinel/monitoring-the-software-supply-chain-with-azure-sentinel/ba-p/2176463'
severity: Medium
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes: 
      - SecurityEvents 
  - connectorId: WindowsForwardedEvents
    dataTypes: 
      - WindowsEvent 
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
relevantTechniques:
  - T1554
tags:
  - Solorigate
  - NOBELIUM
query: |
  // How far back to look for events from
  let timeframe = 1d;
  // How close together build events and file modifications should occur to alert (make this smaller to reduce FPs)
  let time_window = 5m;
  // Edit this to include build processes used
  let build_processes = dynamic(["MSBuild.exe", "dotnet.exe", "VBCSCompiler.exe"]);
  // Include any processes that you want to allow to edit files during/around the build process
  let allow_list = dynamic([""]);
  (union isfuzzy=true
  (SecurityEvent
  | where TimeGenerated > ago(timeframe)
  // Look for build process starts
  | where EventID == 4688
  | where Process has_any (build_processes)
  | summarize by BuildParentProcess=ParentProcessName, BuildProcess=Process, BuildAccount = Account, Computer, BuildCommand=CommandLine, timekey= bin(TimeGenerated, time_window), BuildProcessTime=TimeGenerated
  | join kind=inner(
  SecurityEvent
  | where TimeGenerated > ago(timeframe)
  // Look for file modifications to code file
  | where EventID == 4663
  | where Process !in (allow_list)
  // Look for code files, edit this to include file extensions used in build.
  | where ObjectName endswith ".cs" or ObjectName endswith ".cpp"
  // 0x6 and 0x4 for file append, 0x100 for file replacements
  | where AccessMask == "0x6"  or AccessMask == "0x4" or AccessMask == "0X100"
  | summarize by FileEditParentProcess=ParentProcessName, FileEditAccount = Account, Computer, FileEdited=ObjectName, FileEditProcess=ProcessName, timekey= bin(TimeGenerated, time_window), FileEditTime=TimeGenerated)
  // join where build processes and file modifications seen at same time on same host
  on timekey, Computer
  // Limit to only where the file edit happens after the build process starts
  | where BuildProcessTime <= FileEditTime
  | summarize make_set(FileEdited), make_set(FileEditProcess), make_set(FileEditAccount) by timekey, Computer, BuildParentProcess, BuildProcess
  ),
  (WindowsEvent
  | where TimeGenerated > ago(timeframe)
  // Look for build process starts
  | where EventID == 4688 and EventData has_any (build_processes)
  | extend NewProcessName = tostring(EventData.NewProcessName)
  | extend Process=tostring(split(NewProcessName, '\\')[-1])
  | where Process has_any (build_processes)
  | extend ParentProcessName = tostring(EventData.ParentProcessName)
  | extend Account =  strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
  | extend CommandLine = tostring(EventData.CommandLine) 
  | summarize by BuildParentProcess=ParentProcessName, BuildProcess=Process, BuildAccount = Account, Computer, BuildCommand=CommandLine, timekey= bin(TimeGenerated, time_window), BuildProcessTime=TimeGenerated
  | join kind=inner(
  WindowsEvent
  | where TimeGenerated > ago(timeframe)
  // Look for file modifications to code file
  | where EventID == 4663 and EventData has_any ("0x6", "0x4", "0X100") and EventData has_any (".cs", ".cpp")
  | extend NewProcessName = tostring(EventData.NewProcessName)
  | extend Process=tostring(split(NewProcessName, '\\')[-1])
  | where Process !in (allow_list)
  // Look for code files, edit this to include file extensions used in build.
  | extend ObjectName = tostring(EventData.ObjectName)
  | where ObjectName endswith ".cs" or ObjectName endswith ".cpp"
  // 0x6 and 0x4 for file append, 0x100 for file replacements
  | extend AccessMask = tostring(EventData.AccessMask)  
  | where AccessMask == "0x6"  or AccessMask == "0x4" or AccessMask == "0X100"
  | extend ParentProcessName = tostring(EventData.ParentProcessName)
  | extend Account =  strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
  | extend ProcessName = tostring(EventData.ProcessName)
  | summarize by FileEditParentProcess=ParentProcessName, FileEditAccount = Account, Computer, FileEdited=ObjectName, FileEditProcess=ProcessName, timekey= bin(TimeGenerated, time_window), FileEditTime=TimeGenerated)
  // join where build processes and file modifications seen at same time on same host
  on timekey, Computer
  // Limit to only where the file edit happens after the build process starts
  | where BuildProcessTime <= FileEditTime
  | summarize make_set(FileEdited), make_set(FileEditProcess), make_set(FileEditAccount) by timekey, Computer, BuildParentProcess, BuildProcess
  ))
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
  | project-away DomainIndex
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
version: 1.1.4
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others" ]

Stages and Predicates

Parameters

let timeframe = 1d;
let time_window = 5m;
let build_processes = dynamic(["MSBuild.exe", "dotnet.exe", "VBCSCompiler.exe"]);
let allow_list = dynamic([""]);

union isfuzzy=true (2 sources)

Each leg below queries one source; the rule matches if any leg does. Sources: SecurityEvent, WindowsEvent

Leg 1: SecurityEvent

SecurityEvent
| where TimeGenerated > ago(timeframe)
| where EventID == 4688
| where Process has_any (build_processes)
| summarize by BuildParentProcess=ParentProcessName, BuildProcess=Process, BuildAccount = Account, Computer, BuildCommand=CommandLine, timekey= bin(TimeGenerated, time_window), BuildProcessTime=TimeGenerated
| join kind=inner(
SecurityEvent
| where TimeGenerated > ago(timeframe)
| where EventID == 4663
| where Process !in (allow_list)
| where ObjectName endswith ".cs" or ObjectName endswith ".cpp"
| where AccessMask == "0x6"  or AccessMask == "0x4" or AccessMask == "0X100"
| summarize by FileEditParentProcess=ParentProcessName, FileEditAccount = Account, Computer, FileEdited=ObjectName, FileEditProcess=ProcessName, timekey= bin(TimeGenerated, time_window), FileEditTime=TimeGenerated)
on timekey, Computer
| where BuildProcessTime <= FileEditTime
| summarize make_set(FileEdited), make_set(FileEditProcess), make_set(FileEditAccount) by timekey, Computer, BuildParentProcess, BuildProcess

Leg 2: WindowsEvent

WindowsEvent
| where TimeGenerated > ago(timeframe)
| where EventID == 4688 and EventData has_any (build_processes)
| extend NewProcessName = tostring(EventData.NewProcessName)
| extend Process=tostring(split(NewProcessName, '\\')[-1])
| where Process has_any (build_processes)
| extend ParentProcessName = tostring(EventData.ParentProcessName)
| extend Account =  strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend CommandLine = tostring(EventData.CommandLine) 
| summarize by BuildParentProcess=ParentProcessName, BuildProcess=Process, BuildAccount = Account, Computer, BuildCommand=CommandLine, timekey= bin(TimeGenerated, time_window), BuildProcessTime=TimeGenerated
| join kind=inner(
WindowsEvent
| where TimeGenerated > ago(timeframe)
| where EventID == 4663 and EventData has_any ("0x6", "0x4", "0X100") and EventData has_any (".cs", ".cpp")
| extend NewProcessName = tostring(EventData.NewProcessName)
| extend Process=tostring(split(NewProcessName, '\\')[-1])
| where Process !in (allow_list)
| extend ObjectName = tostring(EventData.ObjectName)
| where ObjectName endswith ".cs" or ObjectName endswith ".cpp"
| extend AccessMask = tostring(EventData.AccessMask)  
| where AccessMask == "0x6"  or AccessMask == "0x4" or AccessMask == "0X100"
| extend ParentProcessName = tostring(EventData.ParentProcessName)
| extend Account =  strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend ProcessName = tostring(EventData.ProcessName)
| summarize by FileEditParentProcess=ParentProcessName, FileEditAccount = Account, Computer, FileEdited=ObjectName, FileEditProcess=ProcessName, timekey= bin(TimeGenerated, time_window), FileEditTime=TimeGenerated)
on timekey, Computer
| where BuildProcessTime <= FileEditTime
| summarize make_set(FileEdited), make_set(FileEditProcess), make_set(FileEditAccount) by timekey, Computer, BuildParentProcess, BuildProcess

Applied to the combined result

| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| project-away DomainIndex

Exclusions

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

FieldKindExcluded values
Processeqallow_list
Processeqallow_list

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
AccessMaskeq
  • 0X100 transforms: cased
  • 0x4 transforms: cased
  • 0x6 transforms: cased corpus 3 (sigma 2, kusto 1)
BuildProcessTimele
  • FileEditTime transforms: cased corpus 2 (kusto 2)
EventDatamatch
  • .cpp
  • .cs
  • 0X100
  • 0x4
  • 0x6
  • MSBuild.exe
  • VBCSCompiler.exe
  • dotnet.exe
EventIDeq
  • 4663 transforms: cased corpus 34 (splunk 29, kusto 5)
  • 4688 transforms: cased corpus 313 (splunk 283, kusto 30)
ObjectNameends_with
  • .cpp
  • .cs
Processmatch
  • MSBuild.exe
  • VBCSCompiler.exe
  • dotnet.exe

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
Computersummarize
FileEditAccountsummarize
FileEditParentProcesssummarize
FileEditProcesssummarize
FileEditTimesummarize
FileEditedsummarize
timekeysummarize