Detection rules › Kusto

ThreatConnect TI map Email entity to SigninLogs

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

Identifies a match in SigninLogs table from any Email IOC from ThreatConnect TI

MITRE ATT&CK coverage

TacticTechniques
Command & Control

Telemetry coverage

Rule body

id: ecb68ce7-c309-59a7-a8de-07ccf2a0ea4f
name: ThreatConnect TI map Email entity to SigninLogs
version: 1.2.5
kind: Scheduled
description: |
  'Identifies a match in SigninLogs table from any Email IOC from ThreatConnect TI'
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  let emailregex = @'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$';
  let aadFunc = (tableName:string){
  ThreatIntelligenceIndicator
    | where TimeGenerated >= ago(ioc_lookBack)
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
    | where ExpirationDateTime > now() and Active == true
  // Filter out non ThreatConnect TI Sources
  | where SourceSystem startswith "ThreatConnect-"
  //Filtering the table for Email related IOCs
  | where isnotempty(EmailSenderAddress)
  // 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 (
      table(tableName) | where TimeGenerated >= ago(dt_lookBack) and isnotempty(UserPrincipalName)
      //Normalizing the column to lower case for exact match with EmailSenderAddress column
      | extend UserPrincipalName = tolower(UserPrincipalName)
      | where UserPrincipalName matches regex emailregex
      | extend Status = todynamic(DeviceDetail), LocationDetails = todynamic(LocationDetails)
      | extend StatusCode = tostring(Status.errorCode), StatusDetails = tostring(Status.additionalDetails)
      | extend State = tostring(LocationDetails.state), City = tostring(LocationDetails.city), Region = tostring(LocationDetails.countryOrRegion)
      // renaming timestamp column so it is clear the log this came from SigninLogs table
      | extend SigninLogs_TimeGenerated = TimeGenerated, Type = Type
  )
  on $left.EmailSenderAddress == $right.UserPrincipalName
  | where SigninLogs_TimeGenerated < ExpirationDateTime
  | summarize SigninLogs_TimeGenerated = arg_max(SigninLogs_TimeGenerated, *) by IndicatorId, UserPrincipalName
  | project SigninLogs_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore,
  EmailSenderName, EmailRecipient, EmailSourceDomain, EmailSourceIpAddress, EmailSubject, FileHashValue, FileHashType, IPAddress, UserPrincipalName, AppDisplayName,
  StatusCode, StatusDetails, NetworkIP, NetworkDestinationIP, NetworkSourceIP, Type
  | extend Name = tostring(split(UserPrincipalName, '@', 0)[0]), UPNSuffix = tostring(split(UserPrincipalName, '@', 1)[0])
  | extend timestamp = SigninLogs_TimeGenerated
  };
  let aadSignin = aadFunc("SigninLogs");
  let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
  union isfuzzy=true aadSignin, aadNonInt
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix 
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url

Stages and Predicates

Stage 0: let

let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let emailregex = @'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$';
let aadFunc = (tableName:string){
ThreatIntelligenceIndicator
  | where TimeGenerated >= ago(ioc_lookBack)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
  | where ExpirationDateTime > now() and Active == true
| where SourceSystem startswith "ThreatConnect-"
| where isnotempty(EmailSenderAddress)
| join kind=innerunique (
    table(tableName) | where TimeGenerated >= ago(dt_lookBack) and isnotempty(UserPrincipalName)
    | extend UserPrincipalName = tolower(UserPrincipalName)
    | where UserPrincipalName matches regex emailregex
    | extend Status = todynamic(DeviceDetail), LocationDetails = todynamic(LocationDetails)
    | extend StatusCode = tostring(Status.errorCode), StatusDetails = tostring(Status.additionalDetails)
    | extend State = tostring(LocationDetails.state), City = tostring(LocationDetails.city), Region = tostring(LocationDetails.countryOrRegion)
    | extend SigninLogs_TimeGenerated = TimeGenerated, Type = Type
)
on $left.EmailSenderAddress == $right.UserPrincipalName
| where SigninLogs_TimeGenerated < ExpirationDateTime
| summarize SigninLogs_TimeGenerated = arg_max(SigninLogs_TimeGenerated, *) by IndicatorId, UserPrincipalName
| project SigninLogs_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore,
EmailSenderName, EmailRecipient, EmailSourceDomain, EmailSourceIpAddress, EmailSubject, FileHashValue, FileHashType, IPAddress, UserPrincipalName, AppDisplayName,
StatusCode, StatusDetails, NetworkIP, NetworkDestinationIP, NetworkSourceIP, Type
| extend Name = tostring(split(UserPrincipalName, '@', 0)[0]), UPNSuffix = tostring(split(UserPrincipalName, '@', 1)[0])
| extend timestamp = SigninLogs_TimeGenerated
};
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");

