Detection rules › Kusto

TI Map File Hash to CommonSecurityLog Event

Severity
medium
Time window
14d
Group by
FileHash, FileHashValue, Id, IndicatorId, ObservableValue
Source
github.com/Azure/Azure-Sentinel

'Identifies a match in CommonSecurityLog Event data from any FileHash IOC from TI'

MITRE ATT&CK coverage

TacticTechniques
Command & Control

Rule body

id: 432996e9-8a93-4407-985f-13707b318a0b
name: TI Map File Hash to CommonSecurityLog Event
description: |
  'Identifies a match in CommonSecurityLog Event data from any FileHash IOC from TI'
severity: Medium
requiredDataConnectors:
  - connectorId: PaloAltoNetworks
    dataTypes:
      - CommonSecurityLog
  - 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;
  let fileHashIndicators = 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)
  | extend IndicatorId = tostring(split(Id, "--")[2])
  | extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
  | where TimeGenerated >= ago(ioc_lookBack)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
  | where IsActive and (ValidUntil > now() or isempty(ValidUntil));
  // Handle matches against both lower case and uppercase versions of the hash:
  (fileHashIndicators | extend  FileHashValue = tolower(FileHashValue)
  | union (fileHashIndicators | extend FileHashValue = toupper(FileHashValue)))
  | 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 (
    CommonSecurityLog | where TimeGenerated >= ago(dt_lookBack)
    | extend CommonSecurityLog_TimeGenerated = TimeGenerated
    )
  on $left.FileHashValue == $right.FileHash
  | where CommonSecurityLog_TimeGenerated < ValidUntil
  | summarize CommonSecurityLog_TimeGenerated = arg_max(CommonSecurityLog_TimeGenerated, *) by IndicatorId, FileHashValue
  | extend Description = tostring(parse_json(Data).description)
  | extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
  | extend Description = tostring(parse_json(Data).description)
  | extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
  | project CommonSecurityLog_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, Confidence,
  SourceIP, SourcePort, DestinationIP, DestinationPort, SourceUserID, SourceUserName, DeviceName, DeviceAction,
  RequestURL, DestinationUserName, DestinationUserID, ApplicationProtocol, Activity, FileHashValue, FileHashType, Url
  | extend HostName = tostring(split(DeviceName, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(DeviceName, '.'), 1, -1), '.'))
  | extend Name = tostring(split(SourceUserName, '@', 0)[0]), UPNSuffix = tostring(split(SourceUserName, '@', 1)[0])
  | extend timestamp = CommonSecurityLog_TimeGenerated
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: SourceUserName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix 
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
  - entityType: FileHash
    fieldMappings:
      - identifier: Value
        columnName: FileHashValue
      - identifier: Algorithm
        columnName: FileHashType
version: 1.3.10
kind: Scheduled

Stages and Predicates

Stage 0: let

let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let fileHashIndicators = ThreatIntelIndicators <inlined as stages below>;

Stage 1: source

ThreatIntelIndicators

Stage 2: extend

| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))

Stage 3: where

| where IndicatorType == "file"

Stage 4: extend (4 consecutive steps)

| extend FileHashType = replace("'", "", substring(ObservableKey, indexof(ObservableKey, "hashes.") + 7, strlen(ObservableKey) - indexof(ObservableKey, "hashes.") - 7))
| extend FileHashValue = toupper(ObservableValue)
| extend IndicatorId = tostring(split(Id, "--")[2])
| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")

Stage 5: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 6: summarize

| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue

Stage 7: where

| where IsActive and (ValidUntil > now() or isempty(ValidUntil))

Stage 8: extend

| extend  FileHashValue = tolower(FileHashValue)

Stage 9: union

union of 1 branches

Stage 10: source

ThreatIntelIndicators

Stage 11: extend

| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))

Stage 12: where

| where IndicatorType == "file"

Stage 13: extend (4 consecutive steps)

| extend FileHashType = replace("'", "", substring(ObservableKey, indexof(ObservableKey, "hashes.") + 7, strlen(ObservableKey) - indexof(ObservableKey, "hashes.") - 7))
| extend FileHashValue = toupper(ObservableValue)
| extend IndicatorId = tostring(split(Id, "--")[2])
| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")

Stage 14: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 15: summarize

| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue

Stage 16: where

| where IsActive and (ValidUntil > now() or isempty(ValidUntil))

Stage 17: extend

| extend FileHashValue = toupper(FileHashValue)

Stage 18: project-reorder

project-reorder

Stage 19: join

| join kind=innerunique (
  CommonSecurityLog | where TimeGenerated >= ago(dt_lookBack)
  | extend CommonSecurityLog_TimeGenerated = TimeGenerated
  )
on $left.FileHashValue == $right.FileHash

Stage 20: where

where CommonSecurityLog_TimeGenerated < ValidUntil

Stage 21: summarize

summarize CommonSecurityLog_TimeGenerated by IndicatorId, FileHashValue

Stage 22: extend (4 consecutive steps)

extend ActivityGroupNames, Description

Stage 23: project

project Activity, ActivityGroupNames, ApplicationProtocol, CommonSecurityLog_TimeGenerated, Confidence, Description, DestinationIP, DestinationPort, DestinationUserID, DestinationUserName, DeviceAction, DeviceName, FileHashType, FileHashValue, Id, RequestURL, SourceIP, SourcePort, SourceUserID, SourceUserName, Url, ValidUntil

Stage 24: extend (3 consecutive steps)

extend DnsDomain, HostName, Name, UPNSuffix, timestamp

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
Activityproject
ActivityGroupNamesproject
ApplicationProtocolproject
CommonSecurityLog_TimeGeneratedproject
Confidenceproject
Descriptionproject
DestinationIPproject
DestinationPortproject
DestinationUserIDproject
DestinationUserNameproject
DeviceActionproject
DeviceNameproject
FileHashTypeproject
FileHashValueproject
Idproject
RequestURLproject
SourceIPproject
SourcePortproject
SourceUserIDproject
SourceUserNameproject
Urlproject
ValidUntilproject
DnsDomainextend
HostNameextend
Nameextend
UPNSuffixextend
timestampextend