Detection rules › Kusto
Tailscale: Device started advertising subnet routes
Identifies when a tailnet device begins advertising subnet routes (subnet-router capability) not present in the previous snapshot. Unexpected advertisement may indicate a compromised node expanding reachable surface area or an unsanctioned admin change.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Lateral Movement |
Rule body
id: c2b3d4e5-2345-6789-01ab-cdef12345002
name: "Tailscale: Device started advertising subnet routes"
description: |
Identifies when a tailnet device begins advertising subnet routes (subnet-router capability) not present in the previous snapshot. Unexpected advertisement may indicate a compromised node expanding reachable surface area or an unsanctioned admin change.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: TailscaleCCF
dataTypes:
- Tailscale_Devices_CL
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- LateralMovement
- Persistence
relevantTechniques:
- T1021
- T1556
query: |
let recent =
Tailscale_Devices_CL
| where TimeGenerated > ago(1h)
| summarize arg_max(TimeGenerated, *) by DeviceId
| where array_length(AdvertisedRoutes) > 0;
let baseline =
Tailscale_Devices_CL
| where TimeGenerated between (ago(1d + 1h) .. ago(1h))
| summarize arg_max(TimeGenerated, *) by DeviceId
| where array_length(AdvertisedRoutes) > 0
| distinct DeviceId;
recent
| join kind=leftanti baseline on DeviceId
| project TimeGenerated, DeviceId, DeviceName, Hostname, User, AdvertisedRoutes, EnabledRoutes, LastSeen
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: Hostname
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: User
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: P1D
matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0
Stages and Predicates
let recent is inlined into the numbered stages below.
Let binding: baseline
let baseline = Tailscale_Devices_CL
| where TimeGenerated between (ago(1d + 1h) .. ago(1h))
| summarize arg_max(TimeGenerated, *) by DeviceId
| where array_length(AdvertisedRoutes) > 0
| distinct DeviceId;
Stages 1 to 4 define let recent (the rule's main pipeline source); stages 5 to 6 run on it.
Stage 1: source
Tailscale_Devices_CL
Stage 2: where
| where TimeGenerated > ago(1h)
Stage 3: summarize
| summarize arg_max(TimeGenerated, *) by DeviceId
Stage 4: where
| where array_length(AdvertisedRoutes) > 0
Stage 5: join (negated)
recent
| join kind=leftanti baseline on DeviceId
Stage 6: project
| project TimeGenerated, DeviceId, DeviceName, Hostname, User, AdvertisedRoutes, EnabledRoutes, LastSeen
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
AdvertisedRoutes | gt | 0 | excludes:AdvertisedRoutes field:"AdvertisedRoutes" value:"0" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AdvertisedRoutes | project |
DeviceId | project |
DeviceName | project |
EnabledRoutes | project |
Hostname | project |
LastSeen | project |
TimeGenerated | project |
User | project |