Detection rules › Kusto

TI Map IP Entity to Azure SQL Security Audit Events

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

This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in SQL Security Audit Events.

MITRE ATT&CK coverage

TacticTechniques
Command & Control

Rules detecting the same action

These rules filter on the same operation.

Rule body

id: d0aa8969-1bbe-4da3-9e76-09e5f67c9d85
name: TI Map IP Entity to Azure SQL Security Audit Events
description: |
  This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in SQL Security Audit Events.
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: AzureSql
    dataTypes:
      - AzureDiagnostics
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h; // Look back 1 hour for AzureDiagnostics logs
  let ioc_lookBack = 14d; // Look back 14 days for threat intelligence indicators
  // Fetch threat intelligence indicators related to IP addresses
  let IP_Indicators = ThreatIntelligenceIndicator
    | where isnotempty(NetworkIP) or isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkSourceIP)
    | where TimeGenerated >= ago(ioc_lookBack)
    | extend TI_ipEntity = iff(isnotempty(NetworkIP), NetworkIP, NetworkDestinationIP)
    | extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
    | extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(EmailSourceIpAddress), EmailSourceIpAddress, TI_ipEntity)
    | where ipv4_is_private(TI_ipEntity) == false and  TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
    | where Active == true and ExpirationDateTime > now();
  // Perform a join between IP indicators and AzureDiagnostics logs for SQL Security Audit events
  IP_Indicators
    // Use innerunique to keep performance fast and result set low, as we only need one match to indicate potential malicious activity that needs investigation
    | join kind=innerunique (
        AzureDiagnostics
        | where TimeGenerated >= ago(dt_lookBack)
        | where ResourceProvider == 'MICROSOFT.SQL'
        | where Category == 'SQLSecurityAuditEvents'
        | extend SQLSecurityAuditEvents_TimeGenerated = TimeGenerated
        | extend ClientIP = column_ifexists("client_ip_s", "Not Available")
        | extend Action = column_ifexists("action_name_s", "Not Available")
        | extend Application = column_ifexists("application_name_s", "Not Available")
        | extend HostName = column_ifexists("host_name_s", "Not Available")
    )
    on $left.TI_ipEntity == $right.ClientIP
    // Filter out logs that occurred after the expiration of the corresponding indicator
    | where SQLSecurityAuditEvents_TimeGenerated < ExpirationDateTime
    // Group the results by IndicatorId and ClientIP, and keep the log entry with the latest timestamp
    | summarize SQLSecurityAuditEvents_TimeGenerated = arg_max(SQLSecurityAuditEvents_TimeGenerated, *) by IndicatorId, ClientIP
    // Select the desired output fields
    | project SQLSecurityAuditEvents_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore,
      TI_ipEntity, ResourceId, ClientIP, Action, Application, HostName, NetworkIP, NetworkDestinationIP, NetworkSourceIP, EmailSourceIpAddress, Type
    // Rename the timestamp field
    | extend timestamp = SQLSecurityAuditEvents_TimeGenerated
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIP
version: 1.3.2
kind: Scheduled

Stages and Predicates

Parameters

let dt_lookBack = 1h;
let ioc_lookBack = 14d;

let IP_Indicators is inlined into the numbered stages below.

Stages 1 to 7 define let IP_Indicators (the rule's main pipeline source); stages 8 to 12 run on it.

Stage 1: source

ThreatIntelligenceIndicator

Stage 2: where

| where isnotempty(NetworkIP) or isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkSourceIP)

Stage 3: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 4: extend (3 consecutive steps)

| extend TI_ipEntity = iff(isnotempty(NetworkIP), NetworkIP, NetworkDestinationIP)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(EmailSourceIpAddress), EmailSourceIpAddress, TI_ipEntity)
TI_ipEntity =
ifisnotempty(NetworkIP)NetworkIP
elseNetworkDestinationIP

Stage 5: where

| where ipv4_is_private(TI_ipEntity) == false and  TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."

Stage 6: summarize

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

Stage 7: where

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

Stage 8: join

IP_Indicators
| join kind=innerunique (
      AzureDiagnostics
      | where TimeGenerated >= ago(dt_lookBack)
      | where ResourceProvider == 'MICROSOFT.SQL'
      | where Category == 'SQLSecurityAuditEvents'
      | extend SQLSecurityAuditEvents_TimeGenerated = TimeGenerated
      | extend ClientIP = column_ifexists("client_ip_s", "Not Available")
      | extend Action = column_ifexists("action_name_s", "Not Available")
      | extend Application = column_ifexists("application_name_s", "Not Available")
      | extend HostName = column_ifexists("host_name_s", "Not Available")
  )
  on $left.TI_ipEntity == $right.ClientIP

Stage 9: where

| where SQLSecurityAuditEvents_TimeGenerated < ExpirationDateTime

Stage 10: summarize

| summarize SQLSecurityAuditEvents_TimeGenerated = arg_max(SQLSecurityAuditEvents_TimeGenerated, *) by IndicatorId, ClientIP

Stage 11: project

| project SQLSecurityAuditEvents_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore,
    TI_ipEntity, ResourceId, ClientIP, Action, Application, HostName, NetworkIP, NetworkDestinationIP, NetworkSourceIP, EmailSourceIpAddress, Type

Stage 12: extend

| extend timestamp = SQLSecurityAuditEvents_TimeGenerated

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
TI_ipEntitycidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:TI_ipEntity
TI_ipEntitystarts_with127.excludes:TI_ipEntity field:"TI_ipEntity" value:"127."
TI_ipEntitystarts_with::excludes:TI_ipEntity field:"TI_ipEntity" value:"::"
TI_ipEntitystarts_withfe80excludes:TI_ipEntity field:"TI_ipEntity" value:"fe80"

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
Actionproject
ActivityGroupNamesproject
Applicationproject
ClientIPproject
ConfidenceScoreproject
Descriptionproject
EmailSourceIpAddressproject
ExpirationDateTimeproject
HostNameproject
IndicatorIdproject
NetworkDestinationIPproject
NetworkIPproject
NetworkSourceIPproject
ResourceIdproject
SQLSecurityAuditEvents_TimeGeneratedproject
TI_ipEntityproject
ThreatTypeproject
Typeproject
Urlproject
timestampextend