Detection rules › Kusto
Tailscale: External (shared-in) device added
Identifies new external (shared-in) devices joining the tailnet that were not present in the prior 24-hour baseline. Each shared-in device expands the trust boundary - confirm the share matches a documented agreement and ACL scope.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule body
id: b2c3d4e5-6789-0123-4567-890123450043
name: "Tailscale: External (shared-in) device added"
description: |
Identifies new external (shared-in) devices joining the tailnet that were not present in the prior 24-hour baseline. Each shared-in device expands the trust boundary - confirm the share matches a documented agreement and ACL scope.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: TailscaleCCF
dataTypes:
- Tailscale_Devices_CL
queryFrequency: 1h
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078
query: |
let recent =
Tailscale_Devices_CL
| where TimeGenerated > ago(1h)
| summarize arg_max(TimeGenerated, *) by DeviceId
| where IsExternal == true
| project DeviceId, DeviceName, Hostname, User, Os, ClientVersion, Created, LastSeen, Tags;
let prior =
Tailscale_Devices_CL
| where TimeGenerated between (ago(2d) .. ago(1h))
| summarize arg_max(TimeGenerated, *) by DeviceId
| where IsExternal == true
| distinct DeviceId;
recent
| join kind=leftanti prior on DeviceId
| project TimeGenerated = now(), DeviceName, Hostname, User, Os, ClientVersion, Created, LastSeen, Tags
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: prior
let prior = Tailscale_Devices_CL
| where TimeGenerated between (ago(2d) .. ago(1h))
| summarize arg_max(TimeGenerated, *) by DeviceId
| where IsExternal == true
| distinct DeviceId;
Stages 1 to 5 define let recent (the rule's main pipeline source); stages 6 to 7 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 IsExternal == true
Stage 5: project
| project DeviceId, DeviceName, Hostname, User, Os, ClientVersion, Created, LastSeen, Tags
Stage 6: join (negated)
recent
| join kind=leftanti prior on DeviceId
Stage 7: project
| project TimeGenerated = now(), DeviceName, Hostname, User, Os, ClientVersion, Created, LastSeen, Tags
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
IsExternal | eq | true | excludes:IsExternal field:"IsExternal" value:"true" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
IsExternal | eq |
| field:"IsExternal" kind:eq value:"true" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ClientVersion | project |
Created | project |
DeviceName | project |
Hostname | project |
LastSeen | project |
Os | project |
Tags | project |
TimeGenerated | project |
User | project |