Detection rules › Kusto

Red Sift - New email with URL from previously unseen sender

Status
available
Severity
medium
Time window
14d
Group by
EmailFrom
Source
github.com/Azure/Azure-Sentinel

'Detects email forensics events that contain one or more URLs where the sender in the from field has not been seen in the previous 14 days, which may indicate phishing activity or a newly observed sender.'

MITRE ATT&CK coverage

TacticTechniques
Initial AccessT1566 Phishing

Rule body kusto

id: 6e0b70d4-0ab8-480e-9707-8ad45fc21a65
name: Red Sift - New email with URL from previously unseen sender
description: |
  'Detects email forensics events that contain one or more URLs where the sender in the from field has not been seen in the previous 14 days, which may indicate phishing activity or a newly observed sender.'
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 historicalSenders = RedSiftEmailForensics_CL
  | extend
      EmailFrom = tostring(column_ifexists("EmailFrom", ""))
  | where TimeGenerated between (ago(lookback) .. ago(recentWindow))
  | where isnotempty(EmailFrom)
  | summarize by EmailFrom;
  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(column_ifexists("EmailUrls", "[]"))
  | where TimeGenerated >= ago(recentWindow)
  | where isnotempty(EmailFrom)
  | extend UrlCount = array_length(EmailUrls)
  | where UrlCount > 0
  | mv-apply Url = EmailUrls on (
      summarize UrlSet = make_set(tostring(Url.url_string), 50)
    )
  | extend UrlList = strcat_array(UrlSet, ", ")
  | join kind=leftanti (historicalSenders) on EmailFrom
  | project
      TimeGenerated,
      EmailFrom,
      EmailSubject,
      EmailReturnPath,
      EmailMessageUid,
      SrcIp,
      DstHostname,
      UrlCount,
      UrlList,
      Severity,
      Message,
      CorrelationUid
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: EmailFrom
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIp
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: DstHostname
customDetails:
  EmailSubject: EmailSubject
  ReturnPath: EmailReturnPath
  UrlCount: UrlCount
  UrlList: UrlList
  CorrelationUid: CorrelationUid
alertDetailsOverride:
  alertDisplayNameFormat: "RedSift - New URL-bearing sender {{EmailFrom}}"
  alertDescriptionFormat: "Email from previously unseen sender {{EmailFrom}} contains {{UrlCount}} URL(s)."
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: Selected
    groupByEntities:
      - Account
    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: historicalSenders

let historicalSenders = RedSiftEmailForensics_CL
| extend
    EmailFrom = tostring(column_ifexists("EmailFrom", ""))
| where TimeGenerated between (ago(lookback) .. ago(recentWindow))
| where isnotempty(EmailFrom)
| summarize by EmailFrom;

Derived from lookback, recentWindow.

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(column_ifexists("EmailUrls", "[]"))

Stage 3: where

| where TimeGenerated >= ago(recentWindow)

Stage 4: where

| where isnotempty(EmailFrom)

Stage 5: extend

| extend UrlCount = array_length(EmailUrls)

Stage 6: where

| where UrlCount > 0

Stage 7: kusto:mv-apply

| mv-apply Url = EmailUrls on (
    summarize UrlSet = make_set(tostring(Url.url_string), 50)
  )

Stage 8: extend

| extend UrlList = strcat_array(UrlSet, ", ")

Stage 9: join (negated)

| join kind=leftanti (historicalSenders) on EmailFrom

Stage 10: project

| project
    TimeGenerated,
    EmailFrom,
    EmailSubject,
    EmailReturnPath,
    EmailMessageUid,
    SrcIp,
    DstHostname,
    UrlCount,
    UrlList,
    Severity,
    Message,
    CorrelationUid

Stage 11: summarize

summarize

Stage 12: summarize

summarize by EmailFrom

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

FieldKindExcluded values
EmailFromis_not_null(no value, null check)

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
EmailFromis_not_null
  • (no value, null check)
UrlCountgt
  • 0 transforms: cased

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
EmailFromsummarize