Back
Id99ae4d06-d352-4926-a1d7-9c28e25b1313
RulenamePRODAFT USTA - TI map URL to Syslog
DescriptionIdentifies a match in Syslog data from any malicious URL indicator ingested from PRODAFT

USTA IoC Threat Intelligence (SourceSystem starting with “PRODAFT USTA”).
SeverityMedium
TacticsCommandAndControl
TechniquesT1071
Required data connectorsPRODAFTUstaIoCUploadIndicators
Syslog
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/TIMapUrlToSyslog.yaml
Version1.0.0
Arm template99ae4d06-d352-4926-a1d7-9c28e25b1313.json
Deploy To Azure
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
ThreatIntelIndicators
// Each USTA IoC feed uploads under its own SourceSystem ("PRODAFT USTA - Malicious URLs", etc.)
| where SourceSystem startswith "PRODAFT USTA"
// ObservableKey is a STIX kv pair ("url:value") - take the type before the colon
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
| where IndicatorType == "url"
| extend Url = 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))
| project-reorder *, Tags, TrafficLightProtocolLevel, Url, Type
// innerunique: one match per indicator is enough to alert, and it keeps the join cheap
| join kind=innerunique (
  Syslog
  | where TimeGenerated >= ago(dt_lookBack)
  // Pull the first URL out of the free-text message; rows without one drop out of the join
  | extend Url = extract("(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)", 1,SyslogMessage)
  | extend Syslog_TimeGenerated = TimeGenerated
) on Url
// non-expiring indicators are allowed above, so they must survive this filter too
| where isempty(ValidUntil) or Syslog_TimeGenerated < ValidUntil
| summarize Syslog_TimeGenerated  = arg_max(Syslog_TimeGenerated , *) by Id, Url
| extend Description = tostring(parse_json(Data).description)
| extend IndicatorTags = tostring(parse_json(Data).labels)
| project timestamp = Syslog_TimeGenerated, Description, IndicatorTags, Id, Type, ValidUntil, Confidence, SyslogMessage, Computer, ProcessName, Url, HostIP
name: PRODAFT USTA - TI map URL to Syslog
triggerOperator: gt
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  ThreatIntelIndicators
  // Each USTA IoC feed uploads under its own SourceSystem ("PRODAFT USTA - Malicious URLs", etc.)
  | where SourceSystem startswith "PRODAFT USTA"
  // ObservableKey is a STIX kv pair ("url:value") - take the type before the colon
  | extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
  | where IndicatorType == "url"
  | extend Url = 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))
  | project-reorder *, Tags, TrafficLightProtocolLevel, Url, Type
  // innerunique: one match per indicator is enough to alert, and it keeps the join cheap
  | join kind=innerunique (
    Syslog
    | where TimeGenerated >= ago(dt_lookBack)
    // Pull the first URL out of the free-text message; rows without one drop out of the join
    | extend Url = extract("(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)", 1,SyslogMessage)
    | extend Syslog_TimeGenerated = TimeGenerated
  ) on Url
  // non-expiring indicators are allowed above, so they must survive this filter too
  | where isempty(ValidUntil) or Syslog_TimeGenerated < ValidUntil
  | summarize Syslog_TimeGenerated  = arg_max(Syslog_TimeGenerated , *) by Id, Url
  | extend Description = tostring(parse_json(Data).description)
  | extend IndicatorTags = tostring(parse_json(Data).labels)
  | project timestamp = Syslog_TimeGenerated, Description, IndicatorTags, Id, Type, ValidUntil, Confidence, SyslogMessage, Computer, ProcessName, Url, HostIP
queryFrequency: 1h
description: |
  'Identifies a match in Syslog data from any malicious URL indicator ingested from PRODAFT
  USTA IoC Threat Intelligence (SourceSystem starting with "PRODAFT USTA").'
id: 99ae4d06-d352-4926-a1d7-9c28e25b1313
triggerThreshold: 0
queryPeriod: 14d
version: 1.0.0
kind: Scheduled
relevantTechniques:
- T1071
severity: Medium
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: PRODAFTUstaIoCUploadIndicators
  dataTypes:
  - ThreatIntelIndicators
OriginalUri: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/PRODAFT%20USTA%20-%20IoC%20Threat%20Intelligence/Analytic%20Rules/TIMapUrlToSyslog.yaml
tactics:
- CommandAndControl
entityMappings:
- fieldMappings:
  - identifier: HostName
    columnName: Computer
  entityType: Host
- fieldMappings:
  - identifier: Address
    columnName: HostIP
  entityType: IP
- fieldMappings:
  - identifier: Url
    columnName: Url
  entityType: URL
{
  "$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/99ae4d06-d352-4926-a1d7-9c28e25b1313')]",
      "kind": "Scheduled",
      "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/99ae4d06-d352-4926-a1d7-9c28e25b1313')]",
      "properties": {
        "alertRuleTemplateName": "99ae4d06-d352-4926-a1d7-9c28e25b1313",
        "customDetails": null,
        "description": "'Identifies a match in Syslog data from any malicious URL indicator ingested from PRODAFT\nUSTA IoC Threat Intelligence (SourceSystem starting with \"PRODAFT USTA\").'\n",
        "displayName": "PRODAFT USTA - TI map URL to Syslog",
        "enabled": true,
        "entityMappings": [
          {
            "entityType": "Host",
            "fieldMappings": [
              {
                "columnName": "Computer",
                "identifier": "HostName"
              }
            ]
          },
          {
            "entityType": "IP",
            "fieldMappings": [
              {
                "columnName": "HostIP",
                "identifier": "Address"
              }
            ]
          },
          {
            "entityType": "URL",
            "fieldMappings": [
              {
                "columnName": "Url",
                "identifier": "Url"
              }
            ]
          }
        ],
        "OriginalUri": "https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/PRODAFT%20USTA%20-%20IoC%20Threat%20Intelligence/Analytic%20Rules/TIMapUrlToSyslog.yaml",
        "query": "let dt_lookBack = 1h;\nlet ioc_lookBack = 14d;\nThreatIntelIndicators\n// Each USTA IoC feed uploads under its own SourceSystem (\"PRODAFT USTA - Malicious URLs\", etc.)\n| where SourceSystem startswith \"PRODAFT USTA\"\n// ObservableKey is a STIX kv pair (\"url:value\") - take the type before the colon\n| extend IndicatorType = replace(@\"\\[|\\]|\\\"\"\", \"\", tostring(split(ObservableKey, \":\", 0)))\n| where IndicatorType == \"url\"\n| extend Url = 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| project-reorder *, Tags, TrafficLightProtocolLevel, Url, Type\n// innerunique: one match per indicator is enough to alert, and it keeps the join cheap\n| join kind=innerunique (\n  Syslog\n  | where TimeGenerated >= ago(dt_lookBack)\n  // Pull the first URL out of the free-text message; rows without one drop out of the join\n  | extend Url = extract(\"(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\\\(\\\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)\", 1,SyslogMessage)\n  | extend Syslog_TimeGenerated = TimeGenerated\n) on Url\n// non-expiring indicators are allowed above, so they must survive this filter too\n| where isempty(ValidUntil) or Syslog_TimeGenerated < ValidUntil\n| summarize Syslog_TimeGenerated  = arg_max(Syslog_TimeGenerated , *) by Id, Url\n| extend Description = tostring(parse_json(Data).description)\n| extend IndicatorTags = tostring(parse_json(Data).labels)\n| project timestamp = Syslog_TimeGenerated, Description, IndicatorTags, Id, Type, ValidUntil, Confidence, SyslogMessage, Computer, ProcessName, Url, HostIP\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"
    }
  ]
}