Detection rules › Kusto
TI map File Hash to Security Event
'Identifies a match in Security Event data from any File Hash IOC from TI'
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Command & Control | T1071 Application Layer Protocol |
Rule body kusto
id: 9f7dc779-1e51-4925-ae4a-db1db933077f
name: TI map File Hash to Security Event
description: |
'Identifies a match in Security Event data from any File Hash IOC from TI'
severity: Medium
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvents
- connectorId: WindowsForwardedEvents
dataTypes:
- WindowsEvent
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelIndicators
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelIndicators
- connectorId: MicrosoftDefenderThreatIntelligence
dataTypes:
- ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071
query: |
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
ThreatIntelIndicators
//extract key part of kv pair
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
| where IndicatorType == "file"
| extend FileHashType = replace("'", "", substring(ObservableKey, indexof(ObservableKey, "hashes.") + 7, strlen(ObservableKey) - indexof(ObservableKey, "hashes.") - 7))
| extend FileHashValue = toupper(ObservableValue)
| where TimeGenerated >= ago(ioc_lookBack)
| extend FileHashValue = toupper(FileHashValue)
| extend IndicatorId = tostring(split(Id, "--")[2])
| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
| project-reorder *, FileHashType, FileHashValue, Type
// using innerunique to keep perf fast and result set low, we only need one match to indicate potential malicious activity that needs to be investigated
| join kind=innerunique ( union isfuzzy=true
(SecurityEvent | where TimeGenerated >= ago(dt_lookBack)
| where EventID in ("8003","8002","8005")
| extend SecurityEvent_TimeGenerated = TimeGenerated, Event = EventID, FileHash = toupper(FileHash)
),
(WindowsEvent | where TimeGenerated >= ago(dt_lookBack)
| where EventID in ("8003","8002","8005")
| extend SecurityEvent_TimeGenerated = TimeGenerated, Event = EventID, FileHash = toupper(EventData.FileHash)
)
)
on $left.FileHashValue == $right.FileHash
| where SecurityEvent_TimeGenerated < ValidUntil
| extend Description = tostring(parse_json(Data).description)
| extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
| summarize SecurityEvent_TimeGenerated = arg_max(SecurityEvent_TimeGenerated, *) by IndicatorId, FileHash
| project SecurityEvent_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ValidUntil, Confidence,
Process, FileHash, Computer, Account, Event, FileHashValue, FileHashType, Url
| extend NTDomain = tostring(split(Account, '\\', 0)[0]), Name = tostring(split(Account, '\\', 1)[0])
| extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
| extend timestamp = SecurityEvent_TimeGenerated
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
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
- entityType: FileHash
fieldMappings:
- identifier: Value
columnName: FileHashValue
- identifier: Algorithm
columnName: FileHashType
version: 1.4.9
kind: Scheduled
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
Stage 1: source
ThreatIntelIndicators
Stage 2: extend
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
Stage 3: where
| where IndicatorType == "file"
Stage 4: extend
| extend FileHashType = replace("'", "", substring(ObservableKey, indexof(ObservableKey, "hashes.") + 7, strlen(ObservableKey) - indexof(ObservableKey, "hashes.") - 7))
Stage 5: extend
| extend FileHashValue = toupper(ObservableValue)
Stage 6: where
| where TimeGenerated >= ago(ioc_lookBack)
Stage 7: extend (3 consecutive steps)
| extend FileHashValue = toupper(FileHashValue)
| extend IndicatorId = tostring(split(Id, "--")[2])
| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
Stage 8: summarize
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
Stage 9: where
| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
Stage 10: project-reorder
| project-reorder *, FileHashType, FileHashValue, Type
Stage 11: join
| join kind=innerunique ( union isfuzzy=true
(SecurityEvent | where TimeGenerated >= ago(dt_lookBack)
| where EventID in ("8003","8002","8005")
| extend SecurityEvent_TimeGenerated = TimeGenerated, Event = EventID, FileHash = toupper(FileHash)
),
(WindowsEvent | where TimeGenerated >= ago(dt_lookBack)
| where EventID in ("8003","8002","8005")
| extend SecurityEvent_TimeGenerated = TimeGenerated, Event = EventID, FileHash = toupper(EventData.FileHash)
)
)
on $left.FileHashValue == $right.FileHash
Stage 12: where
| where SecurityEvent_TimeGenerated < ValidUntil
Stage 13: extend
| extend Description = tostring(parse_json(Data).description)
Stage 14: extend
| extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
Stage 15: summarize
| summarize SecurityEvent_TimeGenerated = arg_max(SecurityEvent_TimeGenerated, *) by IndicatorId, FileHash
Stage 16: project
| project SecurityEvent_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ValidUntil, Confidence,
Process, FileHash, Computer, Account, Event, FileHashValue, FileHashType, Url
Stage 17: extend (3 consecutive steps)
| extend NTDomain = tostring(split(Account, '\\', 0)[0]), Name = tostring(split(Account, '\\', 1)[0])
| extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
| extend timestamp = SecurityEvent_TimeGenerated
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.
| Field | Kind | Values |
|---|---|---|
EventID | in |
|
IndicatorType | eq |
|
SecurityEvent_TimeGenerated | lt |
|
ValidUntil | is_null |
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.
| Field | Source |
|---|---|
Account | project |
ActivityGroupNames | project |
Computer | project |
Confidence | project |
Description | project |
Event | project |
FileHash | project |
FileHashType | project |
FileHashValue | project |
IndicatorId | project |
Process | project |
SecurityEvent_TimeGenerated | project |
Url | project |
ValidUntil | project |
NTDomain | extend |
Name | extend |
DnsDomain | extend |
HostName | extend |
timestamp | extend |