Detection rules › Kusto

TI Map URL Entity to UrlClickEvents

Severity
medium
Time window
14d
Group by
IndicatorId, ReportId, TI_Url, Url, UrlChain
Source
github.com/Azure/Azure-Sentinel

This query identifies any URL indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in UrlClickEvents.

MITRE ATT&CK coverage

TacticTechniques
Command & Control

Telemetry coverage

Rule body

id: 23391c84-87d8-452f-a84c-47a62f01e115
name: TI Map URL Entity to UrlClickEvents
description: |
  'This query identifies any URL indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in UrlClickEvents.'
severity: Medium
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - UrlClickEvents
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - 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 UrlClickEvents_ = materialize(UrlClickEvents
      | where TimeGenerated >= ago(dt_lookBack)
      | extend UrlClickEvents_TimeGenerated = TimeGenerated);
  let ChainReportID = UrlClickEvents_
      | mv-expand todynamic(UrlChain)
      | extend UrlChain = tolower(UrlChain)
      | project ReportId, Url, UrlChain;
  // Url is not always in UrlChain, so we need to check both
  let ClickedUrls = 
    (union isfuzzy=false (ChainReportID), (ChainReportID | project Url = UrlChain))
    | distinct Url
    | summarize make_list(Url);
  let TI = materialize(ThreatIntelligenceIndicator
    | where TimeGenerated >= ago(ioc_lookBack)
    | where isnotempty(Url) and tolower(Url) in (ClickedUrls)
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
    | where Active == true and ExpirationDateTime > now()
    | project-rename TI_Url = Url, TI_Type = Type
    );
  (union isfuzzy=false (TI | join kind=innerunique (ChainReportID) on $left.TI_Url == $right.UrlChain),
    (TI | join kind=innerunique (ChainReportID) on $left.TI_Url == $right.Url))
  | project-away UrlChain
  | join kind=innerunique (UrlClickEvents_) on ReportId
  | where UrlClickEvents_TimeGenerated < ExpirationDateTime
  | summarize UrlClickEvents_TimeGenerated = arg_max(UrlClickEvents_TimeGenerated, *) by IndicatorId
  | project UrlClickEvents_TimeGenerated, AccountUpn, Description, ActivityGroupNames, IndicatorId, ThreatType, ExpirationDateTime, ConfidenceScore, Url, NetworkMessageId
  | extend timestamp = UrlClickEvents_TimeGenerated
  | extend timestamp = UrlClickEvents_TimeGenerated, Name = tostring(split(AccountUpn, '@', 0)[0]), UPNSuffix = tostring(split(AccountUpn, '@', 1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountUpn
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 0: let

let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let UrlClickEvents_ = materialize(UrlClickEvents
    | where TimeGenerated >= ago(dt_lookBack)
    | extend UrlClickEvents_TimeGenerated = TimeGenerated);
let ChainReportID = UrlClickEvents_
    | mv-expand todynamic(UrlChain)
    | extend UrlChain = tolower(UrlChain)
    | project ReportId, Url, UrlChain;
let ClickedUrls = 
  (union isfuzzy=false (ChainReportID), (ChainReportID | project Url = UrlChain))
  | distinct Url
  | summarize make_list(Url);
let TI = materialize(<inlined as stages below>);

Stage 1: source

let UrlClickEvents_

Stage 2: source

let ChainReportID

Stage 3: source

let ClickedUrls

Stage 4: union

union of 2 branches

Stage 5: source

ThreatIntelligenceIndicator

Stage 6: where

where TimeGenerated >= ago(1209600s)

Stage 7: where

where Url =~ "ClickedUrls" and isnotempty(Url)

Stage 8: summarize

summarize LatestIndicatorTime by IndicatorId

Stage 9: where

where Active =~ true

Stage 10: project-rename

project-rename

Stage 11: join

join kind=innerunique (ChainReportID) on TI_Url, UrlChain

Stage 12: source

ThreatIntelligenceIndicator

Stage 13: where

where TimeGenerated >= ago(1209600s)

Stage 14: where

where Url =~ "ClickedUrls" and isnotempty(Url)

Stage 15: summarize

summarize LatestIndicatorTime by IndicatorId

Stage 16: where

where Active =~ true

Stage 17: project-rename

project-rename

Stage 18: join

join kind=innerunique (ChainReportID) on TI_Url, Url

Stage 19: project-away

project-away UrlChain

Stage 20: join

join kind=innerunique (UrlClickEvents_) on ReportId

Stage 21: where

where UrlClickEvents_TimeGenerated < ExpirationDateTime

Stage 22: summarize

summarize UrlClickEvents_TimeGenerated by IndicatorId

Stage 23: project

project AccountUpn, ActivityGroupNames, ConfidenceScore, Description, ExpirationDateTime, IndicatorId, NetworkMessageId, ThreatType, Url, UrlClickEvents_TimeGenerated

Stage 24: extend

extend timestamp

Stage 25: extend

extend Name, UPNSuffix, timestamp

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
AccountUpnproject
ActivityGroupNamesproject
ConfidenceScoreproject
Descriptionproject
ExpirationDateTimeproject
IndicatorIdproject
NetworkMessageIdproject
ThreatTypeproject
Urlproject
UrlClickEvents_TimeGeneratedproject
timestampextend
Nameextend
UPNSuffixextend