Detection rules › Kusto

Red Sift - Email with URL to previously unseen domain

Status
available
Severity
medium
Time window
14d
Group by
CorrelationUid, DstHostname, EmailFrom, EmailMessageUid, EmailReturnPath, EmailSubject, Message, Severity, SrcIp, TimeGenerated, UrlDomain
Source
github.com/Azure/Azure-Sentinel

'Detects email forensics events containing one or more URLs whose domain has not been seen in the previous 14 days, which may indicate newly observed phishing infrastructure or suspicious delivery patterns.'

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Rule body

id: 8972b513-12a2-4b46-8263-3f091d88a8bc
name: Red Sift - Email with URL to previously unseen domain
description: |
  'Detects email forensics events containing one or more URLs whose domain has not been seen in the previous 14 days, which may indicate newly observed phishing infrastructure or suspicious delivery patterns.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: RedSiftPush
    dataTypes:
      - RedSiftEmailForensics_CL
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566
query: |
  let lookback = 14d;
  let recentWindow = 1h;
  let historicalDomains = RedSiftEmailForensics_CL
  | extend EmailUrls = todynamic(EmailUrls)
  | where TimeGenerated between (ago(lookback) .. ago(recentWindow))
  | where isnotempty(EmailUrls) and array_length(EmailUrls) > 0
  | mv-expand Url = EmailUrls
  | extend UrlString = tostring(Url.url_string)
  | where isnotempty(UrlString)
  | extend UrlDomain = tostring(parse_url(UrlString).Host)
  | where isnotempty(UrlDomain)
  | summarize by UrlDomain;
  RedSiftEmailForensics_CL
  | extend
      EmailFrom = tostring(column_ifexists("EmailFrom", "")),
      EmailSubject = tostring(column_ifexists("EmailSubject", "")),
      EmailReturnPath = tostring(column_ifexists("EmailReturnPath", "")),
      EmailMessageUid = tostring(column_ifexists("EmailMessageUid", "")),
      SrcIp = tostring(column_ifexists("SrcIp", "")),
      DstHostname = tostring(column_ifexists("DstHostname", "")),
      Severity = tostring(column_ifexists("Severity", "")),
      Message = tostring(column_ifexists("Message", "")),
      CorrelationUid = tostring(column_ifexists("CorrelationUid", "")),
      EmailUrls = todynamic(EmailUrls)
  | where TimeGenerated >= ago(recentWindow)
  | where isnotempty(EmailUrls) and array_length(EmailUrls) > 0
  | mv-expand Url = EmailUrls
  | extend UrlString = tostring(Url.url_string)
  | where isnotempty(UrlString)
  | extend UrlDomain = tostring(parse_url(UrlString).Host)
  | where isnotempty(UrlDomain)
  | join kind=leftanti (historicalDomains) on UrlDomain
  | summarize
      NewUrlDomains = make_set(UrlDomain, 50),
      NewUrls = make_set(UrlString, 50),
      NewDomainCount = dcount(UrlDomain),
      UrlCount = dcount(UrlString),
      RepresentativeUrlDomain = take_any(UrlDomain)
      by TimeGenerated,
      EmailFrom,
      EmailSubject,
      EmailReturnPath,
      EmailMessageUid,
      SrcIp,
      DstHostname,
      Severity,
      Message,
      CorrelationUid
  | extend
      NewUrlDomainList = strcat_array(NewUrlDomains, ", "),
      NewUrlList = strcat_array(NewUrls, ", ")
  | project
      TimeGenerated,
      EmailFrom,
      EmailSubject,
      EmailReturnPath,
      EmailMessageUid,
      SrcIp,
      DstHostname,
      RepresentativeUrlDomain,
      NewDomainCount,
      UrlCount,
      NewUrlDomainList,
      NewUrlList,
      Severity,
      Message,
      CorrelationUid
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: EmailFrom
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIp
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: RepresentativeUrlDomain
customDetails:
  EmailSubject: EmailSubject
  ReturnPath: EmailReturnPath
  NewDomainCount: NewDomainCount
  NewUrlDomainList: NewUrlDomainList
  NewUrlList: NewUrlList
  CorrelationUid: CorrelationUid
alertDetailsOverride:
  alertDisplayNameFormat: "RedSift - URL to new domain in email from {{EmailFrom}}"
  alertDescriptionFormat: "Email from {{EmailFrom}} contains {{NewDomainCount}} previously unseen URL domain(s): {{NewUrlDomainList}}."
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: Selected
    groupByEntities:
      - Account
      - DNS
    groupByCustomDetails:
      - EmailSubject
