Back
Idc6f85aff-4ef0-4fde-b90e-e0ba1a9c3df6
RulenamePRODAFT USTA - TI map Domain to DnsEvents
DescriptionIdentifies a match in DnsEvents from any domain-name indicator ingested from PRODAFT USTA

IoC Threat Intelligence (SourceSystem starting with “PRODAFT USTA”).
SeverityMedium
TacticsCommandAndControl
TechniquesT1071
Required data connectorsDNS
PRODAFTUstaIoCUploadIndicators
KindScheduled
Query frequency1h
Query period14d
Trigger threshold0
Trigger operatorgt
Source Urihttps://github.com/Azure/Azure-Sentinel/blob/master/Solutions/PRODAFT%20USTA%20-%20IoC%20Threat%20Intelligence/Analytic%20Rules/TIMapDomainToDnsEvents.yaml
Version1.0.0
Arm templatec6f85aff-4ef0-4fde-b90e-e0ba1a9c3df6.json
Deploy To Azure
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let Domain_Indicators = ThreatIntelIndicators
  // Each USTA IoC feed uploads under its own SourceSystem ("PRODAFT USTA - Malicious URLs", etc.)
  | where SourceSystem startswith "PRODAFT USTA"
  | 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));
// TLD allow-list built from the feed: lets the DnsEvents scan discard most rows before the join
let maxListSize = 100000;
let list_tlds = Domain_Indicators
  | extend parts = split(DomainName, '.')
  | extend tld = parts[(array_length(parts)-1)]
  | extend IndicatorId = tostring(split(Id, "--")[2])
  | summarize count() by tostring(tld)
  | project tld
  | summarize make_list(tld, maxListSize);
Domain_Indicators
 | project-reorder *, IsActive, Tags, TrafficLightProtocolLevel, DomainName, Type
  // innerunique: one match per indicator is enough to alert, and it keeps the join cheap
  | join kind=innerunique (
    DnsEvents
    | where TimeGenerated > ago(dt_lookBack)
    | where isnotempty(Name)
    // DnsEvents.Name keeps the queried casing; the indicator side is lowercased, and the join below is case-sensitive
    | extend Name = tolower(Name)
    | extend parts = split(Name, '.')
    | extend tld = parts[(array_length(parts)-1)]
    | where tld in~ (list_tlds)
    | extend DNS_TimeGenerated = TimeGenerated
  ) on $left.DomainName==$right.Name
  // non-expiring indicators are allowed above, so they must survive this filter too
  | where isempty(ValidUntil) or DNS_TimeGenerated < ValidUntil
  | summarize DNS_TimeGenerated = arg_max(DNS_TimeGenerated, *) by Id, Name
  | extend Description = tostring(parse_json(Data).description)
  | extend IndicatorTags = tostring(parse_json(Data).labels)
  | project DNS_TimeGenerated, Description, IndicatorTags, Id, ValidUntil, Confidence, Computer, ClientIP
  | extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
  | extend timestamp = DNS_TimeGenerated
name: PRODAFT USTA - TI map Domain to DnsEvents
triggerOperator: gt
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  let Domain_Indicators = ThreatIntelIndicators
    // Each USTA IoC feed uploads under its own SourceSystem ("PRODAFT USTA - Malicious URLs", etc.)
    | where SourceSystem startswith "PRODAFT USTA"
    | 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));
  // TLD allow-list built from the feed: lets the DnsEvents scan discard most rows before the join
  let maxListSize = 100000;
  let list_tlds = Domain_Indicators
    | extend parts = split(DomainName, '.')
    | extend tld = parts[(array_length(parts)-1)]
    | extend IndicatorId = tostring(split(Id, "--")[2])
    | summarize count() by tostring(tld)
    | project tld
    | summarize make_list(tld, maxListSize);
  Domain_Indicators
   | project-reorder *, IsActive, Tags, TrafficLightProtocolLevel, DomainName, Type
    // innerunique: one match per indicator is enough to alert, and it keeps the join cheap
    | join kind=innerunique (
      DnsEvents
      | where TimeGenerated > ago(dt_lookBack)
      | where isnotempty(Name)
      // DnsEvents.Name keeps the queried casing; the indicator side is lowercased, and the join below is case-sensitive
      | extend Name = tolower(Name)
      | extend parts = split(Name, '.')
      | extend tld = parts[(array_length(parts)-1)]
      | where tld in~ (list_tlds)
      | extend DNS_TimeGenerated = TimeGenerated
    ) on $left.DomainName==$right.Name
    // non-expiring indicators are allowed above, so they must survive this filter too
    | where isempty(ValidUntil) or DNS_TimeGenerated < ValidUntil
    | summarize DNS_TimeGenerated = arg_max(DNS_TimeGenerated, *) by Id, Name
    | extend Description = tostring(parse_json(Data).description)
    | extend IndicatorTags = tostring(parse_json(Data).labels)
    | project DNS_TimeGenerated, Description, IndicatorTags, Id, ValidUntil, Confidence, Computer, ClientIP
    | extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
    | extend timestamp = DNS_TimeGenerated
queryFrequency: 1h
description: |
  'Identifies a match in DnsEvents from any domain-name indicator ingested from PRODAFT USTA
  IoC Threat Intelligence (SourceSystem starting with "PRODAFT USTA").'
id: c6f85aff-4ef0-4fde-b90e-e0ba1a9c3df6
triggerThreshold: 0
queryPeriod: 14d
version: 1.0.0
kind: Scheduled
relevantTechniques:
- T1071
severity: Medium
requiredDataConnectors:
- connectorId: DNS
  dataTypes:
  - DnsEvents
