Detection rules › Kusto

TI map Email entity to OfficeActivity

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

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

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Rule body

id: 4a3f5ed7-8da5-4ce2-af6f-c9ada45060f2
name: TI map Email entity to OfficeActivity
description: |
  'Identifies a match in OfficeActivity table from any Email IOC from TI'
severity: Medium
requiredDataConnectors:
  - connectorId: Office365
    dataTypes:
      - OfficeActivity
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - 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-.]+$';
  let OfficeEvents = materialize(
    OfficeActivity
    | where isnotempty(UserId)
    | where TimeGenerated >= ago(dt_lookBack)
    | where UserId matches regex emailregex
    | project-rename  OfficeActivity_TimeGenerated = TimeGenerated);
  let OfficeActivityUPNs = OfficeEvents | distinct UserId = tolower(UserId) | summarize make_list(UserId);
  ThreatIntelligenceIndicator
  | where isnotempty(EmailSenderAddress)
  | where TimeGenerated >= ago(ioc_lookBack)
  | where tolower(EmailSenderAddress) in (OfficeActivityUPNs)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
  | where Active == true and ExpirationDateTime > now()
  | where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"
  // 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 (OfficeEvents) on $left.EmailSenderAddress == $right.UserId
  | where OfficeActivity_TimeGenerated < ExpirationDateTime
  | summarize OfficeActivity_TimeGenerated = arg_max(OfficeActivity_TimeGenerated, *) by IndicatorId, UserId
  | project OfficeActivity_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore, EmailSenderName, EmailRecipient, EmailSourceDomain, EmailSourceIpAddress, EmailSubject, FileHashValue, FileHashType, UserId, ClientIP, Operation, UserType, RecordType, OfficeWorkload, Parameters
  | extend Name = tostring(split(UserId, '@', 0)[0]), UPNSuffix = tostring(split(UserId, '@', 1)[0])
  | extend timestamp = OfficeActivity_TimeGenerated
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserId
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIP
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.2.7
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-.]+$';

Let binding: OfficeEvents used in Stage 8

let OfficeEvents = materialize(
  OfficeActivity
  | where isnotempty(UserId)
  | where TimeGenerated >= ago(dt_lookBack)
  | where UserId matches regex emailregex
  | project-rename  OfficeActivity_TimeGenerated = TimeGenerated);

Let binding: OfficeActivityUPNs used in Stage 4

let OfficeActivityUPNs = OfficeEvents | distinct UserId = tolower(UserId) | summarize make_list(UserId);

Stage 1: source

ThreatIntelligenceIndicator

Stage 2: where

| where isnotempty(EmailSenderAddress)

Stage 3: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 4: where

| where tolower(EmailSenderAddress) in (OfficeActivityUPNs)

Stage 5: summarize

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

Stage 6: where

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

Stage 7: where

| where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"

Stage 8: join

| join kind=innerunique (OfficeEvents) on $left.EmailSenderAddress == $right.UserId

Stage 9: where

| where OfficeActivity_TimeGenerated < ExpirationDateTime

Stage 10: summarize

| summarize OfficeActivity_TimeGenerated = arg_max(OfficeActivity_TimeGenerated, *) by IndicatorId, UserId

Stage 11: project

| project OfficeActivity_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore, EmailSenderName, EmailRecipient, EmailSourceDomain, EmailSourceIpAddress, EmailSubject, FileHashValue, FileHashType, UserId, ClientIP, Operation, UserType, RecordType, OfficeWorkload, Parameters

Stage 12: extend

| extend Name = tostring(split(UserId, '@', 0)[0]), UPNSuffix = tostring(split(UserId, '@', 1)[0])

Stage 13: extend

| extend timestamp = OfficeActivity_TimeGenerated

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DescriptioncontainsState: falsepos;excludes:Description field:"Description" value:"State: falsepos;"
DescriptioncontainsState: inactive;excludes:Description field:"Description" value:"State: inactive;"

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
ClientIPproject
ConfidenceScoreproject
Descriptionproject
EmailRecipientproject
EmailSenderNameproject
EmailSourceDomainproject
EmailSourceIpAddressproject
EmailSubjectproject
ExpirationDateTimeproject
FileHashTypeproject
FileHashValueproject
IndicatorIdproject
OfficeActivity_TimeGeneratedproject
OfficeWorkloadproject
Operationproject
Parametersproject
RecordTypeproject
ThreatTypeproject
Urlproject
UserIdproject
UserTypeproject
Nameextend
UPNSuffixextend
timestampextend