Detection rules › Kusto

UniFi Site Manager: Data Connector Health

Status
available
Severity
medium
Time window
2h
Source
github.com/Azure/Azure-Sentinel

Identifies when UniFi Site Manager data connectors stop sending data, ensuring visibility into the UniFi estate is maintained.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment

Rule body

id: 1fb5c195-9bff-e18b-3e8f-5123f46bff9d
name: "UniFi Site Manager: Data Connector Health"
description: |
  Identifies when UniFi Site Manager data connectors stop sending data, ensuring visibility into the UniFi estate is maintained.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: UniFiSiteManagerConnectorDefinition
    dataTypes:
      - Unifi_SiteManager_Sites_CL
      - Unifi_SiteManager_Devices_CL
      - Unifi_SiteManager_Hosts_CL
      - Unifi_SiteManager_ISPMetrics_CL
queryFrequency: 15m
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562
subTechniques:
  - T1562.006
query: |
  // UniFi Data Connector Health Monitoring
  let StaleThreshold = 30;
  let DevicesHealth = Unifi_SiteManager_Devices_CL
  | where TimeGenerated > ago(2h)
  | summarize LastReceived = max(TimeGenerated)
  | extend TableName = "Unifi_SiteManager_Devices_CL", TableDisplay = "Devices";
  let HostsHealth = Unifi_SiteManager_Hosts_CL
  | where TimeGenerated > ago(2h)
  | summarize LastReceived = max(TimeGenerated)
  | extend TableName = "Unifi_SiteManager_Hosts_CL", TableDisplay = "Hosts";
  let SitesHealth = Unifi_SiteManager_Sites_CL
  | where TimeGenerated > ago(2h)
  | summarize LastReceived = max(TimeGenerated)
  | extend TableName = "Unifi_SiteManager_Sites_CL", TableDisplay = "Sites";
  let ISPHealth = Unifi_SiteManager_ISPMetrics_CL
  | where TimeGenerated > ago(2h)
  | summarize LastReceived = max(TimeGenerated)
  | extend TableName = "Unifi_SiteManager_ISPMetrics_CL", TableDisplay = "ISP Metrics";
  union DevicesHealth, HostsHealth, SitesHealth, ISPHealth
  | extend 
      MinutesSinceData = datetime_diff('minute', now(), LastReceived),
      Status = case(
          datetime_diff('minute', now(), LastReceived) <= 15, "Healthy",
          datetime_diff('minute', now(), LastReceived) <= 30, "Delayed",
          datetime_diff('minute', now(), LastReceived) <= 60, "Stale",
          "Critical"
      )
  | where MinutesSinceData >= StaleThreshold
  | extend TimeGenerated = now()
  | project
      TimeGenerated,
      TableName,
      TableDisplay,
      LastReceived,
      MinutesSinceData,
      Status
entityMappings:
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: TableDisplay
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT4H
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0

Stages and Predicates

Parameters

let StaleThreshold = 30;

union (4 sources)

Each leg below queries one source; the rule matches if any leg does. Sources: DevicesHealth, HostsHealth, SitesHealth, ISPHealth

Leg 1: DevicesHealth

Leg 2: HostsHealth

Leg 3: SitesHealth

Leg 4: ISPHealth

Applied to the combined result

| extend 
    MinutesSinceData = datetime_diff('minute', now(), LastReceived),
    Status = case(
        datetime_diff('minute', now(), LastReceived) <= 15, "Healthy",
        datetime_diff('minute', now(), LastReceived) <= 30, "Delayed",
        datetime_diff('minute', now(), LastReceived) <= 60, "Stale",
        "Critical"
    ) | where MinutesSinceData >= StaleThreshold | extend TimeGenerated = now() | project
    TimeGenerated,
    TableName,
    TableDisplay,
    LastReceived,
    MinutesSinceData,
    Status

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
LastReceivedproject
MinutesSinceDataproject
Statusproject
TableDisplayproject
TableNameproject
TimeGeneratedproject