Detection rules › Kusto

TI map Email entity to SecurityEvent

Severity
medium
Time window
14d
Group by
EmailSenderAddress, IndicatorId, TargetUserName
Source
github.com/Azure/Azure-Sentinel

'Identifies a match in SecurityEvent table from any Email IOC from TI'

MITRE ATT&CK coverage

TacticTechniques
Initial AccessT1566 Phishing

Rule body kusto

id: 2fc5d810-c9cc-491a-b564-841427ae0e50
name: TI map Email entity to SecurityEvent
description: |
  'Identifies a match in SecurityEvent table from any Email IOC from TI'
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvents
  - connectorId: WindowsForwardedEvents
    dataTypes:
      - WindowsEvent
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  let emailregex = @'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$';
  ThreatIntelligenceIndicator
  //Filtering the table for Email related IOCs
  | where isnotempty(EmailSenderAddress)
  | where TimeGenerated >= ago(ioc_lookBack)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
  | where Active == true and ExpirationDateTime > now()
  // 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) and isnotempty(TargetUserName)
  //Normalizing the column to lower case for exact match with EmailSenderAddress column
  | extend TargetUserName = tolower(TargetUserName)
  // renaming timestamp column so it is clear the log this came from SecurityEvent table
  | extend SecurityEvent_TimeGenerated = TimeGenerated
  ),
  (WindowsEvent
  | where TimeGenerated >= ago(dt_lookBack)
  | extend TargetUserName = tostring(EventData.TargetUserName)
  | where isnotempty(TargetUserName)
  //Normalizing the column to lower case for exact match with EmailSenderAddress column
  | extend TargetUserName = tolower(TargetUserName)
  // renaming timestamp column so it is clear the log this came from SecurityEvent table
  | extend SecurityEvent_TimeGenerated = TimeGenerated
  ))
  )
  on $left.EmailSenderAddress == $right.TargetUserName
  | where SecurityEvent_TimeGenerated < ExpirationDateTime
  | summarize SecurityEvent_TimeGenerated = arg_max(SecurityEvent_TimeGenerated, *) by IndicatorId, TargetUserName
  | project SecurityEvent_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore,
  EmailSenderName, EmailRecipient, EmailSourceDomain, EmailSourceIpAddress, EmailSubject, FileHashValue, FileHashType, Computer, EventID, TargetUserName, Activity, IpAddress, AccountType,
  LogonTypeName, LogonProcessName, Status, SubStatus
  | 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: Name
        columnName: TargetUserName
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain   
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IpAddress
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.3.6
kind: Scheduled

Stages and Predicates

Parameters

let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let emailregex = @'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$';

Stage 1: source

ThreatIntelligenceIndicator

Stage 2: where

| where isnotempty(EmailSenderAddress)

Stage 3: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 4: summarize

| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId

Stage 5: where

| where Active == true and ExpirationDateTime > now()

Stage 6: join

| join kind=innerunique (
(union isfuzzy=true
(SecurityEvent
| where TimeGenerated >= ago(dt_lookBack) and isnotempty(TargetUserName)
| extend TargetUserName = tolower(TargetUserName)
| extend SecurityEvent_TimeGenerated = TimeGenerated
),
(WindowsEvent
| where TimeGenerated >= ago(dt_lookBack)
| extend TargetUserName = tostring(EventData.TargetUserName)
| where isnotempty(TargetUserName)
| extend TargetUserName = tolower(TargetUserName)
| extend SecurityEvent_TimeGenerated = TimeGenerated
))
)
on $left.EmailSenderAddress == $right.TargetUserName

Stage 7: where

| where SecurityEvent_TimeGenerated < ExpirationDateTime

Stage 8: summarize

| summarize SecurityEvent_TimeGenerated = arg_max(SecurityEvent_TimeGenerated, *) by IndicatorId, TargetUserName

Stage 9: project

| project SecurityEvent_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore,
EmailSenderName, EmailRecipient, EmailSourceDomain, EmailSourceIpAddress, EmailSubject, FileHashValue, FileHashType, Computer, EventID, TargetUserName, Activity, IpAddress, AccountType,
LogonTypeName, LogonProcessName, Status, SubStatus

Stage 10: extend

| extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))

Stage 11: extend

| 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.

FieldKindValues
Activeeq
  • true transforms: cased corpus 68 (kusto 68)
EmailSenderAddressis_not_null
  • (no value, null check)
SecurityEvent_TimeGeneratedlt
  • ExpirationDateTime transforms: cased corpus 2 (kusto 2)
TargetUserNameis_not_null
  • (no value, null check)

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
AccountTypeproject
Activityproject
ActivityGroupNamesproject
Computerproject
ConfidenceScoreproject
Descriptionproject
EmailRecipientproject
EmailSenderNameproject
EmailSourceDomainproject
EmailSourceIpAddressproject
EmailSubjectproject
EventIDproject
ExpirationDateTimeproject
FileHashTypeproject
FileHashValueproject
IndicatorIdproject
IpAddressproject
LogonProcessNameproject
LogonTypeNameproject
SecurityEvent_TimeGeneratedproject
Statusproject
SubStatusproject
TargetUserNameproject
ThreatTypeproject
Urlproject
DnsDomainextend
HostNameextend
timestampextend