Detection rules › Kusto

Process Execution Frequency Anomaly

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

This detection identifies anomalous spike in frequency of executions of sensitive processes which are often leveraged as attack vectors. The query leverages KQL's built-in anomaly detection algorithms to find large deviations from baseline patterns. Sudden increases in execution frequency of sensitive processes should be further investigated for malicious activity. Tune the values from 1.5 to 3 in series_decompose_anomalies for further outliers or based on custom threshold values for score.

MITRE ATT&CK coverage

Telemetry coverage

Rule body

id: 2c55fe7a-b06f-4029-a5b9-c54a2320d7b8
name: Process Execution Frequency Anomaly
description: |
  'This detection identifies anomalous spike in frequency of executions of sensitive processes which are often leveraged as attack vectors.
  The query leverages KQL's built-in anomaly detection algorithms to find large deviations from baseline patterns.
  Sudden increases in execution frequency of sensitive processes should be further investigated for malicious activity.
  Tune the values from 1.5 to 3 in series_decompose_anomalies for further outliers or based on custom threshold values for score.'
severity: Medium
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Execution
relevantTechniques:
  - T1059
query: |
  let starttime = 14d;
  let endtime = 1d;
  let timeframe = 1h;
  let TotalEventsThreshold = 5;
  // Configure the list with sensitive process names 
  let ExeList = dynamic(["powershell.exe","cmd.exe","wmic.exe","psexec.exe","cacls.exe","rundll32.exe"]);
  let TimeSeriesData =
  SecurityEvent
  | where EventID == 4688 | extend Process = tolower(Process)
  | where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
  | where Process in~ (ExeList)
  | project TimeGenerated, Computer, AccountType, Account, Process
  | make-series Total=count() on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by Process;
  let TimeSeriesAlerts = materialize(TimeSeriesData
  | extend (anomalies, score, baseline) = series_decompose_anomalies(Total, 1.5, -1, 'linefit')
  | mv-expand Total to typeof(double), TimeGenerated to typeof(datetime), anomalies to typeof(double), score to typeof(double), baseline to typeof(long)
  | where anomalies > 0
  | project Process, TimeGenerated, Total, baseline, anomalies, score
  | where Total > TotalEventsThreshold);
  let AnomalyHours = materialize(TimeSeriesAlerts  | where TimeGenerated > ago(2d) | project TimeGenerated);
  TimeSeriesAlerts
  | where TimeGenerated > ago(2d)
  | join (
  SecurityEvent
  | where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
  | extend DateHour = bin(TimeGenerated, 1h) // create a new column and round to hour
  | where DateHour in ((AnomalyHours)) //filter the dataset to only selected anomaly hours
  | where EventID == 4688 | extend Process = tolower(Process)
  | summarize CommandlineCount = count() by bin(TimeGenerated, 1h), Process, CommandLine, Computer, Account
  ) on Process, TimeGenerated
  | project AnomalyHour = TimeGenerated, Computer, Account, Process, CommandLine, CommandlineCount, Total, baseline, anomalies, score
  | extend timestamp = AnomalyHour, NTDomain = split(Account, '\\', 0)[0], Name = split(Account, '\\', 1)[0], HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
      - identifier: Name
        columnName: Name
      - identifier: NTDomain
        columnName: NTDomain
  - entityType: Host
    fieldMappings: 
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
version: 1.0.6
kind: Scheduled

Stages and Predicates

Parameters

let starttime = 14d;
let endtime = 1d;
let timeframe = 1h;
let TotalEventsThreshold = 5;
let ExeList = dynamic(["powershell.exe","cmd.exe","wmic.exe","psexec.exe","cacls.exe","rundll32.exe"]);

let TimeSeriesData and let TimeSeriesAlerts are inlined into the numbered stages below.

Let binding: AnomalyHours used in Stages 3, 17

let AnomalyHours = materialize(TimeSeriesAlerts  | where TimeGenerated > ago(2d) | project TimeGenerated);

Stages 1 to 15 define let TimeSeriesAlerts (the rule's main pipeline source); stages 16 to 19 run on it.

Stage 1: source

let TimeSeriesData

Stage 2: source

let TimeSeriesAlerts

Stage 3: source

let AnomalyHours

Stage 4: source

SecurityEvent

Stage 5: where

| where EventID == 4688

Stage 6: extend

| extend Process = tolower(Process)

Stage 7: where

| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))

Stage 8: where

| where Process in~ (ExeList)

Stage 9: project

| project TimeGenerated, Computer, AccountType, Account, Process

The stages below score time-series anomalies (make-series, series_decompose_anomalies).

Stage 10: make-series

| make-series Total=count() on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by Process

Stage 11: extend

| extend (anomalies, score, baseline) = series_decompose_anomalies(Total, 1.5, -1, 'linefit')

Stage 12: mv-expand

| mv-expand Total to typeof(double), TimeGenerated to typeof(datetime), anomalies to typeof(double), score to typeof(double), baseline to typeof(long)

Stage 13: where

| where anomalies > 0

Stage 14: project

| project Process, TimeGenerated, Total, baseline, anomalies, score

Stage 15: where

| where Total > TotalEventsThreshold

Stage 16: where

TimeSeriesAlerts
| where TimeGenerated > ago(2d)

Stage 17: join

| join (
SecurityEvent
| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
| extend DateHour = bin(TimeGenerated, 1h)
| where DateHour in ((AnomalyHours))
| where EventID == 4688 | extend Process = tolower(Process)
| summarize CommandlineCount = count() by bin(TimeGenerated, 1h), Process, CommandLine, Computer, Account
) on Process, TimeGenerated

Stage 18: project

| project AnomalyHour = TimeGenerated, Computer, Account, Process, CommandLine, CommandlineCount, Total, baseline, anomalies, score

Stage 19: extend

| extend timestamp = AnomalyHour, NTDomain = split(Account, '\\', 0)[0], Name = split(Account, '\\', 1)[0], HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventIDeq
  • 4688 corpus 317 (splunk 283, kusto 33, elastic 1)
field:"EventID" kind:eq value:"4688"
Processin
  • cacls.exe
  • cmd.exe
  • powershell.exe
  • psexec.exe
  • rundll32.exe
  • wmic.exe
field:"Process" kind:in
Totalgt
  • 5 corpus 2 (kusto 2)
field:"Total" kind:gt value:"5"
anomaliesgt
  • 0 corpus 12 (kusto 12)
field:"anomalies" kind:gt value:"0"

Output fields

These fields are emitted when the rule matches.

FieldSource
Accountproject
AnomalyHourproject
CommandLineproject
CommandlineCountproject
Computerproject
Processproject
Totalproject
anomaliesproject
baselineproject
scoreproject
DnsDomainextend
HostNameextend
NTDomainextend
Nameextend
timestampextend