Detection rules › Kusto
Process Execution Frequency Anomaly
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
| Tactic | Techniques |
|---|---|
| Execution |
Telemetry coverage
| Provider | Record / event type |
|---|---|
| Security-Auditing | Event ID 4688: A new process has been created. |
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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
EventID | eq |
| field:"EventID" kind:eq value:"4688" |
Process | in |
| field:"Process" kind:in |
Total | gt |
| field:"Total" kind:gt value:"5" |
anomalies | gt |
| field:"anomalies" kind:gt value:"0" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Account | project |
AnomalyHour | project |
CommandLine | project |
CommandlineCount | project |
Computer | project |
Process | project |
Total | project |
anomalies | project |
baseline | project |
score | project |
DnsDomain | extend |
HostName | extend |
NTDomain | extend |
Name | extend |
timestamp | extend |