Detection rules › Kusto

Tailscale: External (shared-in) device added

Status
available
Severity
medium
Time window
2d
Group by
DeviceId
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
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 used in Stage 6

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.

FieldKindExcluded valuesSearch
IsExternaleqtrueexcludes:IsExternal field:"IsExternal" value:"true"

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
ClientVersionproject
Createdproject
DeviceNameproject
Hostnameproject
LastSeenproject
Osproject
Tagsproject
TimeGeneratedproject
Userproject