Detection rules › Kusto

TI Map URL Entity to AuditLogs

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

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

MITRE ATT&CK coverage

TacticTechniques
Command & Control

Telemetry coverage

Rule body

id: 9991c277-e0a1-4079-8c40-fbfca2705615
name: TI Map URL Entity to AuditLogs
description: |
  'This query identifies any URL indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in AuditLogs.'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
  - 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;
  let ioc_lookBack = 14d;
  let AuditEvents = materialize(AuditLogs
    | where TimeGenerated >= ago(dt_lookBack)
    // Extract the URL that is contained within the JSON data
    | extend Url = tolower(extract("(http[s]?://[^\\s\\\"\\\\]+)", 1, tostring(TargetResources)))
    | extend userPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
    | extend TargetResourceDisplayName = tostring(TargetResources[0].displayName));
  let AuditUrls = AuditEvents | distinct Url = tolower(Url) | summarize make_list(Url);
  ThreatIntelIndicators
  //extract key part of kv pair
       | extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
       | where IndicatorType == "url"
       | extend Url = tolower(ObservableValue)
       | extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
  | where TimeGenerated >= ago(ioc_lookBack)
  | where tolower(Url) in (AuditUrls)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
  | where IsActive and (ValidUntil > now() or isempty(ValidUntil))
  | extend Description = tostring(parse_json(Data).description)
  | where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"
  // using innerunique to keep perf fast and result set low, we only need one match to indicate potential malicious activity that needs to be investigated
  | join kind=innerunique (AuditEvents) on Url
  | where TimeGenerated < ValidUntil
  | summarize TimeGenerated = arg_max(TimeGenerated, *) by Id, Url
  | extend Description = tostring(parse_json(Data).description)
  | extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
  | project TimeGenerated, Description, ActivityGroupNames, Id, Type, ValidUntil, Confidence,
  OperationName, Identity, userPrincipalName, TargetResourceDisplayName, Url
  | extend AccountName = tostring(split(userPrincipalName, "@")[0]), AccountUPNSuffix = tostring(split(userPrincipalName, "@")[1])
  | extend HostName = tostring(split(TargetResourceDisplayName, ".")[0]), DomainIndex = toint(indexof(TargetResourceDisplayName, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(TargetResourceDisplayName, DomainIndex + 1), TargetResourceDisplayName)
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: userPrincipalName
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: TargetResourceDisplayName
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.2.11
kind: Scheduled

Stages and Predicates

Parameters

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

Let binding: AuditEvents used in Stage 12

let AuditEvents = materialize(AuditLogs
  | where TimeGenerated >= ago(dt_lookBack)
  | extend Url = tolower(extract("(http[s]?://[^\\s\\\"\\\\]+)", 1, tostring(TargetResources)))
  | extend userPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
  | extend TargetResourceDisplayName = tostring(TargetResources[0].displayName));

Let binding: AuditUrls used in Stage 7

let AuditUrls = AuditEvents | distinct Url = tolower(Url) | summarize make_list(Url);

Stage 1: source

ThreatIntelIndicators

Stage 2: extend

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

Stage 3: where

| where IndicatorType == "url"

Stage 4: extend

| extend Url = tolower(ObservableValue)

Stage 5: extend

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

Stage 6: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 7: where

| where tolower(Url) in (AuditUrls)

Stage 8: summarize

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

Stage 9: where

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

Stage 10: extend

| extend Description = tostring(parse_json(Data).description)

Stage 11: where

| where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"

Stage 12: join

| join kind=innerunique (AuditEvents) on Url

Stage 13: where

| where TimeGenerated < ValidUntil

Stage 14: summarize

| summarize TimeGenerated = arg_max(TimeGenerated, *) by Id, Url

Stage 15: extend

| extend Description = tostring(parse_json(Data).description)

Stage 16: extend

| extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))

Stage 17: project

| project TimeGenerated, Description, ActivityGroupNames, Id, Type, ValidUntil, Confidence,
OperationName, Identity, userPrincipalName, TargetResourceDisplayName, Url

Stage 18: extend (3 consecutive steps)

| extend AccountName = tostring(split(userPrincipalName, "@")[0]), AccountUPNSuffix = tostring(split(userPrincipalName, "@")[1])
| extend HostName = tostring(split(TargetResourceDisplayName, ".")[0]), DomainIndex = toint(indexof(TargetResourceDisplayName, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(TargetResourceDisplayName, DomainIndex + 1), TargetResourceDisplayName)

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DescriptioncontainsState: falsepos;excludes:Description field:"Description" value:"State: falsepos;"
DescriptioncontainsState: inactive;excludes:Description field:"Description" value:"State: inactive;"

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
Confidenceproject
Descriptionproject
Idproject
Identityproject
OperationNameproject
TargetResourceDisplayNameproject
TimeGeneratedproject
Typeproject
Urlproject
ValidUntilproject
userPrincipalNameproject
AccountNameextend
AccountUPNSuffixextend
DomainIndexextend
HostNameextend
HostNameDomainextend