Detection rules › Kusto

Dataverse - TI map URL to DataverseActivity

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

Identifies a match in DataverseActivity from any URL IOC from Microsoft Sentinel Threat Intelligence.

MITRE ATT&CK coverage

Rule body kusto

id: d88a0e22-3b6a-40c2-af28-c064b44d03b7
kind: Scheduled
name: Dataverse - TI map URL to DataverseActivity
description: Identifies a match in DataverseActivity from any URL IOC from Microsoft
  Sentinel Threat Intelligence.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Dataverse
    dataTypes:
      - DataverseActivity
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Execution
  - Persistence
relevantTechniques:
  - T1566
  - T1456
  - T1474
  - T0819
  - T0865
  - T0862
  - T0863
  - T1204
  - T1574
  - T0873
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  ThreatIntelligenceIndicator
  | where TimeGenerated >= ago(ioc_lookBack) and ExpirationDateTime > now()
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
  | where Active == true
  | where isnotempty(Url)
  | join kind=innerunique (
      DataverseActivity
      | where TimeGenerated >= ago(dt_lookBack)
      | where Message in ("Create", "Update")
      | where isnotempty(Fields) and Fields has "http"
      | extend
          ExtractedUrls = extract_all("(http[s]?://(?:[a-zA-Z\\.-]|[0-9])+)", tostring(Fields)),
          DataverseActivity_TimeGenerated = TimeGenerated
      | mv-expand Url = ExtractedUrls
      | project
          DataverseActivity_TimeGenerated,
          tostring(Url),
          UserId,
          ClientIp,
          InstanceUrl,
          EntityName
      )
      on Url
  | where DataverseActivity_TimeGenerated < ExpirationDateTime
  | summarize DataverseActivity_TimeGenerated  = arg_max(DataverseActivity_TimeGenerated, *) by IndicatorId, Url
  | extend
      CloudAppId = int(32780),
      AccountName = tostring(split(UserId, '@')[0]),
      UPNSuffix = tostring(split(UserId, '@')[1])
  | project
      DataverseActivity_TimeGenerated,
      Description,
      ActivityGroupNames,
      IndicatorId,
      ThreatType,
      ExpirationDateTime,
      ConfidenceScore,
      UserId,
      ClientIp,
      InstanceUrl,
      CloudAppId,
      AccountName,
      UPNSuffix,
      Url
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIp
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: CloudAppId
      - identifier: InstanceName
        columnName: InstanceUrl
alertDetailsOverride:
  alertDisplayNameFormat: Dataverse - TI match on URL in {{InstanceUrl}}
  alertDescriptionFormat: Malicous IP {{Url}} was found in {{InstanceUrl}}. Associated
    user is {{UserId}}
version: 3.2.0

Stages and Predicates

Parameters

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

Stage 1: source

ThreatIntelligenceIndicator

Stage 2: where

| where TimeGenerated >= ago(ioc_lookBack) and ExpirationDateTime > now()

Stage 3: summarize

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

Stage 4: where

| where Active == true

Stage 5: where

| where isnotempty(Url)

Stage 6: join

| join kind=innerunique (
    DataverseActivity
    | where TimeGenerated >= ago(dt_lookBack)
    | where Message in ("Create", "Update")
    | where isnotempty(Fields) and Fields has "http"
    | extend
        ExtractedUrls = extract_all("(http[s]?://(?:[a-zA-Z\\.-]|[0-9])+)", tostring(Fields)),
        DataverseActivity_TimeGenerated = TimeGenerated
    | mv-expand Url = ExtractedUrls
    | project
        DataverseActivity_TimeGenerated,
        tostring(Url),
        UserId,
        ClientIp,
        InstanceUrl,
        EntityName
    )
    on Url

Stage 7: where

| where DataverseActivity_TimeGenerated < ExpirationDateTime

Stage 8: summarize

| summarize DataverseActivity_TimeGenerated  = arg_max(DataverseActivity_TimeGenerated, *) by IndicatorId, Url

Stage 9: extend

| extend
    CloudAppId = int(32780),
    AccountName = tostring(split(UserId, '@')[0]),
    UPNSuffix = tostring(split(UserId, '@')[1])

Stage 10: project

| project
    DataverseActivity_TimeGenerated,
    Description,
    ActivityGroupNames,
    IndicatorId,
    ThreatType,
    ExpirationDateTime,
    ConfidenceScore,
    UserId,
    ClientIp,
    InstanceUrl,
    CloudAppId,
    AccountName,
    UPNSuffix,
    Url

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
Activeeq
  • true transforms: cased corpus 68 (kusto 68)
DataverseActivity_TimeGeneratedlt
  • ExpirationDateTime transforms: cased corpus 2 (kusto 2)
Fieldsis_not_null
  • (no value, null check)
Fieldsmatch
  • http transforms: term
Messagein
  • Create transforms: cased corpus 2 (kusto 2)
  • Update transforms: cased corpus 5 (kusto 5)
Urlis_not_null
  • (no value, null check)

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
AccountNameproject
ActivityGroupNamesproject
ClientIpproject
CloudAppIdproject
ConfidenceScoreproject
DataverseActivity_TimeGeneratedproject
Descriptionproject
ExpirationDateTimeproject
IndicatorIdproject
InstanceUrlproject
ThreatTypeproject
UPNSuffixproject
Urlproject
UserIdproject