Stage 1: union

union of 2 branches

Stage 2: source

ThreatIntelligenceIndicator

Stage 3: where

where TimeGenerated >= ago(1209600s)

Stage 4: summarize

summarize LatestIndicatorTime by IndicatorId

Stage 5: where

where Active =~ true

Stage 6: where

where SourceSystem startswith "ThreatConnect-"

Stage 7: where

where isnotempty(EmailSenderAddress)

Stage 8: join

join kind=innerunique (...) on EmailSenderAddress, UserPrincipalName

Stage 9: where

where SigninLogs_TimeGenerated < ExpirationDateTime

Stage 10: summarize

summarize SigninLogs_TimeGenerated by IndicatorId, UserPrincipalName

Stage 11: project

project ActivityGroupNames, AppDisplayName, ConfidenceScore, Description, EmailRecipient, EmailSenderName, EmailSourceDomain, EmailSourceIpAddress, EmailSubject, ExpirationDateTime, FileHashType, FileHashValue, IPAddress, IndicatorId, NetworkDestinationIP, NetworkIP, NetworkSourceIP, SigninLogs_TimeGenerated, StatusCode, StatusDetails, ThreatType, Type, Url, UserPrincipalName

Stage 12: extend

extend Name, UPNSuffix

Stage 13: extend

extend timestamp

Stage 14: source

ThreatIntelligenceIndicator

Stage 15: where

where TimeGenerated >= ago(1209600s)

Stage 16: summarize

summarize LatestIndicatorTime by IndicatorId

Stage 17: where

where Active =~ true

Stage 18: where

where SourceSystem startswith "ThreatConnect-"

Stage 19: where

where isnotempty(EmailSenderAddress)

Stage 20: join

join kind=innerunique (...) on EmailSenderAddress, UserPrincipalName

Stage 21: where

where SigninLogs_TimeGenerated < ExpirationDateTime

Stage 22: summarize

summarize SigninLogs_TimeGenerated by IndicatorId, UserPrincipalName

Stage 23: project

project ActivityGroupNames, AppDisplayName, ConfidenceScore, Description, EmailRecipient, EmailSenderName, EmailSourceDomain, EmailSourceIpAddress, EmailSubject, ExpirationDateTime, FileHashType, FileHashValue, IPAddress, IndicatorId, NetworkDestinationIP, NetworkIP, NetworkSourceIP, SigninLogs_TimeGenerated, StatusCode, StatusDetails, ThreatType, Type, Url, UserPrincipalName

Stage 24: extend

extend Name, UPNSuffix

Stage 25: extend

extend timestamp

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
AppDisplayNameproject
ConfidenceScoreproject
Descriptionproject
EmailRecipientproject
EmailSenderNameproject
EmailSourceDomainproject
EmailSourceIpAddressproject
EmailSubjectproject
ExpirationDateTimeproject
FileHashTypeproject
FileHashValueproject
IPAddressproject
IndicatorIdproject
NetworkDestinationIPproject
NetworkIPproject
NetworkSourceIPproject
SigninLogs_TimeGeneratedproject
StatusCodeproject
StatusDetailsproject
ThreatTypeproject
Typeproject
Urlproject
UserPrincipalNameproject
Nameextend
UPNSuffixextend
timestampextend