Detection rules › Kusto

Tailscale Premium: Large outbound transfer over tailnet

Status
available
Severity
medium
Time window
1h
Group by
Dst, DstNodeName, DstUser, NodeId, Proto, Src, SrcNodeName, SrcUser
Source
github.com/Azure/Azure-Sentinel

Identifies when a single src-dst pair transfers more than 100 MB over the tailnet within a 1-hour window. Large bursts can indicate data staging, exfiltration, or a misconfigured backup. Requires Tailscale Premium or Enterprise.

MITRE ATT&CK coverage

Rule body

id: d2e3f4a5-2b3c-4d5e-6f7a-8b9c0d1e2f3a
name: "Tailscale Premium: Large outbound transfer over tailnet"
description: |
  Identifies when a single src-dst pair transfers more than 100 MB over the tailnet within a 1-hour window. Large bursts can indicate data staging, exfiltration, or a misconfigured backup. Requires Tailscale Premium or Enterprise.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: TailscalePremiumCCF
    dataTypes:
      - Tailscale_Network_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
  - Collection
relevantTechniques:
  - T1041
  - T1020
query: |
  let bytesThreshold = 100 * 1024 * 1024;
  Tailscale_Network_CL
  | where TimeGenerated > ago(1h)
  | where HasVirtualTraffic
  | mv-expand t = VirtualTraffic
  | extend Src = tostring(t.src), Dst = tostring(t.dst), Proto = toint(t.proto), Bytes = tolong(t.txBytes) + tolong(t.rxBytes), Pkts = tolong(t.txPkts) + tolong(t.rxPkts)
  | summarize TotalBytes = sum(Bytes), TotalPackets = sum(Pkts) by NodeId, SrcNodeName, SrcUser, DstNodeName, DstUser, Src, Dst, Proto
  | where TotalBytes > bytesThreshold
  | extend TotalMB = round(TotalBytes / 1024.0 / 1024.0, 2)
  | order by TotalBytes desc
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SrcNodeName
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: DstNodeName
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: SrcUser
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: Src
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0

Stages and Predicates

Parameters

let bytesThreshold = 100 * 1024 * 1024;

Stage 1: source

Tailscale_Network_CL

Stage 2: where

| where TimeGenerated > ago(1h)

Stage 3: where

| where HasVirtualTraffic

Stage 4: mv-expand

| mv-expand t = VirtualTraffic

Stage 5: extend

| extend Src = tostring(t.src), Dst = tostring(t.dst), Proto = toint(t.proto), Bytes = tolong(t.txBytes) + tolong(t.rxBytes), Pkts = tolong(t.txPkts) + tolong(t.rxPkts)

Stage 6: summarize

| summarize TotalBytes = sum(Bytes), TotalPackets = sum(Pkts) by NodeId, SrcNodeName, SrcUser, DstNodeName, DstUser, Src, Dst, Proto

Stage 7: where

| where TotalBytes > bytesThreshold

Stage 8: extend

| extend TotalMB = round(TotalBytes / 1024.0 / 1024.0, 2)

Stage 9: sort

| order by TotalBytes desc

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
TotalBytescross_field_compare
  • bytesThreshold transforms: op:gt
field:"TotalBytes" kind:cross_field_compare value:"bytesThreshold"

Output fields

These fields are emitted when the rule matches.

FieldSource
Dstsummarize
DstNodeNamesummarize
DstUsersummarize
NodeIdsummarize
Protosummarize
Srcsummarize
SrcNodeNamesummarize
SrcUsersummarize
TotalBytessummarize
TotalPacketssummarize
TotalMBextend