eventGroupingSettings:
  aggregationKind: AlertPerResult
suppressionEnabled: false
suppressionDuration: PT1H
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let lookback = 14d;
let recentWindow = 1h;

Let binding: historicalDomains used in Stage 10

let historicalDomains = RedSiftEmailForensics_CL
| extend EmailUrls = todynamic(EmailUrls)
| where TimeGenerated between (ago(lookback) .. ago(recentWindow))
| where isnotempty(EmailUrls) and array_length(EmailUrls) > 0
| mv-expand Url = EmailUrls
| extend UrlString = tostring(Url.url_string)
| where isnotempty(UrlString)
| extend UrlDomain = tostring(parse_url(UrlString).Host)
| where isnotempty(UrlDomain)
| summarize by UrlDomain;

Stage 1: source

RedSiftEmailForensics_CL

Stage 2: extend

| extend
    EmailFrom = tostring(column_ifexists("EmailFrom", "")),
    EmailSubject = tostring(column_ifexists("EmailSubject", "")),
    EmailReturnPath = tostring(column_ifexists("EmailReturnPath", "")),
    EmailMessageUid = tostring(column_ifexists("EmailMessageUid", "")),
    SrcIp = tostring(column_ifexists("SrcIp", "")),
    DstHostname = tostring(column_ifexists("DstHostname", "")),
    Severity = tostring(column_ifexists("Severity", "")),
    Message = tostring(column_ifexists("Message", "")),
    CorrelationUid = tostring(column_ifexists("CorrelationUid", "")),
    EmailUrls = todynamic(EmailUrls)

Stage 3: where

| where TimeGenerated >= ago(recentWindow)

Stage 4: where

| where isnotempty(EmailUrls) and array_length(EmailUrls) > 0

Stage 5: mv-expand

| mv-expand Url = EmailUrls

Stage 6: extend

| extend UrlString = tostring(Url.url_string)

Stage 7: where

| where isnotempty(UrlString)

Stage 8: extend

| extend UrlDomain = tostring(parse_url(UrlString).Host)

Stage 9: where

| where isnotempty(UrlDomain)

Stage 10: join (negated)

| join kind=leftanti (historicalDomains) on UrlDomain

Stage 11: summarize

| summarize
    NewUrlDomains = make_set(UrlDomain, 50),
    NewUrls = make_set(UrlString, 50),
    NewDomainCount = dcount(UrlDomain),
    UrlCount = dcount(UrlString),
    RepresentativeUrlDomain = take_any(UrlDomain)
    by TimeGenerated,
    EmailFrom,
    EmailSubject,
    EmailReturnPath,
    EmailMessageUid,
    SrcIp,
    DstHostname,
    Severity,
    Message,
    CorrelationUid

Stage 12: extend

| extend
    NewUrlDomainList = strcat_array(NewUrlDomains, ", "),
    NewUrlList = strcat_array(NewUrls, ", ")

Stage 13: project

| project
    TimeGenerated,
    EmailFrom,
    EmailSubject,
    EmailReturnPath,
    EmailMessageUid,
    SrcIp,
    DstHostname,
    RepresentativeUrlDomain,
    NewDomainCount,
    UrlCount,
    NewUrlDomainList,
    NewUrlList,
    Severity,
    Message,
    CorrelationUid

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
EmailUrlsgt0excludes:EmailUrls field:"EmailUrls" value:"0"
EmailUrlsis_not_null(no value, null check)excludes:EmailUrls
UrlDomainis_not_null(no value, null check)excludes:UrlDomain
UrlStringis_not_null(no value, null check)excludes:UrlString

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EmailUrlsis_not_null
  • (no value, null check)
field:"EmailUrls" kind:is_not_null
UrlDomainis_not_null
  • (no value, null check)
field:"UrlDomain" kind:is_not_null
UrlStringis_not_null
  • (no value, null check)
field:"UrlString" kind:is_not_null

Output fields

These fields are emitted when the rule matches.

FieldSource
CorrelationUidproject
DstHostnameproject
EmailFromproject
EmailMessageUidproject
EmailReturnPathproject
EmailSubjectproject
Messageproject
NewDomainCountproject
NewUrlDomainListproject
NewUrlListproject
RepresentativeUrlDomainproject
Severityproject
SrcIpproject
TimeGeneratedproject
UrlCountproject