Detection rules › Kusto

Whisper Security - Co-Hosted Malware Cluster Detection

Status
available
Severity
high
Time window
1d
Group by
cohostedCount, countries, indicator, parsedIp, registrar
Source
github.com/Azure/Azure-Sentinel

Identifies IP addresses that host multiple domains associated with malware. When an IP has more than 3 co-hosted domains and at least 3 of those domains are flagged as malware by Whisper threat intelligence, it strongly indicates compromised or adversary-controlled infrastructure.

MITRE ATT&CK coverage

TacticTechniques
Resource Development

Rule body

id: 374a77f4-23ed-55dc-8441-8e47a1e079e9
name: Whisper Security - Co-Hosted Malware Cluster Detection
description: |
  Identifies IP addresses that host multiple domains associated with malware. When an IP has more than 3 co-hosted domains and at least 3 of those domains are flagged as malware by Whisper threat intelligence, it strongly indicates compromised or adversary-controlled infrastructure.
severity: High
status: Available
requiredDataConnectors:
- connectorId: WhisperSecurityConnector
  dataTypes:
  - WhisperInfraContext_CL
  - WhisperThreatIntel_CL
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- ResourceDevelopment
relevantTechniques:
- T1584
query: |
  // MITRE ATT&CK: T1584.001 - Compromise Infrastructure: Domains
  // Tactic: Resource Development
  // Detects clusters of malware-hosting domains sharing the same IP infrastructure
  let minCohostedCount = 3;
  let minMalwareCoHosts = 3;
  let cohostedInfra = WhisperInfraContext_CL
      | where TimeGenerated > ago(1d)
      | where cohostedCount > minCohostedCount
      | extend parsedIp = tostring(split(ipAddresses, ",")[0])
      | project indicator, parsedIp, cohostedCount, countries, registrar;
  let malwareIndicators = WhisperThreatIntel_CL
      | where TimeGenerated > ago(1d)
      | where isMalware == true
      | project indicator, threatScore, threatLevel, feedNames;
  cohostedInfra
      | join kind=inner (malwareIndicators) on indicator
      | summarize malwareCoHostCount = dcount(indicator), DnsDomains = make_set(indicator, 10), avgThreatScore = avg(threatScore), maxThreatScore = max(threatScore) by parsedIp, cohostedCount, countries, registrar
      | where malwareCoHostCount >= minMalwareCoHosts
      | extend DnsDomain = tostring(DnsDomains[0]), IPAddress = parsedIp
      | project TimeGenerated = now(), DnsDomain, IPAddress, malwareCoHostCount, cohostedCount, avgThreatScore, maxThreatScore, countries, registrar, DnsDomains
entityMappings:
- entityType: DNS
  fieldMappings:
  - identifier: DomainName
    columnName: DnsDomain
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: IPAddress
customDetails:
  MalwareCoHostCount: malwareCoHostCount
  CoHostedCount: cohostedCount
  MaxThreatScore: maxThreatScore
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let minCohostedCount = 3;
let minMalwareCoHosts = 3;

let cohostedInfra is inlined into the numbered stages below.

Let binding: malwareIndicators used in Stage 6

let malwareIndicators = WhisperThreatIntel_CL
    | where TimeGenerated > ago(1d)
    | where isMalware == true
    | project indicator, threatScore, threatLevel, feedNames;

Stages 1 to 5 define let cohostedInfra (the rule's main pipeline source); stages 6 to 10 run on it.

Stage 1: source

WhisperInfraContext_CL

Stage 2: where

| where TimeGenerated > ago(1d)

Stage 3: where

| where cohostedCount > minCohostedCount

Stage 4: extend

| extend parsedIp = tostring(split(ipAddresses, ",")[0])

Stage 5: project

| project indicator, parsedIp, cohostedCount, countries, registrar

Stage 6: join

cohostedInfra
| join kind=inner (malwareIndicators) on indicator

Stage 7: summarize

| summarize malwareCoHostCount = dcount(indicator), DnsDomains = make_set(indicator, 10), avgThreatScore = avg(threatScore), maxThreatScore = max(threatScore) by parsedIp, cohostedCount, countries, registrar

Stage 8: where

| where malwareCoHostCount >= minMalwareCoHosts

Stage 9: extend

| extend DnsDomain = tostring(DnsDomains[0]), IPAddress = parsedIp

Stage 10: project

| project TimeGenerated = now(), DnsDomain, IPAddress, malwareCoHostCount, cohostedCount, avgThreatScore, maxThreatScore, countries, registrar, DnsDomains

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
DnsDomainproject
DnsDomainsproject
IPAddressproject
TimeGeneratedproject
avgThreatScoreproject
cohostedCountproject
countriesproject
malwareCoHostCountproject
maxThreatScoreproject
registrarproject