Detection rules › Kusto

TI Map Domain entity to Syslog

Severity
medium
Time window
14d
Group by
DomainName, Id, IndicatorId, ObservableValue, domain
Source
github.com/Azure/Azure-Sentinel

Identifies a match in Syslog table from any Domain IOC from TI

MITRE ATT&CK coverage

TacticTechniques
Command & Control

Rule body

id: cd19434e-10f2-4e2f-b3c1-ce6f08ac5357
name: TI Map Domain entity to Syslog
description: |
  Identifies a match in Syslog table from any Domain IOC from TI
severity: Medium
requiredDataConnectors:
  - connectorId: Syslog
    dataTypes:
      - Syslog
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
   let dt_lookBack = 1h;  // Define the time range to look back for syslog data (1 hour)
   let ioc_lookBack = 14d;  // Define the time range to look back for threat intelligence indicators (14 days)
   // Create a list of top-level domains (TLDs) from the threat feed for later validation
   let list_tlds = ThreatIntelIndicators
    | where TimeGenerated > ago(ioc_lookBack)
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id
    | where IsActive and (ValidUntil > now() or isempty(ValidUntil))
    | extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
    | where IndicatorType == "domain-name"
    | extend DomainName = tolower(ObservableValue)
    | extend parts = split(DomainName, '.')
    | extend tld = parts[(array_length(parts)-1)]
    | summarize count() by tostring(tld)
    | summarize make_list(tld);
   // Fetch the latest active domain indicators from the threat intelligence data within the specified time range
   let Domain_Indicators = ThreatIntelIndicators
   | extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
   | where IndicatorType == "domain-name"
   | extend DomainName = tolower(ObservableValue)
   | extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
    | where TimeGenerated >= ago(ioc_lookBack)
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
    | where IsActive and (ValidUntil > now() or isempty(ValidUntil))
    | extend IndicatorId = tostring(split(Id, "--")[2])
    | extend Url = iff(ObservableKey == "url:value", ObservableValue, "");
   // Join the threat intelligence indicators with syslog data on matching domain entities
   Domain_Indicators
   | project-reorder *, IsActive, Tags, TrafficLightProtocolLevel, DomainName, Type
    | join kind=innerunique (
      Syslog
      | where TimeGenerated > ago(dt_lookBack)
      // Extract domain patterns from syslog messages
      | extend domain = extract("(([a-z0-9]+(-[a-z0-9]+)*\\.)+[a-z]{2,})",1, tolower(SyslogMessage))
      | where isnotempty(domain)
      | extend parts = split(domain, '.')
      // Split out the top-level domain (TLD)
      | extend tld = parts[(array_length(parts)-1)]
      // Validate parsed domain by checking if the TLD is in the list of TLDs in our threat feed
      | where tld in~ (list_tlds)
      | extend Syslog_TimeGenerated = TimeGenerated
    ) on $left.DomainName==$right.domain
    | where Syslog_TimeGenerated < ValidUntil
    // Retrieve the latest syslog timestamp for each indicator and domain combination
    | summarize Syslog_TimeGenerated = arg_max(Syslog_TimeGenerated, *) by IndicatorId, domain
    // Select the desired columns for the final result set
    | extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
    | extend Description = tostring(parse_json(Data).description)
    | project Syslog_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, Confidence, SyslogMessage, Computer, ProcessName, domain, HostIP, Type, DomainName, Url
    // Extract the hostname from the Computer field
    | extend HostName = tostring(split(Computer, '.', 0)[0])
    // Extract the DNS domain from the Computer field
    | extend DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
    // Assign the Syslog_TimeGenerated value to the timestamp field
    | extend timestamp = Syslog_TimeGenerated
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: HostIP
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.4.7
kind: Scheduled

Stages and Predicates

Stage 0: let

let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let list_tlds = ThreatIntelIndicators
 | where TimeGenerated > ago(ioc_lookBack)
 | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id
 | where IsActive and (ValidUntil > now() or isempty(ValidUntil))
 | extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
 | where IndicatorType == "domain-name"
 | extend DomainName = tolower(ObservableValue)
 | extend parts = split(DomainName, '.')
 | extend tld = parts[(array_length(parts)-1)]
 | summarize count() by tostring(tld)
 | summarize make_list(tld);
let Domain_Indicators = ThreatIntelIndicators <inlined as stages below>;

Stage 1: source

ThreatIntelIndicators

Stage 2: extend

| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))

Stage 3: where

| where IndicatorType == "domain-name"

Stage 4: extend

| extend DomainName = tolower(ObservableValue)

Stage 5: extend

| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)

Stage 6: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 7: summarize

| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue

Stage 8: where

| where IsActive and (ValidUntil > now() or isempty(ValidUntil))

Stage 9: extend

| extend IndicatorId = tostring(split(Id, "--")[2])

Stage 10: extend

| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
Url =
ifObservableKey == "url:value"ObservableValue
else""

Stage 11: project-reorder

| project-reorder *, IsActive, Tags, TrafficLightProtocolLevel, DomainName, Type

Stage 12: join

| join kind=innerunique (
   Syslog
   | where TimeGenerated > ago(dt_lookBack)
   | extend domain = extract("(([a-z0-9]+(-[a-z0-9]+)*\\.)+[a-z]{2,})",1, tolower(SyslogMessage))
   | where isnotempty(domain)
   | extend parts = split(domain, '.')
   | extend tld = parts[(array_length(parts)-1)]
   | where tld in~ (list_tlds)
   | extend Syslog_TimeGenerated = TimeGenerated
 ) on $left.DomainName==$right.domain

Stage 13: where

where Syslog_TimeGenerated < ValidUntil

Stage 14: summarize

summarize Syslog_TimeGenerated by IndicatorId, domain

Stage 15: extend

extend ActivityGroupNames

Stage 16: extend

extend Description

Stage 17: project

project ActivityGroupNames, Computer, Confidence, Description, DomainName, HostIP, Id, ProcessName, SyslogMessage, Syslog_TimeGenerated, Type, Url, ValidUntil, domain

Stage 18: extend (3 consecutive steps)

extend DnsDomain, HostName, timestamp

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
Computerproject
Confidenceproject
Descriptionproject
DomainNameproject
HostIPproject
Idproject
ProcessNameproject
SyslogMessageproject
Syslog_TimeGeneratedproject
Typeproject
Urlproject
ValidUntilproject
domainproject
HostNameextend
DnsDomainextend
timestampextend