- connectorId: PRODAFTUstaIoCUploadIndicators
  dataTypes:
  - ThreatIntelIndicators
OriginalUri: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/PRODAFT%20USTA%20-%20IoC%20Threat%20Intelligence/Analytic%20Rules/TIMapDomainToDnsEvents.yaml
tactics:
- CommandAndControl
entityMappings:
- fieldMappings:
  - identifier: FullName
    columnName: Computer
  - identifier: HostName
    columnName: HostName
  - identifier: DnsDomain
    columnName: DnsDomain
  entityType: Host
- fieldMappings:
  - identifier: Address
    columnName: ClientIP
  entityType: IP
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "workspace": {
      "type": "String"
    }
  },
  "resources": [
    {
      "apiVersion": "2024-01-01-preview",
      "id": "[concat(resourceId('Microsoft.OperationalInsights/workspaces/providers', parameters('workspace'), 'Microsoft.SecurityInsights'),'/alertRules/c6f85aff-4ef0-4fde-b90e-e0ba1a9c3df6')]",
      "kind": "Scheduled",
      "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/c6f85aff-4ef0-4fde-b90e-e0ba1a9c3df6')]",
      "properties": {
        "alertRuleTemplateName": "c6f85aff-4ef0-4fde-b90e-e0ba1a9c3df6",
        "customDetails": null,
        "description": "'Identifies a match in DnsEvents from any domain-name indicator ingested from PRODAFT USTA\nIoC Threat Intelligence (SourceSystem starting with \"PRODAFT USTA\").'\n",
        "displayName": "PRODAFT USTA - TI map Domain to DnsEvents",
        "enabled": true,
        "entityMappings": [
          {
            "entityType": "Host",
            "fieldMappings": [
              {
                "columnName": "Computer",
                "identifier": "FullName"
              },
              {
                "columnName": "HostName",
                "identifier": "HostName"
              },
              {
                "columnName": "DnsDomain",
                "identifier": "DnsDomain"
              }
            ]
          },
          {
            "entityType": "IP",
            "fieldMappings": [
              {
                "columnName": "ClientIP",
                "identifier": "Address"
              }
            ]
          }
        ],
        "OriginalUri": "https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/PRODAFT%20USTA%20-%20IoC%20Threat%20Intelligence/Analytic%20Rules/TIMapDomainToDnsEvents.yaml",
        "query": "let dt_lookBack = 1h;\nlet ioc_lookBack = 14d;\nlet Domain_Indicators = ThreatIntelIndicators\n  // Each USTA IoC feed uploads under its own SourceSystem (\"PRODAFT USTA - Malicious URLs\", etc.)\n  | where SourceSystem startswith \"PRODAFT USTA\"\n  | extend IndicatorType = replace(@\"\\[|\\]|\\\"\"\", \"\", tostring(split(ObservableKey, \":\", 0)))\n  | where IndicatorType == \"domain-name\"\n  | extend DomainName = tolower(ObservableValue)\n  | extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)\n  | where TimeGenerated >= ago(ioc_lookBack)\n  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue\n  | where IsActive and (ValidUntil > now() or isempty(ValidUntil));\n// TLD allow-list built from the feed: lets the DnsEvents scan discard most rows before the join\nlet maxListSize = 100000;\nlet list_tlds = Domain_Indicators\n  | extend parts = split(DomainName, '.')\n  | extend tld = parts[(array_length(parts)-1)]\n  | extend IndicatorId = tostring(split(Id, \"--\")[2])\n  | summarize count() by tostring(tld)\n  | project tld\n  | summarize make_list(tld, maxListSize);\nDomain_Indicators\n | project-reorder *, IsActive, Tags, TrafficLightProtocolLevel, DomainName, Type\n  // innerunique: one match per indicator is enough to alert, and it keeps the join cheap\n  | join kind=innerunique (\n    DnsEvents\n    | where TimeGenerated > ago(dt_lookBack)\n    | where isnotempty(Name)\n    // DnsEvents.Name keeps the queried casing; the indicator side is lowercased, and the join below is case-sensitive\n    | extend Name = tolower(Name)\n    | extend parts = split(Name, '.')\n    | extend tld = parts[(array_length(parts)-1)]\n    | where tld in~ (list_tlds)\n    | extend DNS_TimeGenerated = TimeGenerated\n  ) on $left.DomainName==$right.Name\n  // non-expiring indicators are allowed above, so they must survive this filter too\n  | where isempty(ValidUntil) or DNS_TimeGenerated < ValidUntil\n  | summarize DNS_TimeGenerated = arg_max(DNS_TimeGenerated, *) by Id, Name\n  | extend Description = tostring(parse_json(Data).description)\n  | extend IndicatorTags = tostring(parse_json(Data).labels)\n  | project DNS_TimeGenerated, Description, IndicatorTags, Id, ValidUntil, Confidence, Computer, ClientIP\n  | extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))\n  | extend timestamp = DNS_TimeGenerated\n",
        "queryFrequency": "PT1H",
        "queryPeriod": "P14D",
        "severity": "Medium",
        "subTechniques": [],
        "suppressionDuration": "PT1H",
        "suppressionEnabled": false,
        "tactics": [
          "CommandAndControl"
        ],
        "techniques": [
          "T1071"
        ],
        "templateVersion": "1.0.0",
        "triggerOperator": "GreaterThan",
        "triggerThreshold": 0
      },
      "type": "Microsoft.OperationalInsights/workspaces/providers/alertRules"
    }
  ]
}