diff --git a/.gitignore b/.gitignore index 8018dfa7359..512ba0d6a37 100644 --- a/.gitignore +++ b/.gitignore @@ -355,3 +355,6 @@ Hunting Queries/DeployedQueries.json .arm-ttk + +# Local-only helper scripts (not for upstream submission) +.local-helpers/ diff --git a/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Audit_CL.json b/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Audit_CL.json new file mode 100644 index 00000000000..87e3511ef41 --- /dev/null +++ b/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Audit_CL.json @@ -0,0 +1,16 @@ +{ "Name": "Tailscale_Audit_CL", +"Properties":[ + { "Name": "TenantId", "Type": "string" }, + { "Name": "SourceSystem", "Type": "string" }, + { "Name": "TimeGenerated", "Type": "datetime" }, + { "Name": "EventTime", "Type": "datetime" }, + { "Name": "EventGroupID", "Type": "string" }, + { "Name": "EventType", "Type": "string" }, + { "Name": "ActionDetails", "Type": "string" }, + { "Name": "Actor", "Type": "dynamic" }, + { "Name": "Action", "Type": "string" }, + { "Name": "Target", "Type": "dynamic" }, + { "Name": "Origin", "Type": "dynamic" }, + { "Name": "New", "Type": "dynamic" }, + { "Name": "Old", "Type": "dynamic" } +]} diff --git a/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Devices_CL.json b/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Devices_CL.json new file mode 100644 index 00000000000..f3a11a7bb79 --- /dev/null +++ b/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Devices_CL.json @@ -0,0 +1,121 @@ +{ + "Name": "Tailscale_Devices_CL", + "Properties": [ + { + "Name": "TenantId", + "Type": "string" + }, + { + "Name": "SourceSystem", + "Type": "string" + }, + { + "Name": "TimeGenerated", + "Type": "datetime" + }, + { + "Name": "DeviceId", + "Type": "string" + }, + { + "Name": "DeviceName", + "Type": "string" + }, + { + "Name": "Hostname", + "Type": "string" + }, + { + "Name": "User", + "Type": "string" + }, + { + "Name": "Os", + "Type": "string" + }, + { + "Name": "ClientVersion", + "Type": "string" + }, + { + "Name": "UpdateAvailable", + "Type": "bool" + }, + { + "Name": "Authorized", + "Type": "bool" + }, + { + "Name": "IsExternal", + "Type": "bool" + }, + { + "Name": "Created", + "Type": "datetime" + }, + { + "Name": "LastSeen", + "Type": "datetime" + }, + { + "Name": "Expires", + "Type": "datetime" + }, + { + "Name": "KeyExpiryDisabled", + "Type": "bool" + }, + { + "Name": "BlocksIncomingConnections", + "Type": "bool" + }, + { + "Name": "Addresses", + "Type": "dynamic" + }, + { + "Name": "Tags", + "Type": "dynamic" + }, + { + "Name": "EnabledRoutes", + "Type": "dynamic" + }, + { + "Name": "AdvertisedRoutes", + "Type": "dynamic" + }, + { + "Name": "ClientConnectivity", + "Type": "dynamic" + }, + { + "Name": "MachineKey", + "Type": "string" + }, + { + "Name": "NodeKey", + "Type": "string" + }, + { + "Name": "Distro", + "Type": "string" + }, + { + "Name": "SshEnabled", + "Type": "bool" + }, + { + "Name": "ConnectedToControl", + "Type": "bool" + }, + { + "Name": "TailnetLockKey", + "Type": "string" + }, + { + "Name": "TailnetLockError", + "Type": "string" + } + ] +} \ No newline at end of file diff --git a/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Dns_CL.json b/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Dns_CL.json new file mode 100644 index 00000000000..99d9b50465f --- /dev/null +++ b/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Dns_CL.json @@ -0,0 +1,10 @@ +{ "Name": "Tailscale_Dns_CL", +"Properties":[ + { "Name": "TenantId", "Type": "string" }, + { "Name": "SourceSystem", "Type": "string" }, + { "Name": "TimeGenerated", "Type": "datetime" }, + { "Name": "ConfigType", "Type": "string" }, + { "Name": "Nameservers", "Type": "dynamic" }, + { "Name": "MagicDNS", "Type": "bool" }, + { "Name": "SearchPaths", "Type": "dynamic" } +]} diff --git a/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Keys_CL.json b/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Keys_CL.json new file mode 100644 index 00000000000..29cd0280364 --- /dev/null +++ b/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Keys_CL.json @@ -0,0 +1,53 @@ +{ + "Name": "Tailscale_Keys_CL", + "Properties": [ + { + "Name": "TenantId", + "Type": "string" + }, + { + "Name": "SourceSystem", + "Type": "string" + }, + { + "Name": "TimeGenerated", + "Type": "datetime" + }, + { + "Name": "KeyId", + "Type": "string" + }, + { + "Name": "Description", + "Type": "string" + }, + { + "Name": "UserId", + "Type": "string" + }, + { + "Name": "Created", + "Type": "datetime" + }, + { + "Name": "Expires", + "Type": "datetime" + }, + { + "Name": "Revoked", + "Type": "datetime" + }, + { + "Name": "Capabilities", + "Type": "dynamic" + }, + { + "Name": "KeyType", + "Type": "string" + }, + { + "Name": "ExpirySeconds", + "Type": "int" + } + ] +} \ No newline at end of file diff --git a/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Network_CL.json b/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Network_CL.json new file mode 100644 index 00000000000..9f076e31472 --- /dev/null +++ b/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Network_CL.json @@ -0,0 +1,32 @@ +{ "Name": "Tailscale_Network_CL", +"Properties":[ + { "Name": "TenantId", "Type": "string" }, + { "Name": "SourceSystem", "Type": "string" }, + { "Name": "TimeGenerated", "Type": "datetime" }, + { "Name": "NodeId", "Type": "string" }, + { "Name": "FlowStart", "Type": "datetime" }, + { "Name": "FlowEnd", "Type": "datetime" }, + { "Name": "SrcNode", "Type": "dynamic" }, + { "Name": "SrcUser", "Type": "string" }, + { "Name": "SrcNodeName", "Type": "string" }, + { "Name": "SrcOs", "Type": "string" }, + { "Name": "SrcTags", "Type": "dynamic" }, + { "Name": "SrcAddresses", "Type": "dynamic" }, + { "Name": "DstNodes", "Type": "dynamic" }, + { "Name": "DstCount", "Type": "int" }, + { "Name": "DstNodeId", "Type": "string" }, + { "Name": "DstNodeName", "Type": "string" }, + { "Name": "DstUser", "Type": "string" }, + { "Name": "DstOs", "Type": "string" }, + { "Name": "DstTags", "Type": "dynamic" }, + { "Name": "DstAddresses", "Type": "dynamic" }, + { "Name": "VirtualTraffic", "Type": "dynamic" }, + { "Name": "SubnetTraffic", "Type": "dynamic" }, + { "Name": "ExitTraffic", "Type": "dynamic" }, + { "Name": "PhysicalTraffic", "Type": "dynamic" }, + { "Name": "HasVirtualTraffic", "Type": "bool" }, + { "Name": "HasSubnetTraffic", "Type": "bool" }, + { "Name": "HasExitTraffic", "Type": "bool" }, + { "Name": "HasPhysicalTraffic", "Type": "bool" }, + { "Name": "IsRelayed", "Type": "bool" } +]} diff --git a/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_PostureIntegrations_CL.json b/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_PostureIntegrations_CL.json new file mode 100644 index 00000000000..482c4e98769 --- /dev/null +++ b/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_PostureIntegrations_CL.json @@ -0,0 +1,13 @@ +{ "Name": "Tailscale_PostureIntegrations_CL", +"Properties":[ + { "Name": "TenantId", "Type": "string" }, + { "Name": "SourceSystem", "Type": "string" }, + { "Name": "TimeGenerated", "Type": "datetime" }, + { "Name": "IntegrationId", "Type": "string" }, + { "Name": "Provider", "Type": "string" }, + { "Name": "CloudId", "Type": "string" }, + { "Name": "ClientId", "Type": "string" }, + { "Name": "TenantId_Provider", "Type": "string" }, + { "Name": "ConfigOverwrites", "Type": "dynamic" }, + { "Name": "Status", "Type": "dynamic" } +]} diff --git a/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Settings_CL.json b/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Settings_CL.json new file mode 100644 index 00000000000..cc53edf1a55 --- /dev/null +++ b/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Settings_CL.json @@ -0,0 +1,14 @@ +{ "Name": "Tailscale_Settings_CL", +"Properties":[ + { "Name": "TenantId", "Type": "string" }, + { "Name": "SourceSystem", "Type": "string" }, + { "Name": "TimeGenerated", "Type": "datetime" }, + { "Name": "DevicesApprovalOn", "Type": "bool" }, + { "Name": "DevicesAutoUpdatesOn", "Type": "bool" }, + { "Name": "DevicesKeyDurationDays", "Type": "int" }, + { "Name": "UsersApprovalOn", "Type": "bool" }, + { "Name": "UsersRoleAllowedToJoinExternalTailnets", "Type": "string" }, + { "Name": "NetworkFlowLoggingOn", "Type": "bool" }, + { "Name": "RegionalRoutingOn", "Type": "bool" }, + { "Name": "PostureIdentityCollectionOn", "Type": "bool" } +]} diff --git a/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Users_CL.json b/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Users_CL.json new file mode 100644 index 00000000000..ed74a89c266 --- /dev/null +++ b/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Users_CL.json @@ -0,0 +1,18 @@ +{ "Name": "Tailscale_Users_CL", +"Properties":[ + { "Name": "TenantId", "Type": "string" }, + { "Name": "SourceSystem", "Type": "string" }, + { "Name": "TimeGenerated", "Type": "datetime" }, + { "Name": "UserId", "Type": "string" }, + { "Name": "DisplayName", "Type": "string" }, + { "Name": "LoginName", "Type": "string" }, + { "Name": "TailnetId", "Type": "string" }, + { "Name": "UserType", "Type": "string" }, + { "Name": "Role", "Type": "string" }, + { "Name": "Status", "Type": "string" }, + { "Name": "DeviceCount", "Type": "int" }, + { "Name": "Created", "Type": "datetime" }, + { "Name": "LastSeen", "Type": "datetime" }, + { "Name": "CurrentlyConnected", "Type": "bool" }, + { "Name": "ProfilePicUrl", "Type": "string" } +]} diff --git a/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Webhooks_CL.json b/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Webhooks_CL.json new file mode 100644 index 00000000000..422a80a7134 --- /dev/null +++ b/.script/tests/KqlvalidationsTests/CustomTables/Tailscale_Webhooks_CL.json @@ -0,0 +1,13 @@ +{ "Name": "Tailscale_Webhooks_CL", +"Properties":[ + { "Name": "TenantId", "Type": "string" }, + { "Name": "SourceSystem", "Type": "string" }, + { "Name": "TimeGenerated", "Type": "datetime" }, + { "Name": "EndpointId", "Type": "string" }, + { "Name": "EndpointUrl", "Type": "string" }, + { "Name": "ProviderType", "Type": "string" }, + { "Name": "CreatorLoginName", "Type": "string" }, + { "Name": "Created", "Type": "datetime" }, + { "Name": "LastModified", "Type": "datetime" }, + { "Name": "Subscriptions", "Type": "dynamic" } +]} diff --git a/.script/tests/detectionTemplateSchemaValidation/ValidConnectorIds.json b/.script/tests/detectionTemplateSchemaValidation/ValidConnectorIds.json index b002df9e16b..afd5096c2bb 100644 --- a/.script/tests/detectionTemplateSchemaValidation/ValidConnectorIds.json +++ b/.script/tests/detectionTemplateSchemaValidation/ValidConnectorIds.json @@ -314,5 +314,6 @@ "UniFiSiteManagerConnectorDefinition", "UtimacoESKMConnector", "GSDetectionAlerts", - "VaikoraSecurityCenter" + "TailscaleCCF", + "TailscalePremiumCCF" ] diff --git a/Logos/Tailscale.svg b/Logos/Tailscale.svg new file mode 100644 index 00000000000..fdd0d65fbae --- /dev/null +++ b/Logos/Tailscale.svg @@ -0,0 +1 @@ + diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleAuthkeycreated.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleAuthkeycreated.yaml new file mode 100644 index 00000000000..8c662aea001 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleAuthkeycreated.yaml @@ -0,0 +1,42 @@ +id: 6b052c8d-5de8-eab0-1956-69a297765a32 +name: "Tailscale: Auth key created" +description: | + Identifies when a new Tailscale auth key is generated. Auth keys allow unattended device enrollment into the tailnet - confirm it was expected and revoke if not. +severity: Low +status: Available +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Audit_CL +queryFrequency: 15m +queryPeriod: 15m +triggerOperator: gt +triggerThreshold: 0 +tactics: + - Persistence +relevantTechniques: + - T1098 +query: | + Tailscale_Audit_CL + | where Action == "CREATE" + | where tostring(Target.type) == "AUTH_KEY" + | extend ActorLogin = tostring(Actor.loginName) + | extend KeyDescription = tostring(New.description) + | extend Reusable = tostring(New.reusable) + | extend Ephemeral = tostring(New.ephemeral) + | project TimeGenerated, ActorLogin, KeyDescription, Reusable, Ephemeral, Origin, New +entityMappings: + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: ActorLogin +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 5h + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleDeviceAdvertisingSubnetRoutes.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleDeviceAdvertisingSubnetRoutes.yaml new file mode 100644 index 00000000000..0854c997cb2 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleDeviceAdvertisingSubnetRoutes.yaml @@ -0,0 +1,54 @@ +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: 1d + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleDeviceKeyExpiringSoon.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleDeviceKeyExpiringSoon.yaml new file mode 100644 index 00000000000..e83535000b4 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleDeviceKeyExpiringSoon.yaml @@ -0,0 +1,46 @@ +id: b1a2c3d4-1234-5678-90ab-cdef12345001 +name: "Tailscale: Device key expiring within 7 days" +description: | + Identifies tailnet devices whose machine key expires within the next 7 days and where key expiry is not disabled. Surface proactively so renewal can be scheduled rather than forced during an outage. +severity: Medium +status: Available +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Devices_CL +queryFrequency: 6h +queryPeriod: 6h +triggerOperator: gt +triggerThreshold: 0 +tactics: + - InitialAccess +relevantTechniques: + - T1078 +query: | + Tailscale_Devices_CL + | where TimeGenerated > ago(6h) + | summarize arg_max(TimeGenerated, *) by DeviceId + | where KeyExpiryDisabled == false + | where isnotnull(Expires) + | where Expires between (now() .. now() + 7d) + | extend DaysToExpiry = datetime_diff('day', Expires, now()) + | project TimeGenerated, DeviceName, Hostname, User, Os, DaysToExpiry, Expires, 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: 1d + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleDeviceSshNewlyEnabled.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleDeviceSshNewlyEnabled.yaml new file mode 100644 index 00000000000..85f3e59513f --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleDeviceSshNewlyEnabled.yaml @@ -0,0 +1,55 @@ +id: f0a1b2c3-4567-8901-23de-f12345670041 +name: "Tailscale: Device Tailscale SSH newly enabled" +description: | + Identifies when Tailscale SSH is enabled on a device that previously did not have it. SSH provides authenticated shell access over the tailnet using Tailscale identity, broadening attack surface if unexpected. Verify and confirm the SSH ACL covers it. +severity: Medium +status: Available +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Devices_CL +queryFrequency: 1h +queryPeriod: 2d +triggerOperator: gt +triggerThreshold: 0 +tactics: + - Persistence + - LateralMovement +relevantTechniques: + - T1021 + - T1098 +query: | + let recent = + Tailscale_Devices_CL + | where TimeGenerated > ago(1h) + | summarize arg_max(TimeGenerated, *) by DeviceId + | where SshEnabled == true + | project DeviceId, DeviceName, Hostname, User, Os, ClientVersion, LastSeen; + let prior = + Tailscale_Devices_CL + | where TimeGenerated between (ago(2d) .. ago(1h)) + | summarize arg_max(TimeGenerated, *) by DeviceId + | where SshEnabled == true + | distinct DeviceId; + recent + | join kind=leftanti prior on DeviceId + | project TimeGenerated = now(), DeviceName, Hostname, User, Os, ClientVersion, 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: 1d + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleDnsNameserversModified.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleDnsNameserversModified.yaml new file mode 100644 index 00000000000..d2dcdf84705 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleDnsNameserversModified.yaml @@ -0,0 +1,46 @@ +id: c5d6e7f8-2345-6789-01ab-cdef12345011 +name: "Tailscale: DNS nameservers modified" +description: | + Identifies when the tailnet's global DNS nameserver list is modified. Adding an attacker-controlled resolver as a tailnet-wide nameserver enables broad DNS hijacking for every device using MagicDNS resolution. +severity: High +status: Available +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Audit_CL +queryFrequency: 15m +queryPeriod: 15m +triggerOperator: gt +triggerThreshold: 0 +tactics: + - DefenseEvasion + - CommandAndControl +relevantTechniques: + - T1556 + - T1568 +query: | + Tailscale_Audit_CL + | where Action == "UPDATE" + | where tostring(Target.type) == "TAILNET" + | where tostring(Target.property) == "DNS_NAMESERVERS" + | extend ActorLogin = tostring(Actor.loginName) + | extend OldServers = Old.dns + | extend NewServers = New.dns + | extend Added = set_difference(NewServers, OldServers) + | extend Removed = set_difference(OldServers, NewServers) + | project TimeGenerated, ActorLogin, OldServers, NewServers, Added, Removed, Origin +entityMappings: + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: ActorLogin +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 1d + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleExitnodeadvertisedorapproved.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleExitnodeadvertisedorapproved.yaml new file mode 100644 index 00000000000..1c1bf64eceb --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleExitnodeadvertisedorapproved.yaml @@ -0,0 +1,45 @@ +id: f42f2906-c8e6-23d0-e48c-0620e50d5510 +name: "Tailscale: Exit node advertised or approved" +description: | + Identifies when a device starts advertising itself as an exit node, or when an admin approves one. Validate the device and operator - rogue exit nodes can intercept tailnet egress. +severity: Low +status: Available +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Audit_CL +queryFrequency: 30m +queryPeriod: 30m +triggerOperator: gt +triggerThreshold: 0 +tactics: + - CommandAndControl + - Exfiltration +relevantTechniques: + - T1090 +query: | + Tailscale_Audit_CL + | where Action contains "EXIT" or tostring(New.advertisedExitNode) == "true" or tostring(New.allowedExitNode) == "true" + | extend ActorLogin = tostring(Actor.loginName) + | extend NodeName = tostring(Target.name) + | extend NodeId = tostring(Target.id) + | project TimeGenerated, ActorLogin, Action, NodeName, NodeId, Origin, New, Old +entityMappings: + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: ActorLogin + - entityType: Host + fieldMappings: + - identifier: HostName + columnName: NodeName +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 5h + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleExternalDeviceAdded.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleExternalDeviceAdded.yaml new file mode 100644 index 00000000000..ab926f2c861 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleExternalDeviceAdded.yaml @@ -0,0 +1,53 @@ +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: 1d + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleMagicDnsDisabled.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleMagicDnsDisabled.yaml new file mode 100644 index 00000000000..182e161b57f --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleMagicDnsDisabled.yaml @@ -0,0 +1,43 @@ +id: f8a9b0c1-4567-8901-23ab-cdef12345020 +name: "Tailscale: MagicDNS disabled" +description: | + Identifies when MagicDNS is turned off on the tailnet. Disabling MagicDNS changes DNS behaviour for every device and is occasionally a precursor to wider DNS hijacking - verify the change was intentional. +severity: Medium +status: Available +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Audit_CL +queryFrequency: 15m +queryPeriod: 15m +triggerOperator: gt +triggerThreshold: 0 +tactics: + - DefenseEvasion +relevantTechniques: + - T1556 +query: | + Tailscale_Audit_CL + | where Action == "UPDATE" + | where tostring(Target.type) == "TAILNET" + | where tostring(Target.property) == "MAGICDNS_PREFERENCES" + | extend ActorLogin = tostring(Actor.loginName) + | extend OldMagicDns = tobool(Old.magicDNS) + | extend NewMagicDns = tobool(New.magicDNS) + | where OldMagicDns == true and NewMagicDns == false + | project TimeGenerated, ActorLogin, OldMagicDns, NewMagicDns, Origin +entityMappings: + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: ActorLogin +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 1d + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleMasscredentialrevocationinshortwindow.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleMasscredentialrevocationinshortwindow.yaml new file mode 100644 index 00000000000..e82b106dba2 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleMasscredentialrevocationinshortwindow.yaml @@ -0,0 +1,48 @@ +id: f817e2fa-6fa0-fc25-5369-cef9b58771af +name: "Tailscale: Mass credential revocation in short window" +description: | + Identifies when five or more API keys, OAuth clients, or auth keys are revoked or deleted within one hour. May be routine rotation, or a typical cleanup pattern after credential compromise. +severity: High +status: Available +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Audit_CL +queryFrequency: 1h +queryPeriod: 1h +triggerOperator: gt +triggerThreshold: 0 +tactics: + - DefenseEvasion + - Impact +relevantTechniques: + - T1070 +query: | + Tailscale_Audit_CL + | where Action in ("REVOKE", "DELETE") + | where tostring(Target.type) in ("API_KEY", "OAUTH_CLIENT", "AUTH_KEY") + | extend ActorLogin = tostring(Actor.loginName) + | summarize + RevokedCount = count(), + TargetTypes = make_set(tostring(Target.type)), + TargetIds = make_set(tostring(Target.id)), + FirstEvent = min(TimeGenerated), + LastEvent = max(TimeGenerated) + by ActorLogin, bin(TimeGenerated, 1h) + | where RevokedCount >= 5 + | project TimeGenerated = LastEvent, ActorLogin, RevokedCount, TargetTypes, TargetIds, FirstEvent, LastEvent +entityMappings: + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: ActorLogin +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 5h + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleNewAPIaccesstokenorOAuthclientcreated.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleNewAPIaccesstokenorOAuthclientcreated.yaml new file mode 100644 index 00000000000..8bf479f5443 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleNewAPIaccesstokenorOAuthclientcreated.yaml @@ -0,0 +1,43 @@ +id: 668b43fd-cf28-961a-85af-957850df5027 +name: "Tailscale: New API access token or OAuth client created" +description: | + Identifies when a new API access token or OAuth client is created in the tailnet. These grant programmatic access - verify the actor and intent. +severity: Medium +status: Available +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Audit_CL +queryFrequency: 15m +queryPeriod: 15m +triggerOperator: gt +triggerThreshold: 0 +tactics: + - Persistence + - CredentialAccess +relevantTechniques: + - T1098 + - T1136 +query: | + Tailscale_Audit_CL + | where Action == "CREATE" + | where tostring(Target.type) in ("API_KEY", "OAUTH_CLIENT") + | extend ActorLogin = tostring(Actor.loginName) + | extend TargetName = tostring(Target.name) + | extend TargetId = tostring(Target.id) + | project TimeGenerated, ActorLogin, Action, TargetName, TargetId, Origin, New +entityMappings: + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: ActorLogin +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 5h + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleOAuthClientCreatedWithWriteScopes.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleOAuthClientCreatedWithWriteScopes.yaml new file mode 100644 index 00000000000..66b8165a34f --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleOAuthClientCreatedWithWriteScopes.yaml @@ -0,0 +1,53 @@ +id: 7237a848-30f2-499b-9ad5-024aea1288bd +name: "Tailscale: OAuth client or API key created with write scopes" +description: Identifies creation of a Tailscale OAuth client or API access key whose granted scopes include WRITE permissions (anything matching :write). Tokens with write scopes are high-value adversary targets. +description-detailed: | + Detects creation of a Tailscale OAuth client or API access key whose granted scopes include WRITE permissions (anything matching ":write"). Tokens with write scopes can modify tailnet configuration, manage devices, write ACLs, and revoke keys - high-value adversary targets. Compare against the recent actor history; revoke immediately if unexpected. +severity: High +status: Available +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Audit_CL + - connectorId: TailscalePremiumCCF + dataTypes: + - Tailscale_Audit_CL +queryFrequency: 15m +queryPeriod: 15m +triggerOperator: gt +triggerThreshold: 0 +tactics: + - Persistence + - PrivilegeEscalation +relevantTechniques: + - T1098 + - T1136 +query: | + Tailscale_Audit_CL + | where EventType == "CONFIG" + | where Action == "CREATE" + | where tostring(Target.type) in ("API_KEY", "OAUTH_CLIENT") + | extend Scopes = extract(@"scopes\s*-\s*(.+)$", 1, ActionDetails) + | where Scopes contains ":write" + | extend WriteScopes = extract_all(@"([a-zA-Z_]+:write)", Scopes) + | extend ActorLogin = tostring(Actor.loginName) + | extend ActorType = tostring(Actor.type) + | extend TargetName = tostring(Target.name) + | extend TargetId = tostring(Target.id) + | extend TargetType = tostring(Target.type) + | project TimeGenerated, ActorLogin, ActorType, Action, TargetType, TargetName, TargetId, WriteScopes, Scopes, Origin, ActionDetails +entityMappings: + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: ActorLogin +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 5h + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePolicyfileACLmodified.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePolicyfileACLmodified.yaml new file mode 100644 index 00000000000..155d98440b7 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePolicyfileACLmodified.yaml @@ -0,0 +1,41 @@ +id: 1e7249c2-1a9d-05fd-45cb-c859eef5b8ae +name: "Tailscale: Policy file (ACL) modified" +description: | + Identifies when the tailnet ACL/policy file is modified. Review the diff - incorrect ACLs can silently expand blast radius across the tailnet. +severity: Medium +status: Available +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Audit_CL +queryFrequency: 15m +queryPeriod: 15m +triggerOperator: gt +triggerThreshold: 0 +tactics: + - DefenseEvasion + - Persistence +relevantTechniques: + - T1556 +query: | + Tailscale_Audit_CL + | where Action == "UPDATE" + | where tostring(Target.type) == "TAILNET" + | where tostring(Target.property) == "ACL" + | extend ActorLogin = tostring(Actor.loginName) + | project TimeGenerated, ActorLogin, Action, Target, Origin, Old, New +entityMappings: + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: ActorLogin +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 5h + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumBeaconingDetected.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumBeaconingDetected.yaml new file mode 100644 index 00000000000..194f9881724 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumBeaconingDetected.yaml @@ -0,0 +1,69 @@ +id: e3f4a5b6-3c4d-5e6f-7a8b-9c0d1e2f3a4b +name: "Tailscale Premium: Network flow beaconing detected" +description: | + Identifies when flows between a src-dst pair recur at a regular interval (80%+ of inter-flow gaps cluster on the same delta over 10+ flows). Signature of C2 beaconing or scheduled exfiltration. Requires Tailscale Premium or Enterprise. +severity: Medium +status: Available +requiredDataConnectors: + - connectorId: TailscalePremiumCCF + dataTypes: + - Tailscale_Network_CL +queryFrequency: 1h +queryPeriod: 2d +triggerOperator: gt +triggerThreshold: 0 +tactics: + - CommandAndControl + - Exfiltration +relevantTechniques: + - T1071 + - T1095 + - T1029 +query: | + let lookback = 2d; + let minFlows = 10; + let beaconPercentThreshold = 80.0; + Tailscale_Network_CL + | where TimeGenerated > ago(lookback) + | where HasVirtualTraffic + | mv-expand t = VirtualTraffic + | extend Src = tostring(t.src), Dst = tostring(t.dst), Proto = toint(t.proto) + | project TimeGenerated, Src, Dst, Proto, SrcNodeName, SrcUser, DstNodeName, DstUser + | sort by Src asc, Dst asc, Proto asc, TimeGenerated asc + | serialize + | extend NextTime = next(TimeGenerated), NextSrc = next(Src), NextDst = next(Dst), NextProto = next(Proto) + | where Src == NextSrc and Dst == NextDst and Proto == NextProto + | extend DeltaSec = datetime_diff('second', NextTime, TimeGenerated) + | where DeltaSec > 5 + | summarize DeltaCount = count() by Src, Dst, Proto, DeltaSec, SrcNodeName, SrcUser, DstNodeName, DstUser + | summarize (MostFrequentDeltaCount, MostFrequentDeltaSec) = arg_max(DeltaCount, DeltaSec), TotalFlows = sum(DeltaCount) by Src, Dst, Proto, SrcNodeName, SrcUser, DstNodeName, DstUser + | where TotalFlows >= minFlows + | extend BeaconPercent = round(MostFrequentDeltaCount * 100.0 / TotalFlows, 1) + | where BeaconPercent >= beaconPercentThreshold +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: 1d + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumDerpRelaySurge.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumDerpRelaySurge.yaml new file mode 100644 index 00000000000..2b7194b412f --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumDerpRelaySurge.yaml @@ -0,0 +1,52 @@ +id: 0a1c8d12-e7d3-4890-8b89-8d6dbc1be2f0 +name: "Tailscale Premium: DERP relay traffic surge" +description: Identifies when a source node has more than 75 percent of its recent flows falling back to a DERP relay (Tailscale IsRelayed flag, traffic via 127.3.3.40). Operational signal useful for spotting policy drift. +description-detailed: | + Identifies when a source node has more than 75 percent of its recent flows falling back to a DERP relay (Tailscale's IsRelayed flag, traffic via 127.3.3.40). Sustained high relay rate indicates direct WireGuard peer-to-peer is failing - causes include NAT/firewall changes, a network blocking UDP 41641, or potential evasion attempts. Operational signal but useful for spotting policy drift. Requires Tailscale Premium or Enterprise. +severity: Low +status: Available +requiredDataConnectors: + - connectorId: TailscalePremiumCCF + dataTypes: + - Tailscale_Network_CL +queryFrequency: 15m +queryPeriod: 15m +triggerOperator: gt +triggerThreshold: 0 +tactics: + - CommandAndControl +relevantTechniques: + - T1572 +query: | + let minFlows = 20; + let relayedPctThreshold = 75.0; + Tailscale_Network_CL + | where TimeGenerated > ago(15m) + | summarize + TotalFlows = count(), + RelayedFlows = countif(IsRelayed) + by SrcNodeName, SrcUser, SrcOs, SrcTags=tostring(SrcTags) + | where TotalFlows >= minFlows + | extend RelayedPct = round(100.0 * RelayedFlows / TotalFlows, 1) + | where RelayedPct > relayedPctThreshold + | project SrcNodeName, SrcUser, SrcOs, SrcTags, TotalFlows, RelayedFlows, RelayedPct + | order by RelayedPct desc +entityMappings: + - entityType: Host + fieldMappings: + - identifier: HostName + columnName: SrcNodeName + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: SrcUser +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 6h + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumLargeOutboundTransfer.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumLargeOutboundTransfer.yaml new file mode 100644 index 00000000000..cfd89a96d1c --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumLargeOutboundTransfer.yaml @@ -0,0 +1,58 @@ +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: 5h + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumMassFanOut.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumMassFanOut.yaml new file mode 100644 index 00000000000..5695dd4f8ec --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumMassFanOut.yaml @@ -0,0 +1,54 @@ +id: f4a5b6c7-4d5e-6f7a-8b9c-0d1e2f3a4b5c +name: "Tailscale Premium: Mass fan-out from single node" +description: | + Identifies when a single node initiates flows to 25 or more unique destinations within a 15-minute window. Sudden fan-out is consistent with port scanning, lateral discovery, or worm-style propagation. Requires Tailscale Premium or Enterprise. +severity: High +status: Available +requiredDataConnectors: + - connectorId: TailscalePremiumCCF + dataTypes: + - Tailscale_Network_CL +queryFrequency: 15m +queryPeriod: 15m +triggerOperator: gt +triggerThreshold: 0 +tactics: + - Discovery + - LateralMovement +relevantTechniques: + - T1018 + - T1021 + - T1046 +query: | + let dstThreshold = 25; + Tailscale_Network_CL + | where TimeGenerated > ago(15m) + | where HasVirtualTraffic + | mv-expand t = VirtualTraffic + | extend Src = tostring(t.src), Dst = tostring(t.dst) + | summarize UniqueDestinations = dcount(Dst), TopDestinations = make_set(Dst, 25) by NodeId, SrcNodeName, SrcUser, SrcOs, SrcTags=tostring(SrcTags), Src + | where UniqueDestinations >= dstThreshold + | order by UniqueDestinations desc +entityMappings: + - entityType: Host + fieldMappings: + - identifier: HostName + columnName: SrcNodeName + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: SrcUser + - entityType: IP + fieldMappings: + - identifier: Address + columnName: Src +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 5h + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumNewPostureIntegration.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumNewPostureIntegration.yaml new file mode 100644 index 00000000000..0d705f006f0 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumNewPostureIntegration.yaml @@ -0,0 +1,42 @@ +id: b2c3d4e5-6789-0123-45ab-cdef12345031 +name: "Tailscale Premium: New posture integration added" +description: Identifies when a new device-posture integration is added to the tailnet (Jamf, Kandji, Intune, Kolide, Defender for Endpoint, CrowdStrike, SentinelOne). Verify the addition was sanctioned. +description-detailed: | + Identifies when a new device-posture integration is added to the tailnet (Jamf, Kandji, Intune, Kolide, Defender for Endpoint, CrowdStrike, SentinelOne, etc.). Unexpected additions may indicate an attacker establishing a control plane or bypassing compliance gates. Requires Tailscale Premium or Enterprise. +severity: Medium +status: Available +requiredDataConnectors: + - connectorId: TailscalePremiumCCF + dataTypes: + - Tailscale_Audit_CL +queryFrequency: 15m +queryPeriod: 15m +triggerOperator: gt +triggerThreshold: 0 +tactics: + - Persistence +relevantTechniques: + - T1098 +query: | + Tailscale_Audit_CL + | where Action == "CREATE" + | where tostring(Target.type) == "POSTURE_INTEGRATION" + or tostring(Target.type) startswith "POSTURE" + | extend ActorLogin = tostring(Actor.loginName) + | extend Provider = tostring(Target.name) + | project TimeGenerated, ActorLogin, Provider, Target, New, Origin +entityMappings: + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: ActorLogin +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 1d + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumPostureIntegrationDisabled.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumPostureIntegrationDisabled.yaml new file mode 100644 index 00000000000..1361958eeec --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumPostureIntegrationDisabled.yaml @@ -0,0 +1,44 @@ +id: a1b2c3d4-5678-9012-34ab-cdef12345030 +name: "Tailscale Premium: Posture integration disabled or removed" +description: Identifies when a device-posture integration is disabled or removed from the tailnet. Posture integrations enforce device compliance - removal weakens fleet posture and is a possible defense-evasion step. +description-detailed: | + Identifies when a device-posture integration is disabled or removed from the tailnet. Posture integrations enforce device compliance; removing one disables that enforcement and increases blast radius for compromised endpoints. Requires Tailscale Premium or Enterprise. +severity: High +status: Available +requiredDataConnectors: + - connectorId: TailscalePremiumCCF + dataTypes: + - Tailscale_Audit_CL +queryFrequency: 15m +queryPeriod: 15m +triggerOperator: gt +triggerThreshold: 0 +tactics: + - DefenseEvasion + - Persistence +relevantTechniques: + - T1562 + - T1556 +query: | + Tailscale_Audit_CL + | where Action in ("DELETE", "UPDATE") + | where tostring(Target.type) == "POSTURE_INTEGRATION" + or tostring(Target.type) startswith "POSTURE" + | extend ActorLogin = tostring(Actor.loginName) + | extend Provider = tostring(Target.name) + | project TimeGenerated, ActorLogin, Action, Provider, Target, Old, New, Origin +entityMappings: + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: ActorLogin +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 1d + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumSubnetRouterThroughputAnomaly.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumSubnetRouterThroughputAnomaly.yaml new file mode 100644 index 00000000000..70723c50128 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumSubnetRouterThroughputAnomaly.yaml @@ -0,0 +1,64 @@ +id: a5b6c7d8-5e6f-7a8b-9c0d-1e2f3a4b5c6d +name: "Tailscale Premium: Subnet router throughput anomaly" +description: | + Identifies when a subnet router (gateway node bridging the tailnet to an on-prem or cloud subnet) handles 3x or more its 7-day baseline traffic in the last hour. Spikes can indicate exfiltration or scanning. Requires Tailscale Premium or Enterprise. +severity: Low +status: Available +requiredDataConnectors: + - connectorId: TailscalePremiumCCF + dataTypes: + - Tailscale_Network_CL +queryFrequency: 1h +queryPeriod: 8d +triggerOperator: gt +triggerThreshold: 0 +tactics: + - Exfiltration + - CommandAndControl +relevantTechniques: + - T1572 + - T1041 +query: | + let baselineDays = 7d; + let recent = 1h; + let multiplier = 3.0; + let recentTraffic = + Tailscale_Network_CL + | where TimeGenerated > ago(recent) + | where HasSubnetTraffic + | mv-expand t = SubnetTraffic + | extend Bytes = tolong(t.txBytes) + tolong(t.rxBytes) + | summarize RecentBytes = sum(Bytes) by NodeId, SrcNodeName, SrcUser, SrcOs, SrcTags=tostring(SrcTags); + let baseline = + Tailscale_Network_CL + | where TimeGenerated between (ago(baselineDays + recent) .. ago(recent)) + | where HasSubnetTraffic + | mv-expand t = SubnetTraffic + | extend Bytes = tolong(t.txBytes) + tolong(t.rxBytes) + | summarize TotalBaselineBytes = sum(Bytes) by NodeId + | extend BaselineHourlyBytes = TotalBaselineBytes / 168.0; + recentTraffic + | join kind=inner baseline on NodeId + | where RecentBytes > BaselineHourlyBytes * multiplier + | extend Multiplier = round(RecentBytes / BaselineHourlyBytes, 1), RecentMB = round(RecentBytes / 1024.0 / 1024.0, 2), BaselineHourlyMB = round(BaselineHourlyBytes / 1024.0 / 1024.0, 2) + | project NodeId, SrcNodeName, SrcUser, SrcOs, SrcTags, RecentMB, BaselineHourlyMB, Multiplier + | order by Multiplier desc +entityMappings: + - entityType: Host + fieldMappings: + - identifier: HostName + columnName: SrcNodeName + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: SrcUser +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 1d + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumUnexpectedExitNodeEgress.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumUnexpectedExitNodeEgress.yaml new file mode 100644 index 00000000000..67f92176534 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscalePremiumUnexpectedExitNodeEgress.yaml @@ -0,0 +1,64 @@ +id: c1d2e3f4-1a2b-3c4d-5e6f-7a8b9c0d1e2f +name: "Tailscale Premium: Unexpected exit-node egress" +description: Identifies when a node sends traffic via an exit node not used in the prior 7-day baseline. First-seen exit destinations from a node may indicate routing-policy drift, data exfiltration, or compromise. +description-detailed: | + Identifies when a node sends traffic via an exit node not used in the prior 7-day baseline. First-seen exit destinations from a specific source may indicate unsanctioned egress, a compromised node pivoting, or a policy misconfiguration. 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: + - CommandAndControl + - Exfiltration +relevantTechniques: + - T1090 + - T1041 +query: | + let baseline = 7d; + let recent = 1h; + let recentEgress = + Tailscale_Network_CL + | where TimeGenerated > ago(recent) + | where HasExitTraffic + | mv-expand t = ExitTraffic + | extend Src = tostring(t.src), ExitDst = tostring(t.dst), Bytes = tolong(t.txBytes) + tolong(t.rxBytes) + | summarize FirstSeen = min(TimeGenerated), TotalBytes = sum(Bytes) by NodeId, SrcNodeName, SrcUser, SrcOs, Src, ExitDst; + let baselineEgress = + Tailscale_Network_CL + | where TimeGenerated between (ago(baseline + recent) .. ago(recent)) + | where HasExitTraffic + | mv-expand t = ExitTraffic + | extend Src = tostring(t.src), ExitDst = tostring(t.dst) + | distinct NodeId, Src, ExitDst; + recentEgress + | join kind=leftanti baselineEgress on NodeId, Src, ExitDst + | extend TotalMB = round(TotalBytes / 1024.0 / 1024.0, 2) +entityMappings: + - entityType: Host + fieldMappings: + - identifier: HostName + columnName: SrcNodeName + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: SrcUser + - entityType: IP + fieldMappings: + - identifier: Address + columnName: Src +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 5h + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleSplitDnsModified.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleSplitDnsModified.yaml new file mode 100644 index 00000000000..c942fa3b27c --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleSplitDnsModified.yaml @@ -0,0 +1,46 @@ +id: b4c5d6e7-1234-5678-90ab-cdef12345010 +name: "Tailscale: Split-DNS configuration modified" +description: | + Identifies when the tailnet split-DNS configuration is modified. Split-DNS overrides per-domain resolution within the tailnet - an attacker adding a new domain mapping or changing the resolver IP can hijack DNS for that domain. +severity: High +status: Available +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Audit_CL +queryFrequency: 15m +queryPeriod: 15m +triggerOperator: gt +triggerThreshold: 0 +tactics: + - DefenseEvasion + - CommandAndControl +relevantTechniques: + - T1556 + - T1568 +query: | + Tailscale_Audit_CL + | where Action == "UPDATE" + | where tostring(Target.type) == "TAILNET" + | where tostring(Target.property) == "DNS_SPLIT_DNS" + | extend ActorLogin = tostring(Actor.loginName) + | extend OldDomains = bag_keys(Old) + | extend NewDomains = bag_keys(New) + | extend AddedDomains = set_difference(NewDomains, OldDomains) + | extend RemovedDomains = set_difference(OldDomains, NewDomains) + | project TimeGenerated, ActorLogin, AddedDomains, RemovedDomains, Old, New, Origin +entityMappings: + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: ActorLogin +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 1d + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleTailnetLockValidationFailed.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleTailnetLockValidationFailed.yaml new file mode 100644 index 00000000000..9fd2801cea0 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleTailnetLockValidationFailed.yaml @@ -0,0 +1,46 @@ +id: e9f0a1b2-3456-7890-12cd-ef1234560040 +name: "Tailscale: Tailnet lock validation failed" +description: Identifies tailnet devices with a non-empty TailnetLockError, indicating the device failed tailnet-lock cryptographic validation. Suspicious - likely an unsigned node attempting to join. +description-detailed: | + Identifies tailnet devices with a non-empty TailnetLockError, indicating the device failed tailnet-lock cryptographic validation. Tailnet lock requires new node-keys be co-signed by trusted signers; errors here are the only direct signal of a node-key injection attempt. +severity: High +status: Available +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Devices_CL +queryFrequency: 1h +queryPeriod: 1h +triggerOperator: gt +triggerThreshold: 0 +tactics: + - DefenseEvasion + - InitialAccess +relevantTechniques: + - T1556 + - T1078 +query: | + Tailscale_Devices_CL + | where TimeGenerated > ago(1h) + | summarize arg_max(TimeGenerated, *) by DeviceId + | where isnotempty(TailnetLockError) + | project TimeGenerated, DeviceName, Hostname, User, Os, ClientVersion, TailnetLockError, TailnetLockKey, LastSeen, Authorized +entityMappings: + - entityType: Host + fieldMappings: + - identifier: HostName + columnName: Hostname + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: User +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 1d + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleUnauthorizedDeviceConnected.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleUnauthorizedDeviceConnected.yaml new file mode 100644 index 00000000000..9df4f541e56 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleUnauthorizedDeviceConnected.yaml @@ -0,0 +1,46 @@ +id: a1b2c3d4-5678-9012-3456-789012340042 +name: "Tailscale: Unauthorized device connected to control plane" +description: Identifies devices actively connected to the Tailscale control plane (ConnectedToControl=true) but not yet authorized by an admin (Authorized=false). Often benign onboarding but can indicate rogue joins. +description-detailed: | + Identifies devices actively connected to the Tailscale control plane (ConnectedToControl=true) but not yet authorized by an admin (Authorized=false). With device approval enabled, persistent entries warrant review; without approval, persistence may indicate a node-key injection attempt. +severity: High +status: Available +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Devices_CL +queryFrequency: 1h +queryPeriod: 1h +triggerOperator: gt +triggerThreshold: 0 +tactics: + - InitialAccess + - Persistence +relevantTechniques: + - T1078 + - T1098 +query: | + Tailscale_Devices_CL + | where TimeGenerated > ago(1h) + | summarize arg_max(TimeGenerated, *) by DeviceId + | where Authorized == false and ConnectedToControl == true + | project TimeGenerated, DeviceName, Hostname, User, Os, ClientVersion, Created, LastSeen, MachineKey, NodeKey +entityMappings: + - entityType: Host + fieldMappings: + - identifier: HostName + columnName: Hostname + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: User +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 1d + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleUserRoleElevated.yaml b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleUserRoleElevated.yaml new file mode 100644 index 00000000000..9d549dee867 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Analytic Rules/TailscaleUserRoleElevated.yaml @@ -0,0 +1,53 @@ +id: d3c4e5f6-3456-7890-12ab-cdef12345003 +name: "Tailscale: User role elevated to admin or owner" +description: | + Identifies when a user's tailnet role changes from a lower-privilege role to admin, network-admin, or owner between consecutive snapshots. Privilege escalation is a high-value attacker objective and warrants prompt review. +severity: High +status: Available +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Users_CL +queryFrequency: 1h +queryPeriod: 2d +triggerOperator: gt +triggerThreshold: 0 +tactics: + - PrivilegeEscalation + - Persistence +relevantTechniques: + - T1078 + - T1098 +query: | + let elevated = dynamic(["admin", "network-admin", "owner"]); + let recent = + Tailscale_Users_CL + | where TimeGenerated > ago(1h) + | summarize arg_max(TimeGenerated, *) by UserId + | where Role in (elevated) + | project UserId, LoginName, RoleNow = Role; + let prior = + Tailscale_Users_CL + | where TimeGenerated between (ago(2d) .. ago(1h)) + | summarize arg_max(TimeGenerated, *) by UserId + | project UserId, RolePrior = Role; + recent + | join kind=inner prior on UserId + | where RoleNow != RolePrior + | where RolePrior !in (elevated) + | project TimeGenerated = now(), UserId, LoginName, RolePrior, RoleNow +entityMappings: + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: LoginName +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: 1d + matchingMethod: AllEntities + groupByEntities: [] +kind: Scheduled +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Data Connectors/TailscaleAuditLogs_ccf/Tailscale_ConnectorDefinition.json b/Solutions/Tailscale (CCF)/Data Connectors/TailscaleAuditLogs_ccf/Tailscale_ConnectorDefinition.json new file mode 100644 index 00000000000..1f3c0e1bef4 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Data Connectors/TailscaleAuditLogs_ccf/Tailscale_ConnectorDefinition.json @@ -0,0 +1,174 @@ +{ + "name": "TailscaleCCF", + "apiVersion": "2022-09-01-preview", + "type": "Microsoft.SecurityInsights/dataConnectorDefinitions", + "kind": "Customizable", + "properties": { + "connectorUiConfig": { + "title": "Tailscale Standard (CCF)", + "publisher": "Community", + "logo": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIGlkPSJMYXllcl8xIiB4PSIwIiB5PSIwIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48c3R5bGU+LnN0MHtvcGFjaXR5Oi4yO2VuYWJsZS1iYWNrZ3JvdW5kOm5ld308L3N0eWxlPjxwYXRoIGQ9Ik02NS42IDEyNy43YzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45UzEwMC45IDAgNjUuNiAwIDEuOCAyOC42IDEuOCA2My45czI4LjYgNjMuOCA2My44IDYzLjgiIGNsYXNzPSJzdDAiLz48cGF0aCBkPSJNNjUuNiAzMTguMWMzNS4zIDAgNjMuOS0yOC42IDYzLjktNjMuOXMtMjguNi02My45LTYzLjktNjMuOVMxLjggMjE5IDEuOCAyNTQuMnMyOC42IDYzLjkgNjMuOCA2My45Ii8+PHBhdGggZD0iTTY1LjYgNTEyYzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45cy0yOC42LTYzLjktNjMuOS02My45LTYzLjggMjguNy02My44IDYzLjlTMzAuNCA1MTIgNjUuNiA1MTIiIGNsYXNzPSJzdDAiLz48cGF0aCBkPSJNMjU3LjIgMzE4LjFjMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlzLTI4LjYtNjMuOS02My45LTYzLjktNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjkgNjMuOSA2My45bTAgMTkzLjljMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlzLTI4LjYtNjMuOS02My45LTYzLjktNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjkgNjMuOSA2My45Ii8+PHBhdGggZD0iTTI1Ny4yIDEyNy43YzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45UzI5Mi41IDAgMjU3LjIgMHMtNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjggNjMuOSA2My44bTE4OS4yIDBjMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlTNDgxLjYgMCA0NDYuNCAwYy0zNS4zIDAtNjMuOSAyOC42LTYzLjkgNjMuOXMyOC42IDYzLjggNjMuOSA2My44IiBjbGFzcz0ic3QwIi8+PHBhdGggZD0iTTQ0Ni40IDMxOC4xYzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45cy0yOC42LTYzLjktNjMuOS02My45LTYzLjkgMjguNi02My45IDYzLjkgMjguNiA2My45IDYzLjkgNjMuOSIvPjxwYXRoIGQ9Ik00NDYuNCA1MTJjMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlzLTI4LjYtNjMuOS02My45LTYzLjktNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjkgNjMuOSA2My45IiBjbGFzcz0ic3QwIi8+PC9zdmc+", + "descriptionMarkdown": "Comprehensive Tailscale telemetry for **Personal (Free) and Standard** tier tailnets. Polls nine endpoints in one Connect:\n\n- `/logging/configuration` - configuration audit events (includes ACL, DNS, tag/group, settings changes)\n- `/devices` - device inventory (hostname, OS, IPs, tags, lastSeen, expiry)\n- `/users` - user inventory (role, status, deviceCount, connection state)\n- `/keys?all=true` - auth keys, API tokens, and OAuth client metadata\n- `/webhooks` - webhook configuration\n- `/dns/nameservers`, `/dns/preferences`, `/dns/searchpaths` - DNS state (merged into single `Tailscale_Dns_CL` table with `ConfigType` discriminator)\n- `/settings` - tailnet settings flags (device approval, key duration, etc.)\n\nSplit-DNS state (per-domain DNS overrides) is captured via the audit log rather than a separate snapshot table - every change is recorded with the full before/after document and actor attribution, which is richer than a periodic snapshot.\n\n**OAuth scopes required on the Tailscale client:** `logs:configuration:read`, `devices:core:read`, `users:read`, `auth_keys:read`, `webhooks:read`, `dns:read`, `feature_settings:read` (or the bundled `all:read`). For Premium and Enterprise tailnets that also need network flow logs and posture integrations, install **Tailscale Premium (CCF)** instead.", + "graphQueries": [ + { + "metricName": "Audit events", + "legend": "Tailscale_Audit_CL", + "baseQuery": "Tailscale_Audit_CL" + }, + { + "metricName": "Device snapshots", + "legend": "Tailscale_Devices_CL", + "baseQuery": "Tailscale_Devices_CL" + }, + { + "metricName": "User snapshots", + "legend": "Tailscale_Users_CL", + "baseQuery": "Tailscale_Users_CL" + }, + { + "metricName": "Auth keys", + "legend": "Tailscale_Keys_CL", + "baseQuery": "Tailscale_Keys_CL" + }, + { + "metricName": "Webhooks", + "legend": "Tailscale_Webhooks_CL", + "baseQuery": "Tailscale_Webhooks_CL" + }, + { + "metricName": "Tailnet settings", + "legend": "Tailscale_Settings_CL", + "baseQuery": "Tailscale_Settings_CL" + }, + { + "metricName": "DNS config snapshots", + "legend": "Tailscale_Dns_CL", + "baseQuery": "Tailscale_Dns_CL" + } + ], + "dataTypes": [ + { + "name": "Tailscale_Audit_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Audit_CL')]" + }, + { + "name": "Tailscale_Devices_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Devices_CL')]" + }, + { + "name": "Tailscale_Users_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Users_CL')]" + }, + { + "name": "Tailscale_Keys_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Keys_CL')]" + }, + { + "name": "Tailscale_Webhooks_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Webhooks_CL')]" + }, + { + "name": "Tailscale_Settings_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Settings_CL')]" + }, + { + "name": "Tailscale_Dns_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Dns_CL')]" + } + ], + "sampleQueries": [ + { + "description": "Recent audit events", + "query": "Tailscale_Audit_CL\n| sort by TimeGenerated desc\n| take 100" + }, + { + "description": "Current device inventory (latest snapshot per device)", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| project DeviceName, User, Os, ClientVersion, LastSeen, Expires, Tags" + }, + { + "description": "Users by role", + "query": "Tailscale_Users_CL\n| summarize arg_max(TimeGenerated, *) by UserId\n| summarize Users = count() by Role" + }, + { + "description": "Auth keys without an expiry", + "query": "Tailscale_Keys_CL\n| summarize arg_max(TimeGenerated, *) by KeyId\n| where isnull(Expires) or Expires == datetime(null)" + }, + { + "description": "Tailnet device-approval status", + "query": "Tailscale_Settings_CL\n| sort by TimeGenerated desc\n| take 1\n| project DevicesApprovalOn, DevicesAutoUpdatesOn, UsersApprovalOn" + }, + { + "description": "Current DNS state (all DNS config in one query)", + "query": "Tailscale_Dns_CL\n| summarize arg_max(TimeGenerated, *) by ConfigType\n| project ConfigType, Nameservers, MagicDNS, SearchPaths" + } + ], + "connectivityCriteria": [ + { + "type": "HasDataConnectors" + } + ], + "availability": { + "status": 1, + "isPreview": true + }, + "permissions": { + "resourceProvider": [ + { + "provider": "Microsoft.OperationalInsights/workspaces", + "permissionsDisplayText": "Read/Write on the workspace", + "providerDisplayName": "Workspace", + "scope": "Workspace", + "requiredPermissions": { + "write": true, + "read": true, + "delete": true + } + } + ] + }, + "instructionSteps": [ + { + "title": "Connect Tailscale", + "description": "Generate an OAuth client at https://login.tailscale.com/admin/settings/oauth with these **Read** scopes: Logs > Configuration, General > DNS, General > Users, Devices > Core, Keys > Auth Keys, Keys > Webhooks, Settings > Feature Settings (or tick `all:read` to grant all read scopes at once). Find your tailnet name on the Keys page.", + "instructions": [ + { + "type": "Textbox", + "parameters": { + "label": "Tailscale tailnet", + "placeholder": "tail-XXXX.ts.net", + "type": "text", + "name": "tailnetName" + } + }, + { + "type": "Textbox", + "parameters": { + "label": "OAuth Client ID", + "placeholder": "k...", + "type": "text", + "name": "clientId" + } + }, + { + "type": "Textbox", + "parameters": { + "label": "OAuth Client Secret", + "placeholder": "tskey-client-...", + "type": "password", + "name": "clientSecret" + } + }, + { + "type": "ConnectionToggleButton", + "parameters": { + "connectLabel": "Connect", + "disconnectLabel": "Disconnect" + } + } + ] + } + ], + "id": "TailscaleCCF" + } + } +} \ No newline at end of file diff --git a/Solutions/Tailscale (CCF)/Data Connectors/TailscaleAuditLogs_ccf/Tailscale_DCR.json b/Solutions/Tailscale (CCF)/Data Connectors/TailscaleAuditLogs_ccf/Tailscale_DCR.json new file mode 100644 index 00000000000..ad29d608675 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Data Connectors/TailscaleAuditLogs_ccf/Tailscale_DCR.json @@ -0,0 +1,447 @@ +{ + "name": "TailscaleDCR", + "apiVersion": "2021-09-01-preview", + "type": "Microsoft.Insights/dataCollectionRules", + "kind": "WorkspaceTransforms", + "properties": { + "dataCollectionEndpointId": "{{dataCollectionEndpointId}}", + "streamDeclarations": { + "Custom-Tailscale_Audit_CL": { + "columns": [ + { + "name": "eventTime", + "type": "datetime" + }, + { + "name": "eventGroupID", + "type": "string" + }, + { + "name": "type", + "type": "string" + }, + { + "name": "actionDetails", + "type": "string" + }, + { + "name": "actor", + "type": "dynamic" + }, + { + "name": "action", + "type": "string" + }, + { + "name": "target", + "type": "dynamic" + }, + { + "name": "origin", + "type": "dynamic" + }, + { + "name": "new", + "type": "dynamic" + }, + { + "name": "old", + "type": "dynamic" + } + ] + }, + "Custom-Tailscale_Devices_CL": { + "columns": [ + { + "name": "id", + "type": "string" + }, + { + "name": "name", + "type": "string" + }, + { + "name": "hostname", + "type": "string" + }, + { + "name": "user", + "type": "string" + }, + { + "name": "os", + "type": "string" + }, + { + "name": "clientVersion", + "type": "string" + }, + { + "name": "updateAvailable", + "type": "boolean" + }, + { + "name": "authorized", + "type": "boolean" + }, + { + "name": "isExternal", + "type": "boolean" + }, + { + "name": "created", + "type": "datetime" + }, + { + "name": "lastSeen", + "type": "datetime" + }, + { + "name": "expires", + "type": "datetime" + }, + { + "name": "keyExpiryDisabled", + "type": "boolean" + }, + { + "name": "blocksIncomingConnections", + "type": "boolean" + }, + { + "name": "addresses", + "type": "dynamic" + }, + { + "name": "tags", + "type": "dynamic" + }, + { + "name": "enabledRoutes", + "type": "dynamic" + }, + { + "name": "advertisedRoutes", + "type": "dynamic" + }, + { + "name": "clientConnectivity", + "type": "dynamic" + }, + { + "name": "machineKey", + "type": "string" + }, + { + "name": "nodeKey", + "type": "string" + }, + { + "name": "distro", + "type": "string" + }, + { + "name": "sshEnabled", + "type": "boolean" + }, + { + "name": "connectedToControl", + "type": "boolean" + }, + { + "name": "tailnetLockKey", + "type": "string" + }, + { + "name": "tailnetLockError", + "type": "string" + } + ] + }, + "Custom-Tailscale_Users_CL": { + "columns": [ + { + "name": "id", + "type": "string" + }, + { + "name": "displayName", + "type": "string" + }, + { + "name": "loginName", + "type": "string" + }, + { + "name": "tailnetId", + "type": "string" + }, + { + "name": "type", + "type": "string" + }, + { + "name": "role", + "type": "string" + }, + { + "name": "status", + "type": "string" + }, + { + "name": "deviceCount", + "type": "int" + }, + { + "name": "created", + "type": "datetime" + }, + { + "name": "lastSeen", + "type": "datetime" + }, + { + "name": "currentlyConnected", + "type": "boolean" + }, + { + "name": "profilePicUrl", + "type": "string" + } + ] + }, + "Custom-Tailscale_Keys_CL": { + "columns": [ + { + "name": "id", + "type": "string" + }, + { + "name": "description", + "type": "string" + }, + { + "name": "userId", + "type": "string" + }, + { + "name": "created", + "type": "datetime" + }, + { + "name": "expires", + "type": "datetime" + }, + { + "name": "revoked", + "type": "datetime" + }, + { + "name": "capabilities", + "type": "dynamic" + }, + { + "name": "keyType", + "type": "string" + }, + { + "name": "expirySeconds", + "type": "int" + } + ] + }, + "Custom-Tailscale_Webhooks_CL": { + "columns": [ + { + "name": "endpointId", + "type": "string" + }, + { + "name": "endpointUrl", + "type": "string" + }, + { + "name": "providerType", + "type": "string" + }, + { + "name": "creatorLoginName", + "type": "string" + }, + { + "name": "created", + "type": "datetime" + }, + { + "name": "lastModified", + "type": "datetime" + }, + { + "name": "subscriptions", + "type": "dynamic" + } + ] + }, + "Custom-Tailscale_DnsNameservers_CL": { + "columns": [ + { + "name": "dns", + "type": "dynamic" + } + ] + }, + "Custom-Tailscale_Settings_CL": { + "columns": [ + { + "name": "devicesApprovalOn", + "type": "boolean" + }, + { + "name": "devicesAutoUpdatesOn", + "type": "boolean" + }, + { + "name": "devicesKeyDurationDays", + "type": "int" + }, + { + "name": "usersApprovalOn", + "type": "boolean" + }, + { + "name": "usersRoleAllowedToJoinExternalTailnets", + "type": "string" + }, + { + "name": "networkFlowLoggingOn", + "type": "boolean" + }, + { + "name": "regionalRoutingOn", + "type": "boolean" + }, + { + "name": "postureIdentityCollectionOn", + "type": "boolean" + } + ] + }, + "Custom-Tailscale_DnsPreferences_CL": { + "columns": [ + { + "name": "magicDNS", + "type": "boolean" + } + ] + }, + "Custom-Tailscale_DnsSearchPaths_CL": { + "columns": [ + { + "name": "searchPaths", + "type": "dynamic" + } + ] + } + }, + "destinations": { + "logAnalytics": [ + { + "workspaceResourceId": "{{workspaceResourceId}}", + "name": "sentinelWorkspace" + } + ] + }, + "dataFlows": [ + { + "streams": [ + "Custom-Tailscale_Audit_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = eventTime | extend EventTime = eventTime | extend EventGroupID = eventGroupID | extend EventType = type | extend ActionDetails = actionDetails | extend Actor = actor | extend Action = action | extend Target = target | extend Origin = origin | extend New = new | extend Old = old | project TimeGenerated, EventTime, EventGroupID, EventType, ActionDetails, Actor, Action, Target, Origin, New, Old", + "outputStream": "Custom-Tailscale_Audit_CL" + }, + { + "streams": [ + "Custom-Tailscale_Devices_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend DeviceId = id | extend DeviceName = name | extend Hostname = hostname | extend User = user | extend Os = os | extend Distro = distro | extend ClientVersion = clientVersion | extend UpdateAvailable = updateAvailable | extend Authorized = authorized | extend IsExternal = isExternal | extend Created = created | extend LastSeen = lastSeen | extend Expires = expires | extend KeyExpiryDisabled = keyExpiryDisabled | extend BlocksIncomingConnections = blocksIncomingConnections | extend SshEnabled = sshEnabled | extend ConnectedToControl = connectedToControl | extend Addresses = addresses | extend Tags = tags | extend EnabledRoutes = enabledRoutes | extend AdvertisedRoutes = advertisedRoutes | extend ClientConnectivity = clientConnectivity | extend MachineKey = machineKey | extend NodeKey = nodeKey | extend TailnetLockKey = tailnetLockKey | extend TailnetLockError = tailnetLockError | project TimeGenerated, DeviceId, DeviceName, Hostname, User, Os, Distro, ClientVersion, UpdateAvailable, Authorized, IsExternal, Created, LastSeen, Expires, KeyExpiryDisabled, BlocksIncomingConnections, SshEnabled, ConnectedToControl, Addresses, Tags, EnabledRoutes, AdvertisedRoutes, ClientConnectivity, MachineKey, NodeKey, TailnetLockKey, TailnetLockError", + "outputStream": "Custom-Tailscale_Devices_CL" + }, + { + "streams": [ + "Custom-Tailscale_Users_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend UserId = id | extend DisplayName = displayName | extend LoginName = loginName | extend TailnetId = tailnetId | extend UserType = type | extend Role = role | extend Status = status | extend DeviceCount = deviceCount | extend Created = created | extend LastSeen = lastSeen | extend CurrentlyConnected = currentlyConnected | extend ProfilePicUrl = profilePicUrl | project TimeGenerated, UserId, DisplayName, LoginName, TailnetId, UserType, Role, Status, DeviceCount, Created, LastSeen, CurrentlyConnected, ProfilePicUrl", + "outputStream": "Custom-Tailscale_Users_CL" + }, + { + "streams": [ + "Custom-Tailscale_Keys_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend KeyId = id | extend Description = description | extend UserId = userId | extend Created = created | extend Expires = expires | extend Revoked = revoked | extend Capabilities = capabilities | extend KeyType = keyType | extend ExpirySeconds = expirySeconds | project TimeGenerated, KeyId, Description, UserId, Created, Expires, Revoked, Capabilities, KeyType, ExpirySeconds", + "outputStream": "Custom-Tailscale_Keys_CL" + }, + { + "streams": [ + "Custom-Tailscale_Webhooks_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend EndpointId = endpointId | extend EndpointUrl = endpointUrl | extend ProviderType = providerType | extend CreatorLoginName = creatorLoginName | extend Created = created | extend LastModified = lastModified | extend Subscriptions = subscriptions | project TimeGenerated, EndpointId, EndpointUrl, ProviderType, CreatorLoginName, Created, LastModified, Subscriptions", + "outputStream": "Custom-Tailscale_Webhooks_CL" + }, + { + "streams": [ + "Custom-Tailscale_DnsNameservers_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend ConfigType = 'nameservers' | extend Nameservers = dns | project TimeGenerated, ConfigType, Nameservers", + "outputStream": "Custom-Tailscale_Dns_CL" + }, + { + "streams": [ + "Custom-Tailscale_Settings_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend DevicesApprovalOn = devicesApprovalOn | extend DevicesAutoUpdatesOn = devicesAutoUpdatesOn | extend DevicesKeyDurationDays = devicesKeyDurationDays | extend UsersApprovalOn = usersApprovalOn | extend UsersRoleAllowedToJoinExternalTailnets = usersRoleAllowedToJoinExternalTailnets | extend NetworkFlowLoggingOn = networkFlowLoggingOn | extend RegionalRoutingOn = regionalRoutingOn | extend PostureIdentityCollectionOn = postureIdentityCollectionOn | project TimeGenerated, DevicesApprovalOn, DevicesAutoUpdatesOn, DevicesKeyDurationDays, UsersApprovalOn, UsersRoleAllowedToJoinExternalTailnets, NetworkFlowLoggingOn, RegionalRoutingOn, PostureIdentityCollectionOn", + "outputStream": "Custom-Tailscale_Settings_CL" + }, + { + "streams": [ + "Custom-Tailscale_DnsPreferences_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend ConfigType = 'preferences' | extend MagicDNS = magicDNS | project TimeGenerated, ConfigType, MagicDNS", + "outputStream": "Custom-Tailscale_Dns_CL" + }, + { + "streams": [ + "Custom-Tailscale_DnsSearchPaths_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend ConfigType = 'searchpaths' | extend SearchPaths = searchPaths | project TimeGenerated, ConfigType, SearchPaths", + "outputStream": "Custom-Tailscale_Dns_CL" + } + ] + } +} \ No newline at end of file diff --git a/Solutions/Tailscale (CCF)/Data Connectors/TailscaleAuditLogs_ccf/Tailscale_PollerConfig.json b/Solutions/Tailscale (CCF)/Data Connectors/TailscaleAuditLogs_ccf/Tailscale_PollerConfig.json new file mode 100644 index 00000000000..5fe64cd3ac1 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Data Connectors/TailscaleAuditLogs_ccf/Tailscale_PollerConfig.json @@ -0,0 +1,383 @@ +[ + { + "name": "TailscaleConfigAuditPoller", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.SecurityInsights/dataConnectors", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscaleCCF", + "dataType": "Tailscale_Audit_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Audit_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/logging/configuration')]", + "httpMethod": "GET", + "queryWindowInMin": 5, + "queryTimeFormat": "yyyy-MM-ddTHH:mm:ssZ", + "startTimeAttributeName": "start", + "endTimeAttributeName": "end", + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$.logs" + ] + }, + "isActive": true + } + }, + { + "name": "TailscaleDevicesPoller", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.SecurityInsights/dataConnectors", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscaleCCF", + "dataType": "Tailscale_Devices_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Devices_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/devices?fields=all')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$.devices" + ] + }, + "isActive": true + } + }, + { + "name": "TailscaleUsersPoller", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.SecurityInsights/dataConnectors", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscaleCCF", + "dataType": "Tailscale_Users_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Users_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/users')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$.users" + ] + }, + "isActive": true + } + }, + { + "name": "TailscaleKeysPoller", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.SecurityInsights/dataConnectors", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscaleCCF", + "dataType": "Tailscale_Keys_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Keys_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/keys?all=true')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$.keys" + ] + }, + "isActive": true + } + }, + { + "name": "TailscaleWebhooksPoller", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.SecurityInsights/dataConnectors", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscaleCCF", + "dataType": "Tailscale_Webhooks_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Webhooks_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/webhooks')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$.webhooks" + ] + }, + "isActive": true + } + }, + { + "name": "TailscaleDnsNameserversPoller", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.SecurityInsights/dataConnectors", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscaleCCF", + "dataType": "Tailscale_Dns_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_DnsNameservers_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/dns/nameservers')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$" + ] + }, + "isActive": true + } + }, + { + "name": "TailscaleSettingsPoller", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.SecurityInsights/dataConnectors", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscaleCCF", + "dataType": "Tailscale_Settings_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Settings_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/settings')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$" + ] + }, + "isActive": true + } + }, + { + "name": "TailscaleDnsPreferencesPoller", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.SecurityInsights/dataConnectors", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscaleCCF", + "dataType": "Tailscale_Dns_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_DnsPreferences_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/dns/preferences')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$" + ] + }, + "isActive": true + } + }, + { + "name": "TailscaleDnsSearchPathsPoller", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.SecurityInsights/dataConnectors", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscaleCCF", + "dataType": "Tailscale_Dns_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_DnsSearchPaths_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/dns/searchpaths')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$" + ] + }, + "isActive": true + } + } +] \ No newline at end of file diff --git a/Solutions/Tailscale (CCF)/Data Connectors/TailscaleAuditLogs_ccf/Tailscale_tables.json b/Solutions/Tailscale (CCF)/Data Connectors/TailscaleAuditLogs_ccf/Tailscale_tables.json new file mode 100644 index 00000000000..91ab32a73b0 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Data Connectors/TailscaleAuditLogs_ccf/Tailscale_tables.json @@ -0,0 +1,425 @@ +[ + { + "name": "Tailscale_Audit_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "properties": { + "schema": { + "name": "Tailscale_Audit_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "EventTime", + "type": "datetime" + }, + { + "name": "EventGroupID", + "type": "string" + }, + { + "name": "EventType", + "type": "string" + }, + { + "name": "ActionDetails", + "type": "string" + }, + { + "name": "Actor", + "type": "dynamic" + }, + { + "name": "Action", + "type": "string" + }, + { + "name": "Target", + "type": "dynamic" + }, + { + "name": "Origin", + "type": "dynamic" + }, + { + "name": "New", + "type": "dynamic" + }, + { + "name": "Old", + "type": "dynamic" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Devices_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "properties": { + "schema": { + "name": "Tailscale_Devices_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "DeviceId", + "type": "string" + }, + { + "name": "DeviceName", + "type": "string" + }, + { + "name": "Hostname", + "type": "string" + }, + { + "name": "User", + "type": "string" + }, + { + "name": "Os", + "type": "string" + }, + { + "name": "ClientVersion", + "type": "string" + }, + { + "name": "UpdateAvailable", + "type": "boolean" + }, + { + "name": "Authorized", + "type": "boolean" + }, + { + "name": "IsExternal", + "type": "boolean" + }, + { + "name": "Created", + "type": "datetime" + }, + { + "name": "LastSeen", + "type": "datetime" + }, + { + "name": "Expires", + "type": "datetime" + }, + { + "name": "KeyExpiryDisabled", + "type": "boolean" + }, + { + "name": "BlocksIncomingConnections", + "type": "boolean" + }, + { + "name": "Addresses", + "type": "dynamic" + }, + { + "name": "Tags", + "type": "dynamic" + }, + { + "name": "EnabledRoutes", + "type": "dynamic" + }, + { + "name": "AdvertisedRoutes", + "type": "dynamic" + }, + { + "name": "ClientConnectivity", + "type": "dynamic" + }, + { + "name": "MachineKey", + "type": "string" + }, + { + "name": "NodeKey", + "type": "string" + }, + { + "name": "Distro", + "type": "string" + }, + { + "name": "SshEnabled", + "type": "boolean" + }, + { + "name": "ConnectedToControl", + "type": "boolean" + }, + { + "name": "TailnetLockKey", + "type": "string" + }, + { + "name": "TailnetLockError", + "type": "string" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Users_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "properties": { + "schema": { + "name": "Tailscale_Users_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "UserId", + "type": "string" + }, + { + "name": "DisplayName", + "type": "string" + }, + { + "name": "LoginName", + "type": "string" + }, + { + "name": "TailnetId", + "type": "string" + }, + { + "name": "UserType", + "type": "string" + }, + { + "name": "Role", + "type": "string" + }, + { + "name": "Status", + "type": "string" + }, + { + "name": "DeviceCount", + "type": "int" + }, + { + "name": "Created", + "type": "datetime" + }, + { + "name": "LastSeen", + "type": "datetime" + }, + { + "name": "CurrentlyConnected", + "type": "boolean" + }, + { + "name": "ProfilePicUrl", + "type": "string" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Keys_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "properties": { + "schema": { + "name": "Tailscale_Keys_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "KeyId", + "type": "string" + }, + { + "name": "Description", + "type": "string" + }, + { + "name": "UserId", + "type": "string" + }, + { + "name": "Created", + "type": "datetime" + }, + { + "name": "Expires", + "type": "datetime" + }, + { + "name": "Revoked", + "type": "datetime" + }, + { + "name": "Capabilities", + "type": "dynamic" + }, + { + "name": "KeyType", + "type": "string" + }, + { + "name": "ExpirySeconds", + "type": "int" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Webhooks_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "properties": { + "schema": { + "name": "Tailscale_Webhooks_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "EndpointId", + "type": "string" + }, + { + "name": "EndpointUrl", + "type": "string" + }, + { + "name": "ProviderType", + "type": "string" + }, + { + "name": "CreatorLoginName", + "type": "string" + }, + { + "name": "Created", + "type": "datetime" + }, + { + "name": "LastModified", + "type": "datetime" + }, + { + "name": "Subscriptions", + "type": "dynamic" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Settings_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "properties": { + "schema": { + "name": "Tailscale_Settings_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "DevicesApprovalOn", + "type": "boolean" + }, + { + "name": "DevicesAutoUpdatesOn", + "type": "boolean" + }, + { + "name": "DevicesKeyDurationDays", + "type": "int" + }, + { + "name": "UsersApprovalOn", + "type": "boolean" + }, + { + "name": "UsersRoleAllowedToJoinExternalTailnets", + "type": "string" + }, + { + "name": "NetworkFlowLoggingOn", + "type": "boolean" + }, + { + "name": "RegionalRoutingOn", + "type": "boolean" + }, + { + "name": "PostureIdentityCollectionOn", + "type": "boolean" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Dns_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "properties": { + "schema": { + "name": "Tailscale_Dns_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "ConfigType", + "type": "string" + }, + { + "name": "Nameservers", + "type": "dynamic" + }, + { + "name": "MagicDNS", + "type": "boolean" + }, + { + "name": "SearchPaths", + "type": "dynamic" + } + ] + }, + "retentionInDays": 90 + } + } +] \ No newline at end of file diff --git a/Solutions/Tailscale (CCF)/Data Connectors/TailscalePremium_ccf/TailscalePremium_ConnectorDefinition.json b/Solutions/Tailscale (CCF)/Data Connectors/TailscalePremium_ccf/TailscalePremium_ConnectorDefinition.json new file mode 100644 index 00000000000..41a03fa977f --- /dev/null +++ b/Solutions/Tailscale (CCF)/Data Connectors/TailscalePremium_ccf/TailscalePremium_ConnectorDefinition.json @@ -0,0 +1,192 @@ +{ + "name": "TailscalePremiumCCF", + "apiVersion": "2022-09-01-preview", + "type": "Microsoft.SecurityInsights/dataConnectorDefinitions", + "kind": "Customizable", + "properties": { + "connectorUiConfig": { + "title": "Tailscale Premium (CCF)", + "publisher": "Community", + "logo": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIGlkPSJMYXllcl8xIiB4PSIwIiB5PSIwIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48c3R5bGU+LnN0MHtvcGFjaXR5Oi4yO2VuYWJsZS1iYWNrZ3JvdW5kOm5ld308L3N0eWxlPjxwYXRoIGQ9Ik02NS42IDEyNy43YzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45UzEwMC45IDAgNjUuNiAwIDEuOCAyOC42IDEuOCA2My45czI4LjYgNjMuOCA2My44IDYzLjgiIGNsYXNzPSJzdDAiLz48cGF0aCBkPSJNNjUuNiAzMTguMWMzNS4zIDAgNjMuOS0yOC42IDYzLjktNjMuOXMtMjguNi02My45LTYzLjktNjMuOVMxLjggMjE5IDEuOCAyNTQuMnMyOC42IDYzLjkgNjMuOCA2My45Ii8+PHBhdGggZD0iTTY1LjYgNTEyYzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45cy0yOC42LTYzLjktNjMuOS02My45LTYzLjggMjguNy02My44IDYzLjlTMzAuNCA1MTIgNjUuNiA1MTIiIGNsYXNzPSJzdDAiLz48cGF0aCBkPSJNMjU3LjIgMzE4LjFjMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlzLTI4LjYtNjMuOS02My45LTYzLjktNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjkgNjMuOSA2My45bTAgMTkzLjljMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlzLTI4LjYtNjMuOS02My45LTYzLjktNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjkgNjMuOSA2My45Ii8+PHBhdGggZD0iTTI1Ny4yIDEyNy43YzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45UzI5Mi41IDAgMjU3LjIgMHMtNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjggNjMuOSA2My44bTE4OS4yIDBjMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlTNDgxLjYgMCA0NDYuNCAwYy0zNS4zIDAtNjMuOSAyOC42LTYzLjkgNjMuOXMyOC42IDYzLjggNjMuOSA2My44IiBjbGFzcz0ic3QwIi8+PHBhdGggZD0iTTQ0Ni40IDMxOC4xYzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45cy0yOC42LTYzLjktNjMuOS02My45LTYzLjkgMjguNi02My45IDYzLjkgMjguNiA2My45IDYzLjkgNjMuOSIvPjxwYXRoIGQ9Ik00NDYuNCA1MTJjMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlzLTI4LjYtNjMuOS02My45LTYzLjktNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjkgNjMuOSA2My45IiBjbGFzcz0ic3QwIi8+PC9zdmc+", + "descriptionMarkdown": "Comprehensive Tailscale telemetry for **Premium and Enterprise** tier tailnets. Polls every endpoint the Standard connector polls, **plus** Premium-only network flow logs and posture-integration inventory. Eleven endpoints in one Connect:\n\n- `/logging/configuration` - configuration audit events\n- `/logging/network` - **Premium** network flow logs (per-node traffic with src/dst/protocol/bytes)\n- `/devices` - device inventory\n- `/users` - user inventory\n- `/keys?all=true` - auth keys + API tokens + OAuth clients\n- `/webhooks` - webhook configuration\n- `/dns/nameservers`, `/dns/preferences`, `/dns/searchpaths` - DNS state (merged into single `Tailscale_Dns_CL` table with `ConfigType` discriminator)\n- `/settings` - tailnet settings flags\n- `/posture/integrations` - **Premium** MDM/EDR integration inventory (Jamf, Kandji, Intune, Kolide, Microsoft Defender for Endpoint, CrowdStrike Falcon, SentinelOne, etc.)\n\n**OAuth scopes required:** `logs:configuration:read`, `logs:network:read`, `devices:core:read`, `users:read`, `auth_keys:read`, `webhooks:read`, `dns:read`, `feature_settings:read` (or the bundled `all:read`).\n\n**If your tailnet is Personal (Free) or Standard tier, install `Tailscale Standard (CCF)` instead - this Premium connector's network and posture pollers will return 403 on lower tiers.**", + "graphQueries": [ + { + "metricName": "Audit", + "legend": "Tailscale_Audit_CL", + "baseQuery": "Tailscale_Audit_CL" + }, + { + "metricName": "Devices", + "legend": "Tailscale_Devices_CL", + "baseQuery": "Tailscale_Devices_CL" + }, + { + "metricName": "Users", + "legend": "Tailscale_Users_CL", + "baseQuery": "Tailscale_Users_CL" + }, + { + "metricName": "Keys", + "legend": "Tailscale_Keys_CL", + "baseQuery": "Tailscale_Keys_CL" + }, + { + "metricName": "Webhooks", + "legend": "Tailscale_Webhooks_CL", + "baseQuery": "Tailscale_Webhooks_CL" + }, + { + "metricName": "Settings", + "legend": "Tailscale_Settings_CL", + "baseQuery": "Tailscale_Settings_CL" + }, + { + "metricName": "Network", + "legend": "Tailscale_Network_CL", + "baseQuery": "Tailscale_Network_CL" + }, + { + "metricName": "PostureIntegrations", + "legend": "Tailscale_PostureIntegrations_CL", + "baseQuery": "Tailscale_PostureIntegrations_CL" + }, + { + "metricName": "DNS config snapshots", + "legend": "Tailscale_Dns_CL", + "baseQuery": "Tailscale_Dns_CL" + } + ], + "dataTypes": [ + { + "name": "Tailscale_Audit_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Audit_CL')]" + }, + { + "name": "Tailscale_Devices_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Devices_CL')]" + }, + { + "name": "Tailscale_Users_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Users_CL')]" + }, + { + "name": "Tailscale_Keys_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Keys_CL')]" + }, + { + "name": "Tailscale_Webhooks_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Webhooks_CL')]" + }, + { + "name": "Tailscale_Settings_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Settings_CL')]" + }, + { + "name": "Tailscale_Network_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Network_CL')]" + }, + { + "name": "Tailscale_PostureIntegrations_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_PostureIntegrations_CL')]" + }, + { + "name": "Tailscale_Dns_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Dns_CL')]" + } + ], + "sampleQueries": [ + { + "description": "Recent network flows", + "query": "Tailscale_Network_CL\n| sort by TimeGenerated desc\n| take 100" + }, + { + "description": "Top exit-node egress destinations", + "query": "Tailscale_Network_CL\n| where array_length(ExitTraffic) > 0\n| mv-expand t = ExitTraffic\n| extend Bytes = tolong(t.txBytes) + tolong(t.rxBytes), ExitDst = tostring(t.dst)\n| summarize TotalBytes = sum(Bytes) by ExitDst\n| top 25 by TotalBytes" + }, + { + "description": "Currently configured posture integrations", + "query": "Tailscale_PostureIntegrations_CL\n| summarize arg_max(TimeGenerated, *) by IntegrationId\n| project IntegrationId, Provider, ClientId, Status" + }, + { + "description": "Device inventory by OS", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| summarize Devices = count() by Os" + }, + { + "description": "Recent audit events", + "query": "Tailscale_Audit_CL\n| sort by TimeGenerated desc\n| take 100" + }, + { + "description": "Current DNS state (all DNS config in one query)", + "query": "Tailscale_Dns_CL\n| summarize arg_max(TimeGenerated, *) by ConfigType\n| project ConfigType, Nameservers, MagicDNS, SearchPaths" + } + ], + "connectivityCriteria": [ + { + "type": "HasDataConnectors" + } + ], + "availability": { + "status": 1, + "isPreview": true + }, + "permissions": { + "resourceProvider": [ + { + "provider": "Microsoft.OperationalInsights/workspaces", + "permissionsDisplayText": "Read/Write on the workspace", + "providerDisplayName": "Workspace", + "scope": "Workspace", + "requiredPermissions": { + "write": true, + "read": true, + "delete": true + } + } + ] + }, + "instructionSteps": [ + { + "title": "Connect Tailscale (Premium)", + "description": "Generate an OAuth client at https://login.tailscale.com/admin/settings/oauth with these **Read** scopes: Logs > Configuration, Logs > Network (Premium), General > DNS, General > Users, Devices > Core, Keys > Auth Keys, Keys > Webhooks, Settings > Feature Settings (or tick `all:read`). Find your tailnet name on the Keys page.", + "instructions": [ + { + "type": "Textbox", + "parameters": { + "label": "Tailscale tailnet", + "placeholder": "tail-XXXX.ts.net", + "type": "text", + "name": "tailnetName" + } + }, + { + "type": "Textbox", + "parameters": { + "label": "OAuth Client ID", + "placeholder": "k...", + "type": "text", + "name": "clientId" + } + }, + { + "type": "Textbox", + "parameters": { + "label": "OAuth Client Secret", + "placeholder": "tskey-client-...", + "type": "password", + "name": "clientSecret" + } + }, + { + "type": "ConnectionToggleButton", + "parameters": { + "connectLabel": "Connect", + "disconnectLabel": "Disconnect" + } + } + ] + } + ], + "id": "TailscalePremiumCCF" + } + } +} \ No newline at end of file diff --git a/Solutions/Tailscale (CCF)/Data Connectors/TailscalePremium_ccf/TailscalePremium_DCR.json b/Solutions/Tailscale (CCF)/Data Connectors/TailscalePremium_ccf/TailscalePremium_DCR.json new file mode 100644 index 00000000000..64ca601323a --- /dev/null +++ b/Solutions/Tailscale (CCF)/Data Connectors/TailscalePremium_ccf/TailscalePremium_DCR.json @@ -0,0 +1,515 @@ +{ + "name": "TailscalePremiumDCR", + "apiVersion": "2021-09-01-preview", + "type": "Microsoft.Insights/dataCollectionRules", + "kind": "WorkspaceTransforms", + "properties": { + "dataCollectionEndpointId": "{{dataCollectionEndpointId}}", + "streamDeclarations": { + "Custom-Tailscale_Audit_CL": { + "columns": [ + { + "name": "eventTime", + "type": "datetime" + }, + { + "name": "eventGroupID", + "type": "string" + }, + { + "name": "type", + "type": "string" + }, + { + "name": "actionDetails", + "type": "string" + }, + { + "name": "actor", + "type": "dynamic" + }, + { + "name": "action", + "type": "string" + }, + { + "name": "target", + "type": "dynamic" + }, + { + "name": "origin", + "type": "dynamic" + }, + { + "name": "new", + "type": "dynamic" + }, + { + "name": "old", + "type": "dynamic" + } + ] + }, + "Custom-Tailscale_Devices_CL": { + "columns": [ + { + "name": "id", + "type": "string" + }, + { + "name": "name", + "type": "string" + }, + { + "name": "hostname", + "type": "string" + }, + { + "name": "user", + "type": "string" + }, + { + "name": "os", + "type": "string" + }, + { + "name": "clientVersion", + "type": "string" + }, + { + "name": "updateAvailable", + "type": "boolean" + }, + { + "name": "authorized", + "type": "boolean" + }, + { + "name": "isExternal", + "type": "boolean" + }, + { + "name": "created", + "type": "datetime" + }, + { + "name": "lastSeen", + "type": "datetime" + }, + { + "name": "expires", + "type": "datetime" + }, + { + "name": "keyExpiryDisabled", + "type": "boolean" + }, + { + "name": "blocksIncomingConnections", + "type": "boolean" + }, + { + "name": "addresses", + "type": "dynamic" + }, + { + "name": "tags", + "type": "dynamic" + }, + { + "name": "enabledRoutes", + "type": "dynamic" + }, + { + "name": "advertisedRoutes", + "type": "dynamic" + }, + { + "name": "clientConnectivity", + "type": "dynamic" + }, + { + "name": "machineKey", + "type": "string" + }, + { + "name": "nodeKey", + "type": "string" + }, + { + "name": "distro", + "type": "string" + }, + { + "name": "sshEnabled", + "type": "boolean" + }, + { + "name": "connectedToControl", + "type": "boolean" + }, + { + "name": "tailnetLockKey", + "type": "string" + }, + { + "name": "tailnetLockError", + "type": "string" + } + ] + }, + "Custom-Tailscale_Users_CL": { + "columns": [ + { + "name": "id", + "type": "string" + }, + { + "name": "displayName", + "type": "string" + }, + { + "name": "loginName", + "type": "string" + }, + { + "name": "tailnetId", + "type": "string" + }, + { + "name": "type", + "type": "string" + }, + { + "name": "role", + "type": "string" + }, + { + "name": "status", + "type": "string" + }, + { + "name": "deviceCount", + "type": "int" + }, + { + "name": "created", + "type": "datetime" + }, + { + "name": "lastSeen", + "type": "datetime" + }, + { + "name": "currentlyConnected", + "type": "boolean" + }, + { + "name": "profilePicUrl", + "type": "string" + } + ] + }, + "Custom-Tailscale_Keys_CL": { + "columns": [ + { + "name": "id", + "type": "string" + }, + { + "name": "description", + "type": "string" + }, + { + "name": "userId", + "type": "string" + }, + { + "name": "created", + "type": "datetime" + }, + { + "name": "expires", + "type": "datetime" + }, + { + "name": "revoked", + "type": "datetime" + }, + { + "name": "capabilities", + "type": "dynamic" + }, + { + "name": "keyType", + "type": "string" + }, + { + "name": "expirySeconds", + "type": "int" + } + ] + }, + "Custom-Tailscale_Webhooks_CL": { + "columns": [ + { + "name": "endpointId", + "type": "string" + }, + { + "name": "endpointUrl", + "type": "string" + }, + { + "name": "providerType", + "type": "string" + }, + { + "name": "creatorLoginName", + "type": "string" + }, + { + "name": "created", + "type": "datetime" + }, + { + "name": "lastModified", + "type": "datetime" + }, + { + "name": "subscriptions", + "type": "dynamic" + } + ] + }, + "Custom-Tailscale_DnsConfig_CL": { + "columns": [ + { + "name": "dns", + "type": "dynamic" + }, + { + "name": "magicDNS", + "type": "boolean" + }, + { + "name": "searchPaths", + "type": "dynamic" + } + ] + }, + "Custom-Tailscale_Settings_CL": { + "columns": [ + { + "name": "devicesApprovalOn", + "type": "boolean" + }, + { + "name": "devicesAutoUpdatesOn", + "type": "boolean" + }, + { + "name": "devicesKeyDurationDays", + "type": "int" + }, + { + "name": "usersApprovalOn", + "type": "boolean" + }, + { + "name": "usersRoleAllowedToJoinExternalTailnets", + "type": "string" + }, + { + "name": "networkFlowLoggingOn", + "type": "boolean" + }, + { + "name": "regionalRoutingOn", + "type": "boolean" + }, + { + "name": "postureIdentityCollectionOn", + "type": "boolean" + } + ] + }, + "Custom-Tailscale_Network_CL": { + "columns": [ + { + "name": "nodeId", + "type": "string" + }, + { + "name": "logged", + "type": "datetime" + }, + { + "name": "start", + "type": "datetime" + }, + { + "name": "end", + "type": "datetime" + }, + { + "name": "srcNode", + "type": "dynamic" + }, + { + "name": "dstNodes", + "type": "dynamic" + }, + { + "name": "virtualTraffic", + "type": "dynamic" + }, + { + "name": "subnetTraffic", + "type": "dynamic" + }, + { + "name": "exitTraffic", + "type": "dynamic" + }, + { + "name": "physicalTraffic", + "type": "dynamic" + } + ] + }, + "Custom-Tailscale_PostureIntegrations_CL": { + "columns": [ + { + "name": "id", + "type": "string" + }, + { + "name": "provider", + "type": "string" + }, + { + "name": "cloudId", + "type": "string" + }, + { + "name": "clientId", + "type": "string" + }, + { + "name": "tenantId", + "type": "string" + }, + { + "name": "configOverwrites", + "type": "dynamic" + }, + { + "name": "status", + "type": "dynamic" + } + ] + } + }, + "destinations": { + "logAnalytics": [ + { + "workspaceResourceId": "{{workspaceResourceId}}", + "name": "sentinelWorkspace" + } + ] + }, + "dataFlows": [ + { + "streams": [ + "Custom-Tailscale_Audit_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = eventTime | extend EventTime = eventTime | extend EventGroupID = eventGroupID | extend EventType = type | extend ActionDetails = actionDetails | extend Actor = actor | extend Action = action | extend Target = target | extend Origin = origin | extend New = new | extend Old = old | project TimeGenerated, EventTime, EventGroupID, EventType, ActionDetails, Actor, Action, Target, Origin, New, Old", + "outputStream": "Custom-Tailscale_Audit_CL" + }, + { + "streams": [ + "Custom-Tailscale_Devices_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend DeviceId = id | extend DeviceName = name | extend Hostname = hostname | extend User = user | extend Os = os | extend Distro = distro | extend ClientVersion = clientVersion | extend UpdateAvailable = updateAvailable | extend Authorized = authorized | extend IsExternal = isExternal | extend Created = created | extend LastSeen = lastSeen | extend Expires = expires | extend KeyExpiryDisabled = keyExpiryDisabled | extend BlocksIncomingConnections = blocksIncomingConnections | extend SshEnabled = sshEnabled | extend ConnectedToControl = connectedToControl | extend Addresses = addresses | extend Tags = tags | extend EnabledRoutes = enabledRoutes | extend AdvertisedRoutes = advertisedRoutes | extend ClientConnectivity = clientConnectivity | extend MachineKey = machineKey | extend NodeKey = nodeKey | extend TailnetLockKey = tailnetLockKey | extend TailnetLockError = tailnetLockError | project TimeGenerated, DeviceId, DeviceName, Hostname, User, Os, Distro, ClientVersion, UpdateAvailable, Authorized, IsExternal, Created, LastSeen, Expires, KeyExpiryDisabled, BlocksIncomingConnections, SshEnabled, ConnectedToControl, Addresses, Tags, EnabledRoutes, AdvertisedRoutes, ClientConnectivity, MachineKey, NodeKey, TailnetLockKey, TailnetLockError", + "outputStream": "Custom-Tailscale_Devices_CL" + }, + { + "streams": [ + "Custom-Tailscale_Users_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend UserId = id | extend DisplayName = displayName | extend LoginName = loginName | extend TailnetId = tailnetId | extend UserType = type | extend Role = role | extend Status = status | extend DeviceCount = deviceCount | extend Created = created | extend LastSeen = lastSeen | extend CurrentlyConnected = currentlyConnected | extend ProfilePicUrl = profilePicUrl | project TimeGenerated, UserId, DisplayName, LoginName, TailnetId, UserType, Role, Status, DeviceCount, Created, LastSeen, CurrentlyConnected, ProfilePicUrl", + "outputStream": "Custom-Tailscale_Users_CL" + }, + { + "streams": [ + "Custom-Tailscale_Keys_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend KeyId = id | extend Description = description | extend UserId = userId | extend Created = created | extend Expires = expires | extend Revoked = revoked | extend Capabilities = capabilities | extend KeyType = keyType | extend ExpirySeconds = expirySeconds | project TimeGenerated, KeyId, Description, UserId, Created, Expires, Revoked, Capabilities, KeyType, ExpirySeconds", + "outputStream": "Custom-Tailscale_Keys_CL" + }, + { + "streams": [ + "Custom-Tailscale_Webhooks_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend EndpointId = endpointId | extend EndpointUrl = endpointUrl | extend ProviderType = providerType | extend CreatorLoginName = creatorLoginName | extend Created = created | extend LastModified = lastModified | extend Subscriptions = subscriptions | project TimeGenerated, EndpointId, EndpointUrl, ProviderType, CreatorLoginName, Created, LastModified, Subscriptions", + "outputStream": "Custom-Tailscale_Webhooks_CL" + }, + { + "streams": [ + "Custom-Tailscale_DnsConfig_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend ConfigType = case(isnotnull(dns), 'nameservers', isnotnull(searchPaths), 'searchpaths', 'preferences') | extend Nameservers = dns | extend MagicDNS = magicDNS | extend SearchPaths = searchPaths | project TimeGenerated, ConfigType, Nameservers, MagicDNS, SearchPaths", + "outputStream": "Custom-Tailscale_Dns_CL" + }, + { + "streams": [ + "Custom-Tailscale_Settings_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend DevicesApprovalOn = devicesApprovalOn | extend DevicesAutoUpdatesOn = devicesAutoUpdatesOn | extend DevicesKeyDurationDays = devicesKeyDurationDays | extend UsersApprovalOn = usersApprovalOn | extend UsersRoleAllowedToJoinExternalTailnets = usersRoleAllowedToJoinExternalTailnets | extend NetworkFlowLoggingOn = networkFlowLoggingOn | extend RegionalRoutingOn = regionalRoutingOn | extend PostureIdentityCollectionOn = postureIdentityCollectionOn | project TimeGenerated, DevicesApprovalOn, DevicesAutoUpdatesOn, DevicesKeyDurationDays, UsersApprovalOn, UsersRoleAllowedToJoinExternalTailnets, NetworkFlowLoggingOn, RegionalRoutingOn, PostureIdentityCollectionOn", + "outputStream": "Custom-Tailscale_Settings_CL" + }, + { + "streams": [ + "Custom-Tailscale_Network_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = logged | extend NodeId = nodeId | extend FlowStart = start | extend FlowEnd = end | extend SrcNode = srcNode | extend SrcUser = tostring(srcNode.user) | extend SrcNodeName = tostring(srcNode.name) | extend SrcOs = tostring(srcNode.os) | extend SrcTags = srcNode.tags | extend SrcAddresses = srcNode.addresses | extend DstNodes = dstNodes | extend DstCount = toint(array_length(dstNodes)) | extend DstNodeId = tostring(dstNodes[0].nodeId) | extend DstNodeName = tostring(dstNodes[0].name) | extend DstUser = tostring(dstNodes[0].user) | extend DstOs = tostring(dstNodes[0].os) | extend DstTags = dstNodes[0].tags | extend DstAddresses = dstNodes[0].addresses | extend VirtualTraffic = virtualTraffic | extend SubnetTraffic = subnetTraffic | extend ExitTraffic = exitTraffic | extend PhysicalTraffic = physicalTraffic | extend HasVirtualTraffic = array_length(virtualTraffic) > 0 | extend HasSubnetTraffic = array_length(subnetTraffic) > 0 | extend HasExitTraffic = array_length(exitTraffic) > 0 | extend HasPhysicalTraffic = array_length(physicalTraffic) > 0 | extend IsRelayed = tostring(physicalTraffic) contains '127.3.3.40' | project TimeGenerated, NodeId, FlowStart, FlowEnd, SrcNode, SrcUser, SrcNodeName, SrcOs, SrcTags, SrcAddresses, DstNodes, DstCount, DstNodeId, DstNodeName, DstUser, DstOs, DstTags, DstAddresses, VirtualTraffic, SubnetTraffic, ExitTraffic, PhysicalTraffic, HasVirtualTraffic, HasSubnetTraffic, HasExitTraffic, HasPhysicalTraffic, IsRelayed", + "outputStream": "Custom-Tailscale_Network_CL" + }, + { + "streams": [ + "Custom-Tailscale_PostureIntegrations_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend IntegrationId = id | extend Provider = provider | extend CloudId = cloudId | extend ClientId = clientId | extend TenantId_Provider = tenantId | extend ConfigOverwrites = configOverwrites | extend Status = status | project TimeGenerated, IntegrationId, Provider, CloudId, ClientId, TenantId_Provider, ConfigOverwrites, Status", + "outputStream": "Custom-Tailscale_PostureIntegrations_CL" + } + ] + } +} \ No newline at end of file diff --git a/Solutions/Tailscale (CCF)/Data Connectors/TailscalePremium_ccf/TailscalePremium_PollerConfig.json b/Solutions/Tailscale (CCF)/Data Connectors/TailscalePremium_ccf/TailscalePremium_PollerConfig.json new file mode 100644 index 00000000000..e00d83fc569 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Data Connectors/TailscalePremium_ccf/TailscalePremium_PollerConfig.json @@ -0,0 +1,470 @@ +[ + { + "name": "TailscalePremiumConfigAuditPoller", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.SecurityInsights/dataConnectors", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscalePremiumCCF", + "dataType": "Tailscale_Audit_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Audit_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/logging/configuration')]", + "httpMethod": "GET", + "queryWindowInMin": 5, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + }, + "queryTimeFormat": "yyyy-MM-ddTHH:mm:ssZ", + "startTimeAttributeName": "start", + "endTimeAttributeName": "end" + }, + "response": { + "eventsJsonPaths": [ + "$.logs" + ] + }, + "isActive": true + } + }, + { + "name": "TailscalePremiumNetworkPoller", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.SecurityInsights/dataConnectors", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscalePremiumCCF", + "dataType": "Tailscale_Network_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Network_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/logging/network')]", + "httpMethod": "GET", + "queryWindowInMin": 5, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + }, + "queryTimeFormat": "yyyy-MM-ddTHH:mm:ssZ", + "startTimeAttributeName": "start", + "endTimeAttributeName": "end" + }, + "response": { + "eventsJsonPaths": [ + "$.logs" + ] + }, + "isActive": true + } + }, + { + "name": "TailscalePremiumDevicesPoller", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.SecurityInsights/dataConnectors", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscalePremiumCCF", + "dataType": "Tailscale_Devices_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Devices_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/devices?fields=all')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$.devices" + ] + }, + "isActive": true + } + }, + { + "name": "TailscalePremiumUsersPoller", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.SecurityInsights/dataConnectors", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscalePremiumCCF", + "dataType": "Tailscale_Users_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Users_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/users')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$.users" + ] + }, + "isActive": true + } + }, + { + "name": "TailscalePremiumKeysPoller", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.SecurityInsights/dataConnectors", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscalePremiumCCF", + "dataType": "Tailscale_Keys_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Keys_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/keys?all=true')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$.keys" + ] + }, + "isActive": true + } + }, + { + "name": "TailscalePremiumWebhooksPoller", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.SecurityInsights/dataConnectors", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscalePremiumCCF", + "dataType": "Tailscale_Webhooks_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Webhooks_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/webhooks')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$.webhooks" + ] + }, + "isActive": true + } + }, + { + "name": "TailscalePremiumDnsNameserversPoller", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.SecurityInsights/dataConnectors", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscalePremiumCCF", + "dataType": "Tailscale_Dns_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_DnsConfig_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/dns/nameservers')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$" + ] + }, + "isActive": true + } + }, + { + "name": "TailscalePremiumDnsPreferencesPoller", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.SecurityInsights/dataConnectors", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscalePremiumCCF", + "dataType": "Tailscale_Dns_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_DnsConfig_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/dns/preferences')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$" + ] + }, + "isActive": true + } + }, + { + "name": "TailscalePremiumDnsSearchPathsPoller", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.SecurityInsights/dataConnectors", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscalePremiumCCF", + "dataType": "Tailscale_Dns_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_DnsConfig_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/dns/searchpaths')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$" + ] + }, + "isActive": true + } + }, + { + "name": "TailscalePremiumSettingsPoller", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.SecurityInsights/dataConnectors", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscalePremiumCCF", + "dataType": "Tailscale_Settings_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Settings_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/settings')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$" + ] + }, + "isActive": true + } + }, + { + "name": "TailscalePremiumPostureIntegrationsPoller", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.SecurityInsights/dataConnectors", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscalePremiumCCF", + "dataType": "Tailscale_PostureIntegrations_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_PostureIntegrations_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/posture/integrations')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$.integrations" + ] + }, + "isActive": true + } + } +] \ No newline at end of file diff --git a/Solutions/Tailscale (CCF)/Data Connectors/TailscalePremium_ccf/TailscalePremium_tables.json b/Solutions/Tailscale (CCF)/Data Connectors/TailscalePremium_ccf/TailscalePremium_tables.json new file mode 100644 index 00000000000..5ab2dcabcf2 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Data Connectors/TailscalePremium_ccf/TailscalePremium_tables.json @@ -0,0 +1,591 @@ +[ + { + "name": "Tailscale_Audit_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "properties": { + "schema": { + "name": "Tailscale_Audit_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "EventTime", + "type": "datetime" + }, + { + "name": "EventGroupID", + "type": "string" + }, + { + "name": "EventType", + "type": "string" + }, + { + "name": "ActionDetails", + "type": "string" + }, + { + "name": "Actor", + "type": "dynamic" + }, + { + "name": "Action", + "type": "string" + }, + { + "name": "Target", + "type": "dynamic" + }, + { + "name": "Origin", + "type": "dynamic" + }, + { + "name": "New", + "type": "dynamic" + }, + { + "name": "Old", + "type": "dynamic" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Devices_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "properties": { + "schema": { + "name": "Tailscale_Devices_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "DeviceId", + "type": "string" + }, + { + "name": "DeviceName", + "type": "string" + }, + { + "name": "Hostname", + "type": "string" + }, + { + "name": "User", + "type": "string" + }, + { + "name": "Os", + "type": "string" + }, + { + "name": "ClientVersion", + "type": "string" + }, + { + "name": "UpdateAvailable", + "type": "boolean" + }, + { + "name": "Authorized", + "type": "boolean" + }, + { + "name": "IsExternal", + "type": "boolean" + }, + { + "name": "Created", + "type": "datetime" + }, + { + "name": "LastSeen", + "type": "datetime" + }, + { + "name": "Expires", + "type": "datetime" + }, + { + "name": "KeyExpiryDisabled", + "type": "boolean" + }, + { + "name": "BlocksIncomingConnections", + "type": "boolean" + }, + { + "name": "Addresses", + "type": "dynamic" + }, + { + "name": "Tags", + "type": "dynamic" + }, + { + "name": "EnabledRoutes", + "type": "dynamic" + }, + { + "name": "AdvertisedRoutes", + "type": "dynamic" + }, + { + "name": "ClientConnectivity", + "type": "dynamic" + }, + { + "name": "MachineKey", + "type": "string" + }, + { + "name": "NodeKey", + "type": "string" + }, + { + "name": "Distro", + "type": "string" + }, + { + "name": "SshEnabled", + "type": "boolean" + }, + { + "name": "ConnectedToControl", + "type": "boolean" + }, + { + "name": "TailnetLockKey", + "type": "string" + }, + { + "name": "TailnetLockError", + "type": "string" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Users_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "properties": { + "schema": { + "name": "Tailscale_Users_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "UserId", + "type": "string" + }, + { + "name": "DisplayName", + "type": "string" + }, + { + "name": "LoginName", + "type": "string" + }, + { + "name": "TailnetId", + "type": "string" + }, + { + "name": "UserType", + "type": "string" + }, + { + "name": "Role", + "type": "string" + }, + { + "name": "Status", + "type": "string" + }, + { + "name": "DeviceCount", + "type": "int" + }, + { + "name": "Created", + "type": "datetime" + }, + { + "name": "LastSeen", + "type": "datetime" + }, + { + "name": "CurrentlyConnected", + "type": "boolean" + }, + { + "name": "ProfilePicUrl", + "type": "string" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Keys_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "properties": { + "schema": { + "name": "Tailscale_Keys_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "KeyId", + "type": "string" + }, + { + "name": "Description", + "type": "string" + }, + { + "name": "UserId", + "type": "string" + }, + { + "name": "Created", + "type": "datetime" + }, + { + "name": "Expires", + "type": "datetime" + }, + { + "name": "Revoked", + "type": "datetime" + }, + { + "name": "Capabilities", + "type": "dynamic" + }, + { + "name": "KeyType", + "type": "string" + }, + { + "name": "ExpirySeconds", + "type": "int" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Webhooks_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "properties": { + "schema": { + "name": "Tailscale_Webhooks_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "EndpointId", + "type": "string" + }, + { + "name": "EndpointUrl", + "type": "string" + }, + { + "name": "ProviderType", + "type": "string" + }, + { + "name": "CreatorLoginName", + "type": "string" + }, + { + "name": "Created", + "type": "datetime" + }, + { + "name": "LastModified", + "type": "datetime" + }, + { + "name": "Subscriptions", + "type": "dynamic" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Settings_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "properties": { + "schema": { + "name": "Tailscale_Settings_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "DevicesApprovalOn", + "type": "boolean" + }, + { + "name": "DevicesAutoUpdatesOn", + "type": "boolean" + }, + { + "name": "DevicesKeyDurationDays", + "type": "int" + }, + { + "name": "UsersApprovalOn", + "type": "boolean" + }, + { + "name": "UsersRoleAllowedToJoinExternalTailnets", + "type": "string" + }, + { + "name": "NetworkFlowLoggingOn", + "type": "boolean" + }, + { + "name": "RegionalRoutingOn", + "type": "boolean" + }, + { + "name": "PostureIdentityCollectionOn", + "type": "boolean" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Network_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "properties": { + "schema": { + "name": "Tailscale_Network_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "NodeId", + "type": "string" + }, + { + "name": "FlowStart", + "type": "datetime" + }, + { + "name": "FlowEnd", + "type": "datetime" + }, + { + "name": "SrcNode", + "type": "dynamic" + }, + { + "name": "SrcUser", + "type": "string" + }, + { + "name": "SrcNodeName", + "type": "string" + }, + { + "name": "SrcOs", + "type": "string" + }, + { + "name": "SrcTags", + "type": "dynamic" + }, + { + "name": "SrcAddresses", + "type": "dynamic" + }, + { + "name": "DstNodes", + "type": "dynamic" + }, + { + "name": "DstCount", + "type": "int" + }, + { + "name": "DstNodeId", + "type": "string" + }, + { + "name": "DstNodeName", + "type": "string" + }, + { + "name": "DstUser", + "type": "string" + }, + { + "name": "DstOs", + "type": "string" + }, + { + "name": "DstTags", + "type": "dynamic" + }, + { + "name": "DstAddresses", + "type": "dynamic" + }, + { + "name": "VirtualTraffic", + "type": "dynamic" + }, + { + "name": "SubnetTraffic", + "type": "dynamic" + }, + { + "name": "ExitTraffic", + "type": "dynamic" + }, + { + "name": "PhysicalTraffic", + "type": "dynamic" + }, + { + "name": "HasVirtualTraffic", + "type": "boolean" + }, + { + "name": "HasSubnetTraffic", + "type": "boolean" + }, + { + "name": "HasExitTraffic", + "type": "boolean" + }, + { + "name": "HasPhysicalTraffic", + "type": "boolean" + }, + { + "name": "IsRelayed", + "type": "boolean" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_PostureIntegrations_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "properties": { + "schema": { + "name": "Tailscale_PostureIntegrations_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "IntegrationId", + "type": "string" + }, + { + "name": "Provider", + "type": "string" + }, + { + "name": "CloudId", + "type": "string" + }, + { + "name": "ClientId", + "type": "string" + }, + { + "name": "TenantId_Provider", + "type": "string" + }, + { + "name": "ConfigOverwrites", + "type": "dynamic" + }, + { + "name": "Status", + "type": "dynamic" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Dns_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "properties": { + "schema": { + "name": "Tailscale_Dns_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "ConfigType", + "type": "string" + }, + { + "name": "Nameservers", + "type": "dynamic" + }, + { + "name": "MagicDNS", + "type": "boolean" + }, + { + "name": "SearchPaths", + "type": "dynamic" + } + ] + }, + "retentionInDays": 90 + } + } +] \ No newline at end of file diff --git a/Solutions/Tailscale (CCF)/Data/Solution_Tailscale.json b/Solutions/Tailscale (CCF)/Data/Solution_Tailscale.json new file mode 100644 index 00000000000..52ad7fdb74f --- /dev/null +++ b/Solutions/Tailscale (CCF)/Data/Solution_Tailscale.json @@ -0,0 +1,73 @@ +{ + "Name": "Tailscale (CCF)", + "Author": "noodlemctwoodle - ccfconnectors.county118@passmail.com", + "Logo": "", + "Description": "The [Tailscale](https://tailscale.com/) solution for Microsoft Sentinel ingests Tailscale identity, device, configuration, audit and (Premium) network-flow telemetry via OAuth2-secured APIs. Built on the Codeless Connector Framework (CCF) - no Function App or container required.\n\n**Data connectors in this solution (install the one matching your Tailscale plan):**\n- **Tailscale Standard (CCF)** - Configuration audit, devices, users, keys, webhooks, DNS, settings. Use on **Personal (Free), Starter and Premium** tailnets.\n- **Tailscale Premium (CCF)** - Everything in Standard plus network flow logs and posture integrations. Use on **Premium and Enterprise** tailnets for full coverage.\n\n**Pre-requisites:**\n1. Sign in to [Tailscale OAuth settings](https://login.tailscale.com/admin/settings/oauth)\n2. Create an OAuth client with the scopes for your tier (see the README in this solution).\n3. Copy the client ID and client secret (secret shown once).\n4. Note your tailnet name (e.g. `tailb094d7.ts.net`) from the [Keys page](https://login.tailscale.com/admin/settings/keys).", + "BasePath": "C:\\GitHub\\azure-Sentinel\\Solutions\\Tailscale (CCF)", + "Version": "3.0.0", + "TemplateSpec": false, + "Is1PConnector": false, + "Data Connectors": [ + "Data Connectors/TailscaleAuditLogs_ccf/Tailscale_ConnectorDefinition.json", + "Data Connectors/TailscalePremium_ccf/TailscalePremium_ConnectorDefinition.json" + ], + "Analytic Rules": [ + "Analytic Rules/TailscaleNewAPIaccesstokenorOAuthclientcreated.yaml", + "Analytic Rules/TailscaleOAuthClientCreatedWithWriteScopes.yaml", + "Analytic Rules/TailscalePolicyfileACLmodified.yaml", + "Analytic Rules/TailscaleAuthkeycreated.yaml", + "Analytic Rules/TailscaleExitnodeadvertisedorapproved.yaml", + "Analytic Rules/TailscaleMasscredentialrevocationinshortwindow.yaml", + "Analytic Rules/TailscaleDeviceKeyExpiringSoon.yaml", + "Analytic Rules/TailscaleDeviceAdvertisingSubnetRoutes.yaml", + "Analytic Rules/TailscaleUserRoleElevated.yaml", + "Analytic Rules/TailscaleSplitDnsModified.yaml", + "Analytic Rules/TailscaleDnsNameserversModified.yaml", + "Analytic Rules/TailscaleMagicDnsDisabled.yaml", + "Analytic Rules/TailscaleTailnetLockValidationFailed.yaml", + "Analytic Rules/TailscaleDeviceSshNewlyEnabled.yaml", + "Analytic Rules/TailscaleUnauthorizedDeviceConnected.yaml", + "Analytic Rules/TailscaleExternalDeviceAdded.yaml", + "Analytic Rules/TailscalePremiumUnexpectedExitNodeEgress.yaml", + "Analytic Rules/TailscalePremiumLargeOutboundTransfer.yaml", + "Analytic Rules/TailscalePremiumBeaconingDetected.yaml", + "Analytic Rules/TailscalePremiumMassFanOut.yaml", + "Analytic Rules/TailscalePremiumSubnetRouterThroughputAnomaly.yaml", + "Analytic Rules/TailscalePremiumPostureIntegrationDisabled.yaml", + "Analytic Rules/TailscalePremiumNewPostureIntegration.yaml", + "Analytic Rules/TailscalePremiumDerpRelaySurge.yaml" + ], + "Hunting Queries": [ + "Hunting Queries/TailscaleFirstSeenActor.yaml", + "Hunting Queries/TailscaleACLPolicyChurn.yaml", + "Hunting Queries/TailscaleOffHoursConfigChanges.yaml", + "Hunting Queries/TailscaleAuthKeySprawl.yaml", + "Hunting Queries/TailscaleDormantDevices.yaml", + "Hunting Queries/TailscaleAuthKeysNoExpiry.yaml", + "Hunting Queries/TailscaleOrphanedUsers.yaml", + "Hunting Queries/TailscaleSplitDnsPerDomainChanges.yaml", + "Hunting Queries/TailscaleDevicesWithSshEnabled.yaml", + "Hunting Queries/TailscaleExternalDeviceInventory.yaml", + "Hunting Queries/TailscaleOutdatedClients.yaml", + "Hunting Queries/TailscaleSubnetRouteExposure.yaml", + "Hunting Queries/TailscalePremiumNewNodePairs.yaml", + "Hunting Queries/TailscalePremiumTopTalkers.yaml", + "Hunting Queries/TailscalePremiumExitNodeUsage.yaml", + "Hunting Queries/TailscalePremiumBeaconingCandidates.yaml", + "Hunting Queries/TailscalePremiumPostureInventory.yaml", + "Hunting Queries/TailscalePremiumDerpRelayPersistence.yaml", + "Hunting Queries/TailscalePremiumTaggedServiceFanIn.yaml", + "Hunting Queries/TailscalePremiumCrossTagFlowMatrix.yaml", + "Hunting Queries/TailscalePremiumOffHoursFlows.yaml", + "Hunting Queries/TailscalePremiumUserMultiDevice.yaml" + ], + "Workbooks": [ + "Workbooks/TailscaleStandardOperations.json", + "Workbooks/TailscalePremiumOperations.json" + ], + "Parsers": [ + "Parsers/vimNetworkSessionTailscale.yaml", + "Parsers/ASimNetworkSessionTailscale.yaml" + ], + "Metadata": "SolutionMetadata.json" +} diff --git a/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleACLPolicyChurn.yaml b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleACLPolicyChurn.yaml new file mode 100644 index 00000000000..1084aabf5fb --- /dev/null +++ b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleACLPolicyChurn.yaml @@ -0,0 +1,33 @@ +id: b82e5d4d-2c8f-5e3b-ad2e-1f4c6e8d9eab +name: "Tailscale: ACL policy churn" +description: Identifies short windows where the tailnet ACL policy file was rewritten multiple times. Iterative policy edits during an incident can indicate a defender adjusting rules or an attacker probing. +description-detailed: | + Identifies short windows where the tailnet ACL policy file was rewritten multiple times. Iterative policy edits during an incident can indicate rule-hunting by an attacker or a misconfiguration spiral; legitimate ACL refactors usually land as a single change. +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Audit_CL +tactics: + - DefenseEvasion + - PrivilegeEscalation +relevantTechniques: + - T1098 + - T1556 +query: | + let bucket = 30m; + let churnThreshold = 3; + Tailscale_Audit_CL + | where Action == "UPDATE" + | where tostring(Target.type) == "TAILNET" + | where tostring(Target.property) == "ACL" + | extend ActorLogin = tostring(Actor.loginName) + | summarize EditCount = count(), Editors = make_set(ActorLogin), FirstEdit = min(TimeGenerated), LastEdit = max(TimeGenerated) + by bin(TimeGenerated, bucket) + | where EditCount >= churnThreshold + | order by EditCount desc +entityMappings: + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: Editors +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleAuthKeySprawl.yaml b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleAuthKeySprawl.yaml new file mode 100644 index 00000000000..9ecdae14d99 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleAuthKeySprawl.yaml @@ -0,0 +1,32 @@ +id: d64e7f6f-4eab-7a5d-cf4a-3b6e8aafbcad +name: "Tailscale: Auth key sprawl" +description: Identifies actors creating multiple auth keys in a short window. A single admin creating many keys for unattended enrollment is normal during a rollout; same pattern can also indicate token-spraying. +description-detailed: | + Identifies actors creating multiple auth keys in a short window. A single admin creating many keys for unattended enrollment is normal during a rollout; the same pattern can also indicate token-spraying for persistence. Cross-reference with the Tailscale: Auth key created alert to filter context. +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Audit_CL +tactics: + - Persistence + - CredentialAccess +relevantTechniques: + - T1098 + - T1136 +query: | + let bucket = 1h; + let sprawlThreshold = 5; + Tailscale_Audit_CL + | where Action == "CREATE" + | where tostring(Target.type) == "AUTH_KEY" + | extend ActorLogin = tostring(Actor.loginName) + | summarize KeysCreated = count(), KeyIDs = make_set(tostring(Target.id)), Reusable = make_set(tostring(New.reusable)), Ephemeral = make_set(tostring(New.ephemeral)) + by bin(TimeGenerated, bucket), ActorLogin + | where KeysCreated >= sprawlThreshold + | order by KeysCreated desc +entityMappings: + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: ActorLogin +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleAuthKeysNoExpiry.yaml b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleAuthKeysNoExpiry.yaml new file mode 100644 index 00000000000..0be911d6674 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleAuthKeysNoExpiry.yaml @@ -0,0 +1,27 @@ +id: f5e6a7b8-5678-9012-34ab-cdef12345005 +name: "Tailscale: Auth keys with no expiry" +description: | + Identifies tailnet auth keys that have no expiry timestamp set. Non-expiring keys grant unattended enrollment in perpetuity and should be rotated or replaced with ephemeral, time-bounded keys. +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Keys_CL +tactics: + - Persistence + - CredentialAccess +relevantTechniques: + - T1098 + - T1136 +query: | + Tailscale_Keys_CL + | summarize arg_max(TimeGenerated, *) by KeyId + | where isnull(Revoked) or Revoked == datetime(null) + | where isnull(Expires) or Expires == datetime(null) + | project KeyId, Description, UserId, Created, Capabilities + | order by Created asc +entityMappings: + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: UserId +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleDevicesWithSshEnabled.yaml b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleDevicesWithSshEnabled.yaml new file mode 100644 index 00000000000..affba148758 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleDevicesWithSshEnabled.yaml @@ -0,0 +1,26 @@ +id: c3d4e5f6-7890-1234-5678-901234560044 +name: "Tailscale: Devices with Tailscale SSH enabled" +description: Identifies devices that currently have Tailscale SSH enabled. Tailscale SSH delivers SSH access over the tailnet using Tailscale identity and is governed by the SSH ACL block in the policy file. +description-detailed: | + Identifies devices that currently have Tailscale SSH enabled. Tailscale SSH delivers SSH access over the tailnet using Tailscale identity (no SSH keys needed) and is governed by the SSH ACL block in the policy file. Cross-reference this list with the SSH ACL to confirm only the intended devices are reachable as SSH targets. +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Devices_CL +tactics: + - LateralMovement + - Persistence +relevantTechniques: + - T1021 +query: | + Tailscale_Devices_CL + | summarize arg_max(TimeGenerated, *) by DeviceId + | where SshEnabled == true + | project DeviceName, Hostname, User, Os, Distro, ClientVersion, LastSeen, Tags + | order by DeviceName asc +entityMappings: + - entityType: Host + fieldMappings: + - identifier: HostName + columnName: Hostname +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleDormantDevices.yaml b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleDormantDevices.yaml new file mode 100644 index 00000000000..80aa9a978e4 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleDormantDevices.yaml @@ -0,0 +1,26 @@ +id: e4d5f6a7-4567-8901-23ab-cdef12345004 +name: "Tailscale: Devices not seen in 30+ days" +description: Identifies tailnet devices that have not connected to the control plane for at least 30 days. Dormant devices accumulate risk - they may still have valid keys, advertised routes, or tags. +description-detailed: | + Identifies tailnet devices that have not connected to the control plane for at least 30 days. Dormant devices accumulate risk - they may still have valid keys, advertised routes, or tags but no operational oversight. Candidate for removal or key rotation. +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Devices_CL +tactics: + - Discovery +relevantTechniques: + - T1078 +query: | + Tailscale_Devices_CL + | summarize arg_max(TimeGenerated, *) by DeviceId + | where LastSeen < ago(30d) + | extend DaysSinceSeen = datetime_diff('day', now(), LastSeen) + | project DeviceName, Hostname, User, Os, ClientVersion, LastSeen, DaysSinceSeen, Tags, AdvertisedRoutes + | order by DaysSinceSeen desc +entityMappings: + - entityType: Host + fieldMappings: + - identifier: HostName + columnName: Hostname +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleExternalDeviceInventory.yaml b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleExternalDeviceInventory.yaml new file mode 100644 index 00000000000..9ad35e7c3b8 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleExternalDeviceInventory.yaml @@ -0,0 +1,29 @@ +id: d4e5f6a7-8901-2345-6789-012345670045 +name: "Tailscale: External (shared-in) device inventory" +description: Identifies external (shared-in) devices currently active in the tailnet. These devices belong to another tailnet and have been admitted via a Tailscale sharing arrangement. +description-detailed: | + Identifies external (shared-in) devices currently active in the tailnet. These devices belong to another tailnet and have been admitted via a Tailscale sharing arrangement. Confirm each entry maps to a documented collaboration and that the corresponding ACL restricts the device to the intended resources. +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Devices_CL +tactics: + - InitialAccess +relevantTechniques: + - T1078 +query: | + Tailscale_Devices_CL + | summarize arg_max(TimeGenerated, *) by DeviceId + | where IsExternal == true + | project DeviceName, Hostname, User, Os, ClientVersion, Created, LastSeen, Tags + | order by Created desc +entityMappings: + - entityType: Host + fieldMappings: + - identifier: HostName + columnName: Hostname + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: User +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleFirstSeenActor.yaml b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleFirstSeenActor.yaml new file mode 100644 index 00000000000..de917c1d588 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleFirstSeenActor.yaml @@ -0,0 +1,34 @@ +id: a91f4d3c-1b7e-4f2a-9c1d-0e3b5f7c8d9a +name: "Tailscale: First-seen actor making configuration changes" +description: | + Identifies actors making their first configuration change in the lookback window. New legitimate admins look identical to compromised credentials - review whether each surfaced actor is expected to have admin rights. +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Audit_CL +tactics: + - InitialAccess + - Persistence +relevantTechniques: + - T1078 +query: | + let lookback = 14d; + let baselineWindow = 14d; + Tailscale_Audit_CL + | where TimeGenerated > ago(lookback) + | extend ActorLogin = tostring(Actor.loginName) + | where isnotempty(ActorLogin) + | summarize FirstSeen = min(TimeGenerated), Actions = make_set(Action), Targets = make_set(tostring(Target.type)), TotalEvents = count() by ActorLogin + | join kind=leftanti ( + Tailscale_Audit_CL + | where TimeGenerated between (ago(lookback + baselineWindow) .. ago(lookback)) + | extend ActorLogin = tostring(Actor.loginName) + | distinct ActorLogin + ) on ActorLogin + | order by FirstSeen asc +entityMappings: + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: ActorLogin +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleOffHoursConfigChanges.yaml b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleOffHoursConfigChanges.yaml new file mode 100644 index 00000000000..a5180186d29 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleOffHoursConfigChanges.yaml @@ -0,0 +1,32 @@ +id: c73f6e5e-3d9a-6f4c-be3f-2a5d7f9eafbc +name: "Tailscale: Off-hours configuration changes" +description: Identifies configuration audit events that occurred outside typical business hours (Monday-Friday 08:00-18:00 UTC). Useful for spotting impromptu maintenance, account compromise, or insider activity. +description-detailed: | + Identifies configuration audit events that occurred outside typical business hours (defined as Monday-Friday 08:00-18:00 UTC). Adjust the time window to match your operating region. Useful for spotting impromptu maintenance, account compromise, or insider activity. +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Audit_CL +tactics: + - InitialAccess + - Persistence +relevantTechniques: + - T1078 +query: | + let businessStartUtc = 8; + let businessEndUtc = 18; + Tailscale_Audit_CL + | extend HourOfDay = datetime_part("hour", TimeGenerated), DayOfWeek = dayofweek(TimeGenerated) + | where DayOfWeek in (0d, 6d) // Sunday and Saturday + or HourOfDay < businessStartUtc + or HourOfDay >= businessEndUtc + | extend ActorLogin = tostring(Actor.loginName) + | extend TargetType = tostring(Target.type) + | project TimeGenerated, ActorLogin, Action, TargetType, Target, Origin + | order by TimeGenerated desc +entityMappings: + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: ActorLogin +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleOrphanedUsers.yaml b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleOrphanedUsers.yaml new file mode 100644 index 00000000000..ba5b0e293d5 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleOrphanedUsers.yaml @@ -0,0 +1,26 @@ +id: a6f7b8c9-6789-0123-45ab-cdef12345006 +name: "Tailscale: Users with zero devices" +description: Identifies tailnet users who have no devices currently registered. Orphaned identities are candidates for off-boarding - they retain whatever role/permissions they were granted. +description-detailed: | + Identifies tailnet users who have no devices currently registered. Orphaned identities are candidates for off-boarding - they retain whatever role/permissions they were granted and can still create auth keys or invite others. Review against your HR / identity-provider join-leaver records. +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Users_CL +tactics: + - InitialAccess +relevantTechniques: + - T1078 +query: | + Tailscale_Users_CL + | summarize arg_max(TimeGenerated, *) by UserId + | where DeviceCount == 0 + | where Status != "suspended" + | project LoginName, DisplayName, Role, Status, DeviceCount, Created, LastSeen + | order by LastSeen asc +entityMappings: + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: LoginName +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleOutdatedClients.yaml b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleOutdatedClients.yaml new file mode 100644 index 00000000000..a5e7bfa7bda --- /dev/null +++ b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleOutdatedClients.yaml @@ -0,0 +1,25 @@ +id: e5f6a7b8-9012-3456-7890-123456780046 +name: "Tailscale: Devices with outdated client version" +description: Identifies tailnet devices that report UpdateAvailable=true on the latest snapshot. Tailscale releases security updates regularly; outdated clients lack the most recent improvements. +description-detailed: | + Identifies tailnet devices that report UpdateAvailable=true on the latest snapshot. Tailscale releases security and feature updates regularly; a device showing UpdateAvailable lacks the most recent improvements. Use this list to plan a fleet update, especially before enabling new ACL features that require minimum client versions. +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Devices_CL +tactics: + - DefenseEvasion +relevantTechniques: + - T1562 +query: | + Tailscale_Devices_CL + | summarize arg_max(TimeGenerated, *) by DeviceId + | where UpdateAvailable == true + | project DeviceName, Hostname, User, Os, Distro, ClientVersion, LastSeen, Tags + | order by ClientVersion asc, LastSeen desc +entityMappings: + - entityType: Host + fieldMappings: + - identifier: HostName + columnName: Hostname +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumBeaconingCandidates.yaml b/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumBeaconingCandidates.yaml new file mode 100644 index 00000000000..48cddb9b063 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumBeaconingCandidates.yaml @@ -0,0 +1,43 @@ +id: b28cbdd2-8cef-be9b-a38e-7faceedfdbec +name: "Tailscale Premium: Beaconing candidates (regular periodic flows)" +description: Identifies flows that recur at a highly regular interval, which is the signature of C2 beaconing or scheduled exfiltration jobs. Looser threshold than the analytic rule - investigation aid. +description-detailed: | + Identifies flows that recur at a highly regular interval, which is the signature of C2 beaconing or scheduled exfiltration jobs. Looks for src->dst pairs where 80%+ of inter-flow gaps cluster around the same delta. Requires Tailscale Premium or Enterprise. +requiredDataConnectors: + - connectorId: TailscalePremiumCCF + dataTypes: + - Tailscale_Network_CL +tactics: + - CommandAndControl + - Exfiltration +relevantTechniques: + - T1071 + - T1095 + - T1029 +query: | + let lookback = 2d; + let minFlows = 10; + let beaconPercentThreshold = 80.0; + Tailscale_Network_CL + | where TimeGenerated > ago(lookback) + | mv-expand t = VirtualTraffic + | extend Src = tostring(t.src), Dst = tostring(t.dst), Proto = toint(t.proto) + | project TimeGenerated, Src, Dst, Proto + | sort by Src asc, Dst asc, Proto asc, TimeGenerated asc + | serialize + | extend NextTime = next(TimeGenerated), NextSrc = next(Src), NextDst = next(Dst), NextProto = next(Proto) + | where Src == NextSrc and Dst == NextDst and Proto == NextProto + | extend DeltaSec = datetime_diff('second', NextTime, TimeGenerated) + | where DeltaSec > 5 + | summarize DeltaCount = count() by Src, Dst, Proto, DeltaSec + | summarize (MostFrequentDeltaCount, MostFrequentDeltaSec) = arg_max(DeltaCount, DeltaSec), TotalFlows = sum(DeltaCount) by Src, Dst, Proto + | where TotalFlows >= minFlows + | extend BeaconPercent = round(MostFrequentDeltaCount * 100.0 / TotalFlows, 1) + | where BeaconPercent >= beaconPercentThreshold + | order by BeaconPercent desc +entityMappings: + - entityType: IP + fieldMappings: + - identifier: Address + columnName: Src +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumCrossTagFlowMatrix.yaml b/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumCrossTagFlowMatrix.yaml new file mode 100644 index 00000000000..9b424a7221a --- /dev/null +++ b/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumCrossTagFlowMatrix.yaml @@ -0,0 +1,42 @@ +id: a8978f27-3c85-4c29-a45a-c4a5e43fef2d +name: "Tailscale Premium: Cross-tag flow matrix" +description: Identifies network flows pivoted by source-tag x destination-tag over 7 days. Highlights tag-to-tag traffic, useful for ACL validation. Same-tag loops can signal worm-style propagation. +description-detailed: | + Identifies network flows pivoted by source-tag x destination-tag over the past 7 days, treating untagged user devices as ``. Highlights which tagged services interact - useful for ACL validation, detecting unexpected tag-to-tag traffic, and spotting tag-to-same-tag loops that can indicate worm-style propagation or service-mesh anomalies. Order by Flows to find the heaviest tag pairs; sort by DistinctSrcDevices to find broadly-used services. Requires Tailscale Premium or Enterprise. +requiredDataConnectors: + - connectorId: TailscalePremiumCCF + dataTypes: + - Tailscale_Network_CL +tactics: + - LateralMovement + - Discovery +relevantTechniques: + - T1021 + - T1046 +query: | + Tailscale_Network_CL + | where TimeGenerated > ago(7d) + | extend SrcCategory = case( + isnotempty(SrcTags), tostring(SrcTags), + isnotempty(SrcUser), "", + "") + | extend DstCategory = case( + isnotempty(DstTags), tostring(DstTags), + isnotempty(DstUser), "", + "") + | summarize + Flows = count(), + DistinctSrcDevices = dcount(SrcNodeName), + DistinctDstDevices = dcount(DstNodeName), + FirstSeen = min(TimeGenerated), + LastSeen = max(TimeGenerated) + by SrcCategory, DstCategory + | extend SameTagLoop = SrcCategory == DstCategory and SrcCategory != "" and SrcCategory != "" + | project SrcCategory, DstCategory, Flows, DistinctSrcDevices, DistinctDstDevices, SameTagLoop, FirstSeen, LastSeen + | order by Flows desc +entityMappings: + - entityType: CloudApplication + fieldMappings: + - identifier: Name + columnName: SrcCategory +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumDerpRelayPersistence.yaml b/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumDerpRelayPersistence.yaml new file mode 100644 index 00000000000..25e55a16556 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumDerpRelayPersistence.yaml @@ -0,0 +1,38 @@ +id: 20457fba-08e2-42d7-b972-fbe9acf583c8 +name: "Tailscale Premium: Devices with persistent DERP relay usage" +description: Identifies devices that have consistently fallen back to DERP relay (IsRelayed=true) over the past 24 hours. Sustained relay usage points to NAT/firewall misconfiguration or deliberate evasion. +description-detailed: | + Identifies devices that have consistently fallen back to DERP relay (IsRelayed = true) over the past 24 hours. Sustained relay usage points to NAT/firewall misconfiguration on the device's network, a tunnel-blocking middlebox, or in rare cases deliberate evasion attempting to obscure direct peer-to-peer paths. Useful for proactive network-hygiene investigation and capacity planning. Requires Tailscale Premium or Enterprise. +requiredDataConnectors: + - connectorId: TailscalePremiumCCF + dataTypes: + - Tailscale_Network_CL +tactics: + - CommandAndControl +relevantTechniques: + - T1572 +query: | + Tailscale_Network_CL + | where TimeGenerated > ago(24h) + | summarize + TotalFlows = count(), + RelayedFlows = countif(IsRelayed), + DistinctDsts = dcount(DstNodeName), + FirstFlow = min(TimeGenerated), + LastFlow = max(TimeGenerated) + by SrcNodeName, SrcUser, SrcOs, SrcTags=tostring(SrcTags) + | where TotalFlows >= 50 + | extend RelayedPct = round(100.0 * RelayedFlows / TotalFlows, 1) + | where RelayedPct >= 30.0 + | project SrcNodeName, SrcUser, SrcOs, SrcTags, TotalFlows, RelayedFlows, RelayedPct, DistinctDsts, FirstFlow, LastFlow + | order by RelayedPct desc, TotalFlows desc +entityMappings: + - entityType: Host + fieldMappings: + - identifier: HostName + columnName: SrcNodeName + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: SrcUser +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumExitNodeUsage.yaml b/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumExitNodeUsage.yaml new file mode 100644 index 00000000000..799b953ac78 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumExitNodeUsage.yaml @@ -0,0 +1,36 @@ +id: a37bacc1-7bde-ad8a-f27d-6e9bcdcecadb +name: "Tailscale Premium: Exit-node usage patterns" +description: Identifies traffic leaving the tailnet via exit nodes. Exit-node use is typically intentional (regional egress, privacy routing) but unexpected egress from a node warrants investigation. +description-detailed: | + Identifies traffic leaving the tailnet via exit nodes. Exit node use is typically intentional (regional egress, privacy routing) but unexpected exit-node traffic from a node should be investigated as a potential pivot point or unsanctioned egress. Requires Tailscale Premium or Enterprise. +requiredDataConnectors: + - connectorId: TailscalePremiumCCF + dataTypes: + - Tailscale_Network_CL +tactics: + - CommandAndControl + - Exfiltration +relevantTechniques: + - T1090 + - T1041 +query: | + Tailscale_Network_CL + | where TimeGenerated > ago(1d) + | where array_length(ExitTraffic) > 0 + | mv-expand t = ExitTraffic + | extend Src = tostring(t.src), ExitDst = tostring(t.dst), Proto = toint(t.proto), TxBytes = tolong(t.txBytes), RxBytes = tolong(t.rxBytes) + | summarize TotalBytes = sum(TxBytes + RxBytes), FlowCount = count(), ExitDestinations = make_set(ExitDst, 25) + by NodeId, SrcNodeName = tostring(SrcNode.name), Src + | extend TotalMB = round(TotalBytes / 1024.0 / 1024.0, 2) + | order by TotalBytes desc + | take 100 +entityMappings: + - entityType: Host + fieldMappings: + - identifier: HostName + columnName: SrcNodeName + - entityType: IP + fieldMappings: + - identifier: Address + columnName: Src +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumNewNodePairs.yaml b/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumNewNodePairs.yaml new file mode 100644 index 00000000000..cc6263a845b --- /dev/null +++ b/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumNewNodePairs.yaml @@ -0,0 +1,39 @@ +id: e55f8aaf-5fbc-8b6e-d05b-4c7faabcadbe +name: "Tailscale Premium: New src->dst node pairs (lateral movement candidates)" +description: Identifies tailnet src->dst pairs observed in the last 24h that were NOT observed in the prior 7-day baseline. Useful for spotting lateral movement to nodes that don't usually talk. +description-detailed: | + Identifies tailnet src->dst pairs observed in the last 24h that were NOT observed in the prior 7-day baseline. Useful for spotting lateral movement to nodes that don't usually talk to each other. Requires Tailscale Premium or Enterprise (network flow logs). +requiredDataConnectors: + - connectorId: TailscalePremiumCCF + dataTypes: + - Tailscale_Network_CL +tactics: + - LateralMovement + - Discovery +relevantTechniques: + - T1021 + - T1018 +query: | + let recent = 1d; + let baseline = 7d; + let recentPairs = + Tailscale_Network_CL + | where TimeGenerated > ago(recent) + | mv-expand t = VirtualTraffic + | extend Src = tostring(t.src), Dst = tostring(t.dst), Proto = toint(t.proto) + | summarize FirstSeen = min(TimeGenerated), TxBytes = sum(tolong(t.txBytes)), RxBytes = sum(tolong(t.rxBytes)), FlowCount = count() by Src, Dst, Proto; + let baselinePairs = + Tailscale_Network_CL + | where TimeGenerated between (ago(baseline + recent) .. ago(recent)) + | mv-expand t = VirtualTraffic + | extend Src = tostring(t.src), Dst = tostring(t.dst), Proto = toint(t.proto) + | distinct Src, Dst, Proto; + recentPairs + | join kind=leftanti baselinePairs on Src, Dst, Proto + | order by FirstSeen asc +entityMappings: + - entityType: IP + fieldMappings: + - identifier: Address + columnName: Src +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumOffHoursFlows.yaml b/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumOffHoursFlows.yaml new file mode 100644 index 00000000000..4990f80cdec --- /dev/null +++ b/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumOffHoursFlows.yaml @@ -0,0 +1,41 @@ +id: 622ce88a-0838-4bbe-8a00-ab8ac8377f41 +name: "Tailscale Premium: Network flows outside business hours" +description: Identifies network flows occurring outside 07:00-19:00 UTC on weekdays plus all weekend, over 7 days. Filters to virtual/subnet/exit traffic. Useful for spotting unattended automation gone wrong. +description-detailed: | + Identifies network flows occurring outside 07:00-19:00 UTC on weekdays, plus all of weekend, over the past 7 days. Filters to virtual/subnet/exit traffic (drops DERP-only keepalive noise). Useful for spotting unattended automation gone wrong, scheduled exfiltration, or unsanctioned after-hours access by humans. The TaggedSource column makes it easy to separate cron-like service traffic (tag:backup, tag:cron) from human user activity. Requires Tailscale Premium or Enterprise. +requiredDataConnectors: + - connectorId: TailscalePremiumCCF + dataTypes: + - Tailscale_Network_CL +tactics: + - Exfiltration + - CommandAndControl +relevantTechniques: + - T1029 + - T1071 +query: | + Tailscale_Network_CL + | where TimeGenerated > ago(7d) + | where HasVirtualTraffic or HasSubnetTraffic or HasExitTraffic + | extend HourUtc = hourofday(TimeGenerated), Dow = dayofweek(TimeGenerated) + | where HourUtc < 7 or HourUtc >= 19 or Dow in (0d, 6d) + | extend TaggedSource = isnotempty(SrcTags) + | summarize + Flows = count(), + FirstFlow = min(TimeGenerated), + LastFlow = max(TimeGenerated), + DistinctHours = dcount(bin(TimeGenerated, 1h)) + by SrcNodeName, SrcUser, SrcTags=tostring(SrcTags), DstNodeName, DstTags=tostring(DstTags), TaggedSource + | where Flows >= 5 + | project SrcNodeName, SrcUser, SrcTags, TaggedSource, DstNodeName, DstTags, Flows, DistinctHours, FirstFlow, LastFlow + | order by Flows desc +entityMappings: + - entityType: Host + fieldMappings: + - identifier: HostName + columnName: SrcNodeName + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: SrcUser +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumPostureInventory.yaml b/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumPostureInventory.yaml new file mode 100644 index 00000000000..5a245511699 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumPostureInventory.yaml @@ -0,0 +1,24 @@ +id: c3d4e5f6-7890-1234-56ab-cdef12345032 +name: "Tailscale Premium: Current posture integration inventory" +description: Identifies the current set of device-posture integrations configured on the tailnet (latest snapshot per integration). Useful for compliance attestation and detecting drift from the expected baseline. +description-detailed: | + Identifies the current set of device-posture integrations configured on the tailnet (latest snapshot per integration). Useful for compliance attestation - confirms what MDM/EDR systems are connected and what their status is. Compare against the expected baseline to detect drift. Requires Tailscale Premium or Enterprise. +requiredDataConnectors: + - connectorId: TailscalePremiumCCF + dataTypes: + - Tailscale_PostureIntegrations_CL +tactics: + - DefenseEvasion +relevantTechniques: + - T1562 +query: | + Tailscale_PostureIntegrations_CL + | summarize arg_max(TimeGenerated, *) by IntegrationId + | project IntegrationId, Provider, ClientId, TenantId_Provider, Status, ConfigOverwrites + | order by Provider asc +entityMappings: + - entityType: CloudApplication + fieldMappings: + - identifier: Name + columnName: Provider +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumTaggedServiceFanIn.yaml b/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumTaggedServiceFanIn.yaml new file mode 100644 index 00000000000..23fc4a069ee --- /dev/null +++ b/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumTaggedServiceFanIn.yaml @@ -0,0 +1,37 @@ +id: f8d4e7bc-3450-4c55-84ac-90e6e9c6b8fe +name: "Tailscale Premium: Tagged services with broad inbound exposure" +description: Identifies tagged services (devices with non-empty DstTags) ranked by inbound diversity over 7 days. Surfaces services with the broadest blast-radius; ACL drift candidates. +description-detailed: | + Identifies tagged services (devices with non-empty DstTags) ranked by inbound diversity over 7 days. Baseline: a tag:plex serving the household typically sees connections from 1-3 user devices; a tag:db or tag:internal that receives connections from 10+ distinct users or 3+ OS families may indicate ACL drift, credential sharing, or unauthorised access. Sort by DistinctSrcDevices to surface services with the broadest blast-radius. Requires Tailscale Premium or Enterprise. +requiredDataConnectors: + - connectorId: TailscalePremiumCCF + dataTypes: + - Tailscale_Network_CL +tactics: + - LateralMovement + - InitialAccess +relevantTechniques: + - T1021 + - T1133 +query: | + Tailscale_Network_CL + | where TimeGenerated > ago(7d) + | where isnotempty(DstTags) + | where HasVirtualTraffic or HasSubnetTraffic + | summarize + DistinctSrcUsers = dcount(SrcUser), + DistinctSrcDevices = dcount(SrcNodeName), + DistinctSrcOs = dcount(SrcOs), + Flows = count(), + FirstFlow = min(TimeGenerated), + LastFlow = max(TimeGenerated) + by DstNodeName, DstTags=tostring(DstTags) + | extend ShortDstName = tostring(split(DstNodeName, ".")[0]) + | project ShortDstName, DstTags, DistinctSrcDevices, DistinctSrcUsers, DistinctSrcOs, Flows, FirstFlow, LastFlow + | order by DistinctSrcDevices desc, Flows desc +entityMappings: + - entityType: Host + fieldMappings: + - identifier: HostName + columnName: ShortDstName +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumTopTalkers.yaml b/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumTopTalkers.yaml new file mode 100644 index 00000000000..8d4d8455275 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumTopTalkers.yaml @@ -0,0 +1,28 @@ +id: f46a9bb0-6acd-9c7f-e16c-5d8abbcdbeca +name: "Tailscale Premium: Top talkers by bytes (virtual traffic)" +description: | + Identifies tailnet src->dst pairs ranked by total bytes transferred over the last 24h. Useful for capacity planning, identifying data-heavy flows, and spotting unexpected volume that could indicate data staging. Requires Tailscale Premium or Enterprise. +requiredDataConnectors: + - connectorId: TailscalePremiumCCF + dataTypes: + - Tailscale_Network_CL +tactics: + - Exfiltration + - Collection +relevantTechniques: + - T1041 + - T1567 +query: | + Tailscale_Network_CL + | where TimeGenerated > ago(1d) + | mv-expand t = VirtualTraffic + | extend Src = tostring(t.src), Dst = tostring(t.dst), Proto = toint(t.proto), TxBytes = tolong(t.txBytes), RxBytes = tolong(t.rxBytes) + | summarize TotalBytes = sum(TxBytes + RxBytes), TotalPackets = sum(tolong(t.txPkts) + tolong(t.rxPkts)), FlowCount = count() by Src, Dst, Proto + | extend TotalMB = round(TotalBytes / 1024.0 / 1024.0, 2) + | top 50 by TotalBytes +entityMappings: + - entityType: IP + fieldMappings: + - identifier: Address + columnName: Src +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumUserMultiDevice.yaml b/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumUserMultiDevice.yaml new file mode 100644 index 00000000000..0a2df129d51 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Hunting Queries/TailscalePremiumUserMultiDevice.yaml @@ -0,0 +1,44 @@ +id: daac10bd-d842-4122-90cc-9957256f04e3 +name: "Tailscale Premium: Users generating traffic from multiple devices" +description: Identifies users (SrcUser) generating tailnet flows from more than one distinct device in the past 24 hours. Useful for spotting account compromise (sudden new device) or unauthorised device enrollment. +description-detailed: | + Identifies users (SrcUser) generating tailnet flows from more than one distinct device in the past 24 hours. Normal for a user with phone + laptop. Useful for spotting account compromise (sudden new device for a user), unauthorised device enrollment, or device sharing across users. Cross-reference NewDevicesToday against Tailscale_Devices_CL.Created to confirm whether each device is genuinely new vs long-known. Requires Tailscale Premium or Enterprise. +requiredDataConnectors: + - connectorId: TailscalePremiumCCF + dataTypes: + - Tailscale_Network_CL + - Tailscale_Devices_CL +tactics: + - InitialAccess + - Persistence +relevantTechniques: + - T1078 +query: | + let recent = Tailscale_Network_CL + | where TimeGenerated > ago(24h) + | where isnotempty(SrcUser) + | summarize + Devices = make_set(SrcNodeName, 20), + DeviceCount = dcount(SrcNodeName), + OsTypes = make_set(SrcOs, 10), + FirstFlow = min(TimeGenerated), + LastFlow = max(TimeGenerated), + Flows = count() + by SrcUser + | where DeviceCount >= 2; + let devicesCreatedToday = Tailscale_Devices_CL + | where TimeGenerated > ago(24h) + | summarize arg_max(TimeGenerated, *) by DeviceId + | where Created > ago(24h) + | distinct DeviceName; + recent + | extend NewDevicesToday = set_intersect(Devices, toscalar(devicesCreatedToday | summarize make_set(DeviceName))) + | extend HasNewDevice = array_length(NewDevicesToday) > 0 + | project SrcUser, DeviceCount, Devices, OsTypes, HasNewDevice, NewDevicesToday, Flows, FirstFlow, LastFlow + | order by HasNewDevice desc, DeviceCount desc +entityMappings: + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: SrcUser +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleSplitDnsPerDomainChanges.yaml b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleSplitDnsPerDomainChanges.yaml new file mode 100644 index 00000000000..e871bc93bfa --- /dev/null +++ b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleSplitDnsPerDomainChanges.yaml @@ -0,0 +1,43 @@ +id: e7f8a9b0-3456-7890-12ab-cdef12345012 +name: "Tailscale: Split-DNS per-domain change history" +description: Identifies the per-domain Split-DNS change history from the audit log. For each modification event, expands Old and New documents and surfaces which domains were added, removed, or changed. +description-detailed: | + Identifies the per-domain Split-DNS change history from the audit log. For each modification event, expands the Old and New documents and surfaces which domains were added, removed, or had their resolver IPs changed. Useful for forensic review after a suspected DNS hijack and for confirming that a planned DNS migration deployed as intended. +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Audit_CL +tactics: + - DefenseEvasion + - CommandAndControl +relevantTechniques: + - T1556 + - T1568 +query: | + Tailscale_Audit_CL + | where Action == "UPDATE" + | where tostring(Target.type) == "TAILNET" + | where tostring(Target.property) == "DNS_SPLIT_DNS" + | extend ActorLogin = tostring(Actor.loginName) + | extend OldKeys = bag_keys(Old), NewKeys = bag_keys(New) + | extend AllDomains = set_union(OldKeys, NewKeys) + | mv-expand Domain = AllDomains to typeof(string) + | extend OldResolvers = Old[Domain], NewResolvers = New[Domain] + | extend Change = case( + isnull(OldResolvers) and isnotnull(NewResolvers), "added", + isnotnull(OldResolvers) and isnull(NewResolvers), "removed", + tostring(OldResolvers) != tostring(NewResolvers), "resolvers-changed", + "unchanged") + | where Change != "unchanged" + | project TimeGenerated, ActorLogin, Domain, Change, OldResolvers, NewResolvers + | order by TimeGenerated desc +entityMappings: + - entityType: Account + fieldMappings: + - identifier: FullName + columnName: ActorLogin + - entityType: DNS + fieldMappings: + - identifier: DomainName + columnName: Domain +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleSubnetRouteExposure.yaml b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleSubnetRouteExposure.yaml new file mode 100644 index 00000000000..fc31c96f864 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Hunting Queries/TailscaleSubnetRouteExposure.yaml @@ -0,0 +1,30 @@ +id: f6a7b8c9-0123-4567-8901-234567890047 +name: "Tailscale: Subnet router CIDR exposure inventory" +description: Identifies every device currently advertising or enabling subnet routes (bridging non-tailnet networks into the tailnet). Excludes pure exit-node advertisements so only true subnet exposure is surfaced. +description-detailed: | + Identifies every device currently advertising or enabling subnet routes (i.e., bridging non-tailnet networks into the tailnet). Excludes pure exit-node advertisements (0.0.0.0/0 and ::/0) so only true subnet exposure is surfaced. Use this for change-control - confirm every CIDR listed is intended to be reachable from the tailnet, and that the device's ACL restricts who can route through it. +requiredDataConnectors: + - connectorId: TailscaleCCF + dataTypes: + - Tailscale_Devices_CL +tactics: + - LateralMovement +relevantTechniques: + - T1021 + - T1018 +query: | + let exitRoutes = dynamic(["0.0.0.0/0", "::/0"]); + Tailscale_Devices_CL + | summarize arg_max(TimeGenerated, *) by DeviceId + | where array_length(AdvertisedRoutes) > 0 or array_length(EnabledRoutes) > 0 + | extend SubnetAdvertised = set_difference(AdvertisedRoutes, exitRoutes) + | extend SubnetEnabled = set_difference(EnabledRoutes, exitRoutes) + | where array_length(SubnetAdvertised) > 0 or array_length(SubnetEnabled) > 0 + | project DeviceName, Hostname, User, Os, SubnetAdvertised, SubnetEnabled, Tags, LastSeen + | order by DeviceName asc +entityMappings: + - entityType: Host + fieldMappings: + - identifier: HostName + columnName: Hostname +version: 1.0.0 diff --git a/Solutions/Tailscale (CCF)/PREMIUM-ENDPOINTS.md b/Solutions/Tailscale (CCF)/PREMIUM-ENDPOINTS.md new file mode 100644 index 00000000000..a4d182b0957 --- /dev/null +++ b/Solutions/Tailscale (CCF)/PREMIUM-ENDPOINTS.md @@ -0,0 +1,41 @@ +# Premium / Enterprise Tailscale endpoints + +Endpoints that gave 403 or are explicitly Premium-tier features. These are NOT polled by the **Tailscale Standard (CCF)** connector. They should be added to **Tailscale Premium (CCF)** when validating that connector against a real Premium tailnet. + +## Already in Premium connector + +- `/logging/network` -> `Tailscale_Network_CL` - network flow logs (Premium feature; scope `logs:network:read`) + +## Premium-only data sources to ADD to Premium connector + +| Endpoint | Scope | Yields | Notes | +|---|---|---|---| +| `/posture/integrations` | `feature_settings:read` | List of MDM/EDR integrations (Jamf, Kandji, Intune, Kolide, Microsoft Defender for Endpoint, CrowdStrike Falcon, SentinelOne, etc.) configured for device posture enforcement | Endpoint returns 200 with empty list on Standard tier - only meaningful on Premium | + +## Tier-gated but uses scopes we already grant + +| Endpoint | Scope | Status | Notes | +|---|---|---|---| +| `/user-invites` | needs WRITE `users` scope | 403 with `users:read` | Tailscale requires write scope to read invites (PII concern). Audit log captures invite events (CREATE/UPDATE/DELETE on USER_INVITE targets) so we have visibility without granting elevated scope. | +| `/acl` snapshot | `policy_file:read` | 403 with our scopes | Audit log captures full ACL document on every change (Old + New + Actor). Snapshot is redundant. | + +## Aliases / paths that 404 because the data lives at the canonical name + +Some external references (third-party MCP servers, blog posts) use names that Tailscale's API returns 404 for at the exact path - but the data is already available via the canonical endpoint, which the connector polls today. Don't add new pollers for these: + +- `/audit-logs` -> canonical path is `/logging/configuration`, polled by both Standard and Premium audit pollers. Returns `{"logs":[...]}` with `start`/`end` query params (verified live, May 2026). +- `/network-flow-logs` -> canonical path is `/logging/network`, polled by the Premium network poller. `/network-logs` is also a working alias for the same data. + +## Endpoints that 404'd on Tailscale's API (tier-independent) + +Genuinely missing paths - no known canonical alternative: + +- `/services`, `/services/*` - Tailscale Services (newer feature, may require enablement) +- `/contacts` - tailnet contact info +- `/status`, `/log-streaming`, `/nameservers` - alternate paths used by some clients + +If Tailscale ships these endpoints in future, add as needed. + +## Split-DNS coverage decision + +`/dns/split-dns` returns a dynamic-key object (`{"domain.example": ["resolver"]}`) that doesn't fit Sentinel CCP's strict-schema model. Covered via audit-log-based analytic rules and a hunting query instead - strictly richer than a periodic snapshot because we get the full Old + New diff + actor attribution on every change. diff --git a/Solutions/Tailscale (CCF)/Package/3.0.0.zip b/Solutions/Tailscale (CCF)/Package/3.0.0.zip new file mode 100644 index 00000000000..2e07c0f245d Binary files /dev/null and b/Solutions/Tailscale (CCF)/Package/3.0.0.zip differ diff --git a/Solutions/Tailscale (CCF)/Package/createUiDefinition.json b/Solutions/Tailscale (CCF)/Package/createUiDefinition.json new file mode 100644 index 00000000000..0084ccf6de7 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Package/createUiDefinition.json @@ -0,0 +1,854 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/0.1.2-preview/CreateUIDefinition.MultiVm.json#", + "handler": "Microsoft.Azure.CreateUIDef", + "version": "0.1.2-preview", + "parameters": { + "config": { + "isWizard": false, + "basics": { + "description": "\n\n**Note:** Please refer to the following before installing the solution: \n\n• Review the solution [Release Notes](https://github.com/Azure/Azure-Sentinel/tree/master/Solutions/Tailscale%20%28CCF%29/ReleaseNotes.md)\n\n • There may be [known issues](https://aka.ms/sentinelsolutionsknownissues) pertaining to this Solution, please refer to them before installing.\n\nThe [Tailscale](https://tailscale.com/) solution for Microsoft Sentinel ingests Tailscale identity, device, configuration, audit and (Premium) network-flow telemetry via OAuth2-secured APIs. Built on the Codeless Connector Framework (CCF) - no Function App or container required.\n\n**Data connectors in this solution (install the one matching your Tailscale plan):**\n- **Tailscale Standard (CCF)** - Configuration audit, devices, users, keys, webhooks, DNS, settings. Use on **Personal (Free), Starter and Premium** tailnets.\n- **Tailscale Premium (CCF)** - Everything in Standard plus network flow logs and posture integrations. Use on **Premium and Enterprise** tailnets for full coverage.\n\n**Pre-requisites:**\n1. Sign in to [Tailscale OAuth settings](https://login.tailscale.com/admin/settings/oauth)\n2. Create an OAuth client with the scopes for your tier (see the README in this solution).\n3. Copy the client ID and client secret (secret shown once).\n4. Note your tailnet name (e.g. `tailb094d7.ts.net`) from the [Keys page](https://login.tailscale.com/admin/settings/keys).\n\n**Data Connectors:** 2, **Parsers:** 2, **Workbooks:** 2, **Analytic Rules:** 24, **Hunting Queries:** 22\n\n[Learn more about Microsoft Sentinel](https://aka.ms/azuresentinel) | [Learn more about Solutions](https://aka.ms/azuresentinelsolutionsdoc)", + "subscription": { + "resourceProviders": [ + "Microsoft.OperationsManagement/solutions", + "Microsoft.OperationalInsights/workspaces/providers/alertRules", + "Microsoft.Insights/workbooks", + "Microsoft.Logic/workflows" + ] + }, + "location": { + "metadata": { + "hidden": "Hiding location, we get it from the log analytics workspace" + }, + "visible": false + }, + "resourceGroup": { + "allowExisting": true + } + } + }, + "basics": [ + { + "name": "getLAWorkspace", + "type": "Microsoft.Solutions.ArmApiControl", + "toolTip": "This filters by workspaces that exist in the Resource Group selected", + "condition": "[greater(length(resourceGroup().name),0)]", + "request": { + "method": "GET", + "path": "[concat(subscription().id,'/providers/Microsoft.OperationalInsights/workspaces?api-version=2020-08-01')]" + } + }, + { + "name": "workspace", + "type": "Microsoft.Common.DropDown", + "label": "Workspace", + "placeholder": "Select a workspace", + "toolTip": "This dropdown will list only workspace that exists in the Resource Group selected", + "constraints": { + "allowedValues": "[map(filter(basics('getLAWorkspace').value, (filter) => contains(toLower(filter.id), toLower(resourceGroup().name))), (item) => parse(concat('{\"label\":\"', item.name, '\",\"value\":\"', item.name, '\"}')))]", + "required": true + }, + "visible": true + } + ], + "steps": [ + { + "name": "dataconnectors", + "label": "Data Connectors", + "bladeTitle": "Data Connectors", + "elements": [ + { + "name": "dataconnectors1-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "This Solution installs the data connector for Tailscale Standard (CCF). You can get Tailscale Standard (CCF) data in your Microsoft Sentinel workspace. After installing the solution, configure and enable this data connector by following guidance in Manage solution view." + } + }, + { + "name": "dataconnectors-link1", + "type": "Microsoft.Common.TextBlock", + "options": { + "link": { + "label": "Learn more about connecting data sources", + "uri": "https://docs.microsoft.com/azure/sentinel/connect-data-sources" + } + } + }, + { + "name": "dataconnectors2-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "This Solution installs the data connector for Tailscale Premium (CCF). You can get Tailscale Premium (CCF) data in your Microsoft Sentinel workspace. After installing the solution, configure and enable this data connector by following guidance in Manage solution view." + } + }, + { + "name": "dataconnectors-link2", + "type": "Microsoft.Common.TextBlock", + "options": { + "link": { + "label": "Learn more about connecting data sources", + "uri": "https://docs.microsoft.com/azure/sentinel/connect-data-sources" + } + } + } + ] + }, + { + "name": "workbooks", + "label": "Workbooks", + "subLabel": { + "preValidation": "Configure the workbooks", + "postValidation": "Done" + }, + "bladeTitle": "Workbooks", + "elements": [ + { + "name": "workbooks-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "This solution installs workbook(s) to help you gain insights into the telemetry collected in Microsoft Sentinel. After installing the solution, start using the workbook in Manage solution view." + } + }, + { + "name": "workbooks-link", + "type": "Microsoft.Common.TextBlock", + "options": { + "link": { + "label": "Learn more", + "uri": "https://docs.microsoft.com/azure/sentinel/tutorial-monitor-your-data" + } + } + }, + { + "name": "workbook1", + "type": "Microsoft.Common.Section", + "label": "Tailscale Operations (Standard)", + "elements": [ + { + "name": "workbook1-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Tailscale Operations workbook for Standard tier. Nine tabs covering an at-a-glance KPI hero row, audit activity overview, an actor + device drilldown (Investigate), embedded hunting queries (first-seen actors, off-hours changes, key-expiry-disabled devices, never-expire auth keys, outdated clients, dormant devices, subnet route exposure, SSH-enabled devices), identity (user roles, status, last login recency, role escalation history, orphaned users), devices (OS / version / tag distribution, devices needing attention, full inventory, subnet routers), credentials (expiry timeline, never-expire flag, CRUD events), admin audit (action heatmap, actor x action heatmap, recent 100), network and DNS (current snapshot, tailnet policy gates, ACL change history), and pipeline health (per-table freshness, ingest rate, operational events). Driven by data polled from the Tailscale REST API." + } + } + ] + }, + { + "name": "workbook2", + "type": "Microsoft.Common.Section", + "label": "Tailscale Operations (Premium)", + "elements": [ + { + "name": "workbook2-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Tailscale Operations workbook for Premium / Enterprise tier - everything in the Standard workbook plus network flow analysis (top talkers, src-dst pairs, exit-node egress, beaconing candidates) and posture integration inventory." + } + } + ] + } + ] + }, + { + "name": "analytics", + "label": "Analytics", + "subLabel": { + "preValidation": "Configure the analytics", + "postValidation": "Done" + }, + "bladeTitle": "Analytics", + "elements": [ + { + "name": "analytics-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "This solution installs the following analytic rule templates. After installing the solution, create and enable analytic rules in Manage solution view." + } + }, + { + "name": "analytics-link", + "type": "Microsoft.Common.TextBlock", + "options": { + "link": { + "label": "Learn more", + "uri": "https://docs.microsoft.com/azure/sentinel/tutorial-detect-threats-custom?WT.mc_id=Portal-Microsoft_Azure_CreateUIDef" + } + } + }, + { + "name": "analytic1", + "type": "Microsoft.Common.Section", + "label": "Tailscale: New API access token or OAuth client created", + "elements": [ + { + "name": "analytic1-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies when a new API access token or OAuth client is created in the tailnet. These grant programmatic access - verify the actor and intent." + } + } + ] + }, + { + "name": "analytic2", + "type": "Microsoft.Common.Section", + "label": "Tailscale: OAuth client or API key created with write scopes", + "elements": [ + { + "name": "analytic2-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies creation of a Tailscale OAuth client or API access key whose granted scopes include WRITE permissions (anything matching :write). Tokens with write scopes are high-value adversary targets." + } + } + ] + }, + { + "name": "analytic3", + "type": "Microsoft.Common.Section", + "label": "Tailscale: Policy file (ACL) modified", + "elements": [ + { + "name": "analytic3-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies when the tailnet ACL/policy file is modified. Review the diff - incorrect ACLs can silently expand blast radius across the tailnet." + } + } + ] + }, + { + "name": "analytic4", + "type": "Microsoft.Common.Section", + "label": "Tailscale: Auth key created", + "elements": [ + { + "name": "analytic4-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies when a new Tailscale auth key is generated. Auth keys allow unattended device enrollment into the tailnet - confirm it was expected and revoke if not." + } + } + ] + }, + { + "name": "analytic5", + "type": "Microsoft.Common.Section", + "label": "Tailscale: Exit node advertised or approved", + "elements": [ + { + "name": "analytic5-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies when a device starts advertising itself as an exit node, or when an admin approves one. Validate the device and operator - rogue exit nodes can intercept tailnet egress." + } + } + ] + }, + { + "name": "analytic6", + "type": "Microsoft.Common.Section", + "label": "Tailscale: Mass credential revocation in short window", + "elements": [ + { + "name": "analytic6-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies when five or more API keys, OAuth clients, or auth keys are revoked or deleted within one hour. May be routine rotation, or a typical cleanup pattern after credential compromise." + } + } + ] + }, + { + "name": "analytic7", + "type": "Microsoft.Common.Section", + "label": "Tailscale: Device key expiring within 7 days", + "elements": [ + { + "name": "analytic7-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies tailnet devices whose machine key expires within the next 7 days and where key expiry is not disabled. Surface proactively so renewal can be scheduled rather than forced during an outage." + } + } + ] + }, + { + "name": "analytic8", + "type": "Microsoft.Common.Section", + "label": "Tailscale: Device started advertising subnet routes", + "elements": [ + { + "name": "analytic8-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "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." + } + } + ] + }, + { + "name": "analytic9", + "type": "Microsoft.Common.Section", + "label": "Tailscale: User role elevated to admin or owner", + "elements": [ + { + "name": "analytic9-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies when a user's tailnet role changes from a lower-privilege role to admin, network-admin, or owner between consecutive snapshots. Privilege escalation is a high-value attacker objective and warrants prompt review." + } + } + ] + }, + { + "name": "analytic10", + "type": "Microsoft.Common.Section", + "label": "Tailscale: Split-DNS configuration modified", + "elements": [ + { + "name": "analytic10-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies when the tailnet split-DNS configuration is modified. Split-DNS overrides per-domain resolution within the tailnet - an attacker adding a new domain mapping or changing the resolver IP can hijack DNS for that domain." + } + } + ] + }, + { + "name": "analytic11", + "type": "Microsoft.Common.Section", + "label": "Tailscale: DNS nameservers modified", + "elements": [ + { + "name": "analytic11-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies when the tailnet's global DNS nameserver list is modified. Adding an attacker-controlled resolver as a tailnet-wide nameserver enables broad DNS hijacking for every device using MagicDNS resolution." + } + } + ] + }, + { + "name": "analytic12", + "type": "Microsoft.Common.Section", + "label": "Tailscale: MagicDNS disabled", + "elements": [ + { + "name": "analytic12-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies when MagicDNS is turned off on the tailnet. Disabling MagicDNS changes DNS behaviour for every device and is occasionally a precursor to wider DNS hijacking - verify the change was intentional." + } + } + ] + }, + { + "name": "analytic13", + "type": "Microsoft.Common.Section", + "label": "Tailscale: Tailnet lock validation failed", + "elements": [ + { + "name": "analytic13-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies tailnet devices with a non-empty TailnetLockError, indicating the device failed tailnet-lock cryptographic validation. Suspicious - likely an unsigned node attempting to join." + } + } + ] + }, + { + "name": "analytic14", + "type": "Microsoft.Common.Section", + "label": "Tailscale: Device Tailscale SSH newly enabled", + "elements": [ + { + "name": "analytic14-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies when Tailscale SSH is enabled on a device that previously did not have it. SSH provides authenticated shell access over the tailnet using Tailscale identity, broadening attack surface if unexpected. Verify and confirm the SSH ACL covers it." + } + } + ] + }, + { + "name": "analytic15", + "type": "Microsoft.Common.Section", + "label": "Tailscale: Unauthorized device connected to control plane", + "elements": [ + { + "name": "analytic15-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies devices actively connected to the Tailscale control plane (ConnectedToControl=true) but not yet authorized by an admin (Authorized=false). Often benign onboarding but can indicate rogue joins." + } + } + ] + }, + { + "name": "analytic16", + "type": "Microsoft.Common.Section", + "label": "Tailscale: External (shared-in) device added", + "elements": [ + { + "name": "analytic16-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "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." + } + } + ] + }, + { + "name": "analytic17", + "type": "Microsoft.Common.Section", + "label": "Tailscale Premium: Unexpected exit-node egress", + "elements": [ + { + "name": "analytic17-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies when a node sends traffic via an exit node not used in the prior 7-day baseline. First-seen exit destinations from a node may indicate routing-policy drift, data exfiltration, or compromise." + } + } + ] + }, + { + "name": "analytic18", + "type": "Microsoft.Common.Section", + "label": "Tailscale Premium: Large outbound transfer over tailnet", + "elements": [ + { + "name": "analytic18-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "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." + } + } + ] + }, + { + "name": "analytic19", + "type": "Microsoft.Common.Section", + "label": "Tailscale Premium: Network flow beaconing detected", + "elements": [ + { + "name": "analytic19-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies when flows between a src-dst pair recur at a regular interval (80%+ of inter-flow gaps cluster on the same delta over 10+ flows). Signature of C2 beaconing or scheduled exfiltration. Requires Tailscale Premium or Enterprise." + } + } + ] + }, + { + "name": "analytic20", + "type": "Microsoft.Common.Section", + "label": "Tailscale Premium: Mass fan-out from single node", + "elements": [ + { + "name": "analytic20-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies when a single node initiates flows to 25 or more unique destinations within a 15-minute window. Sudden fan-out is consistent with port scanning, lateral discovery, or worm-style propagation. Requires Tailscale Premium or Enterprise." + } + } + ] + }, + { + "name": "analytic21", + "type": "Microsoft.Common.Section", + "label": "Tailscale Premium: Subnet router throughput anomaly", + "elements": [ + { + "name": "analytic21-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies when a subnet router (gateway node bridging the tailnet to an on-prem or cloud subnet) handles 3x or more its 7-day baseline traffic in the last hour. Spikes can indicate exfiltration or scanning. Requires Tailscale Premium or Enterprise." + } + } + ] + }, + { + "name": "analytic22", + "type": "Microsoft.Common.Section", + "label": "Tailscale Premium: Posture integration disabled or removed", + "elements": [ + { + "name": "analytic22-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies when a device-posture integration is disabled or removed from the tailnet. Posture integrations enforce device compliance - removal weakens fleet posture and is a possible defense-evasion step." + } + } + ] + }, + { + "name": "analytic23", + "type": "Microsoft.Common.Section", + "label": "Tailscale Premium: New posture integration added", + "elements": [ + { + "name": "analytic23-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies when a new device-posture integration is added to the tailnet (Jamf, Kandji, Intune, Kolide, Defender for Endpoint, CrowdStrike, SentinelOne). Verify the addition was sanctioned." + } + } + ] + }, + { + "name": "analytic24", + "type": "Microsoft.Common.Section", + "label": "Tailscale Premium: DERP relay traffic surge", + "elements": [ + { + "name": "analytic24-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies when a source node has more than 75 percent of its recent flows falling back to a DERP relay (Tailscale IsRelayed flag, traffic via 127.3.3.40). Operational signal useful for spotting policy drift." + } + } + ] + } + ] + }, + { + "name": "huntingqueries", + "label": "Hunting Queries", + "bladeTitle": "Hunting Queries", + "elements": [ + { + "name": "huntingqueries-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "This solution installs the following hunting queries. After installing the solution, run these hunting queries to hunt for threats in Manage solution view. " + } + }, + { + "name": "huntingqueries-link", + "type": "Microsoft.Common.TextBlock", + "options": { + "link": { + "label": "Learn more", + "uri": "https://docs.microsoft.com/azure/sentinel/hunting" + } + } + }, + { + "name": "huntingquery1", + "type": "Microsoft.Common.Section", + "label": "Tailscale: First-seen actor making configuration changes", + "elements": [ + { + "name": "huntingquery1-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies actors making their first configuration change in the lookback window. New legitimate admins look identical to compromised credentials - review whether each surfaced actor is expected to have admin rights. This hunting query depends on TailscaleCCF data connector (Tailscale_Audit_CL Parser or Table)" + } + } + ] + }, + { + "name": "huntingquery2", + "type": "Microsoft.Common.Section", + "label": "Tailscale: ACL policy churn", + "elements": [ + { + "name": "huntingquery2-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies short windows where the tailnet ACL policy file was rewritten multiple times. Iterative policy edits during an incident can indicate a defender adjusting rules or an attacker probing. This hunting query depends on TailscaleCCF data connector (Tailscale_Audit_CL Parser or Table)" + } + } + ] + }, + { + "name": "huntingquery3", + "type": "Microsoft.Common.Section", + "label": "Tailscale: Off-hours configuration changes", + "elements": [ + { + "name": "huntingquery3-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies configuration audit events that occurred outside typical business hours (Monday-Friday 08:00-18:00 UTC). Useful for spotting impromptu maintenance, account compromise, or insider activity. This hunting query depends on TailscaleCCF data connector (Tailscale_Audit_CL Parser or Table)" + } + } + ] + }, + { + "name": "huntingquery4", + "type": "Microsoft.Common.Section", + "label": "Tailscale: Auth key sprawl", + "elements": [ + { + "name": "huntingquery4-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies actors creating multiple auth keys in a short window. A single admin creating many keys for unattended enrollment is normal during a rollout; same pattern can also indicate token-spraying. This hunting query depends on TailscaleCCF data connector (Tailscale_Audit_CL Parser or Table)" + } + } + ] + }, + { + "name": "huntingquery5", + "type": "Microsoft.Common.Section", + "label": "Tailscale: Devices not seen in 30+ days", + "elements": [ + { + "name": "huntingquery5-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies tailnet devices that have not connected to the control plane for at least 30 days. Dormant devices accumulate risk - they may still have valid keys, advertised routes, or tags. This hunting query depends on TailscaleCCF data connector (Tailscale_Devices_CL Parser or Table)" + } + } + ] + }, + { + "name": "huntingquery6", + "type": "Microsoft.Common.Section", + "label": "Tailscale: Auth keys with no expiry", + "elements": [ + { + "name": "huntingquery6-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies tailnet auth keys that have no expiry timestamp set. Non-expiring keys grant unattended enrollment in perpetuity and should be rotated or replaced with ephemeral, time-bounded keys. This hunting query depends on TailscaleCCF data connector (Tailscale_Keys_CL Parser or Table)" + } + } + ] + }, + { + "name": "huntingquery7", + "type": "Microsoft.Common.Section", + "label": "Tailscale: Users with zero devices", + "elements": [ + { + "name": "huntingquery7-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies tailnet users who have no devices currently registered. Orphaned identities are candidates for off-boarding - they retain whatever role/permissions they were granted. This hunting query depends on TailscaleCCF data connector (Tailscale_Users_CL Parser or Table)" + } + } + ] + }, + { + "name": "huntingquery8", + "type": "Microsoft.Common.Section", + "label": "Tailscale: Split-DNS per-domain change history", + "elements": [ + { + "name": "huntingquery8-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies the per-domain Split-DNS change history from the audit log. For each modification event, expands Old and New documents and surfaces which domains were added, removed, or changed. This hunting query depends on TailscaleCCF data connector (Tailscale_Audit_CL Parser or Table)" + } + } + ] + }, + { + "name": "huntingquery9", + "type": "Microsoft.Common.Section", + "label": "Tailscale: Devices with Tailscale SSH enabled", + "elements": [ + { + "name": "huntingquery9-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies devices that currently have Tailscale SSH enabled. Tailscale SSH delivers SSH access over the tailnet using Tailscale identity and is governed by the SSH ACL block in the policy file. This hunting query depends on TailscaleCCF data connector (Tailscale_Devices_CL Parser or Table)" + } + } + ] + }, + { + "name": "huntingquery10", + "type": "Microsoft.Common.Section", + "label": "Tailscale: External (shared-in) device inventory", + "elements": [ + { + "name": "huntingquery10-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies external (shared-in) devices currently active in the tailnet. These devices belong to another tailnet and have been admitted via a Tailscale sharing arrangement. This hunting query depends on TailscaleCCF data connector (Tailscale_Devices_CL Parser or Table)" + } + } + ] + }, + { + "name": "huntingquery11", + "type": "Microsoft.Common.Section", + "label": "Tailscale: Devices with outdated client version", + "elements": [ + { + "name": "huntingquery11-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies tailnet devices that report UpdateAvailable=true on the latest snapshot. Tailscale releases security updates regularly; outdated clients lack the most recent improvements. This hunting query depends on TailscaleCCF data connector (Tailscale_Devices_CL Parser or Table)" + } + } + ] + }, + { + "name": "huntingquery12", + "type": "Microsoft.Common.Section", + "label": "Tailscale: Subnet router CIDR exposure inventory", + "elements": [ + { + "name": "huntingquery12-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies every device currently advertising or enabling subnet routes (bridging non-tailnet networks into the tailnet). Excludes pure exit-node advertisements so only true subnet exposure is surfaced. This hunting query depends on TailscaleCCF data connector (Tailscale_Devices_CL Parser or Table)" + } + } + ] + }, + { + "name": "huntingquery13", + "type": "Microsoft.Common.Section", + "label": "Tailscale Premium: New src->dst node pairs (lateral movement candidates)", + "elements": [ + { + "name": "huntingquery13-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies tailnet src->dst pairs observed in the last 24h that were NOT observed in the prior 7-day baseline. Useful for spotting lateral movement to nodes that don't usually talk. This hunting query depends on TailscalePremiumCCF data connector (Tailscale_Network_CL Parser or Table)" + } + } + ] + }, + { + "name": "huntingquery14", + "type": "Microsoft.Common.Section", + "label": "Tailscale Premium: Top talkers by bytes (virtual traffic)", + "elements": [ + { + "name": "huntingquery14-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies tailnet src->dst pairs ranked by total bytes transferred over the last 24h. Useful for capacity planning, identifying data-heavy flows, and spotting unexpected volume that could indicate data staging. Requires Tailscale Premium or Enterprise. This hunting query depends on TailscalePremiumCCF data connector (Tailscale_Network_CL Parser or Table)" + } + } + ] + }, + { + "name": "huntingquery15", + "type": "Microsoft.Common.Section", + "label": "Tailscale Premium: Exit-node usage patterns", + "elements": [ + { + "name": "huntingquery15-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies traffic leaving the tailnet via exit nodes. Exit-node use is typically intentional (regional egress, privacy routing) but unexpected egress from a node warrants investigation. This hunting query depends on TailscalePremiumCCF data connector (Tailscale_Network_CL Parser or Table)" + } + } + ] + }, + { + "name": "huntingquery16", + "type": "Microsoft.Common.Section", + "label": "Tailscale Premium: Beaconing candidates (regular periodic flows)", + "elements": [ + { + "name": "huntingquery16-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies flows that recur at a highly regular interval, which is the signature of C2 beaconing or scheduled exfiltration jobs. Looser threshold than the analytic rule - investigation aid. This hunting query depends on TailscalePremiumCCF data connector (Tailscale_Network_CL Parser or Table)" + } + } + ] + }, + { + "name": "huntingquery17", + "type": "Microsoft.Common.Section", + "label": "Tailscale Premium: Current posture integration inventory", + "elements": [ + { + "name": "huntingquery17-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies the current set of device-posture integrations configured on the tailnet (latest snapshot per integration). Useful for compliance attestation and detecting drift from the expected baseline. This hunting query depends on TailscalePremiumCCF data connector (Tailscale_PostureIntegrations_CL Parser or Table)" + } + } + ] + }, + { + "name": "huntingquery18", + "type": "Microsoft.Common.Section", + "label": "Tailscale Premium: Devices with persistent DERP relay usage", + "elements": [ + { + "name": "huntingquery18-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies devices that have consistently fallen back to DERP relay (IsRelayed=true) over the past 24 hours. Sustained relay usage points to NAT/firewall misconfiguration or deliberate evasion. This hunting query depends on TailscalePremiumCCF data connector (Tailscale_Network_CL Parser or Table)" + } + } + ] + }, + { + "name": "huntingquery19", + "type": "Microsoft.Common.Section", + "label": "Tailscale Premium: Tagged services with broad inbound exposure", + "elements": [ + { + "name": "huntingquery19-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies tagged services (devices with non-empty DstTags) ranked by inbound diversity over 7 days. Surfaces services with the broadest blast-radius; ACL drift candidates. This hunting query depends on TailscalePremiumCCF data connector (Tailscale_Network_CL Parser or Table)" + } + } + ] + }, + { + "name": "huntingquery20", + "type": "Microsoft.Common.Section", + "label": "Tailscale Premium: Cross-tag flow matrix", + "elements": [ + { + "name": "huntingquery20-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies network flows pivoted by source-tag x destination-tag over 7 days. Highlights tag-to-tag traffic, useful for ACL validation. Same-tag loops can signal worm-style propagation. This hunting query depends on TailscalePremiumCCF data connector (Tailscale_Network_CL Parser or Table)" + } + } + ] + }, + { + "name": "huntingquery21", + "type": "Microsoft.Common.Section", + "label": "Tailscale Premium: Network flows outside business hours", + "elements": [ + { + "name": "huntingquery21-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies network flows occurring outside 07:00-19:00 UTC on weekdays plus all weekend, over 7 days. Filters to virtual/subnet/exit traffic. Useful for spotting unattended automation gone wrong. This hunting query depends on TailscalePremiumCCF data connector (Tailscale_Network_CL Parser or Table)" + } + } + ] + }, + { + "name": "huntingquery22", + "type": "Microsoft.Common.Section", + "label": "Tailscale Premium: Users generating traffic from multiple devices", + "elements": [ + { + "name": "huntingquery22-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Identifies users (SrcUser) generating tailnet flows from more than one distinct device in the past 24 hours. Useful for spotting account compromise (sudden new device) or unauthorised device enrollment. This hunting query depends on TailscalePremiumCCF data connector (Tailscale_Network_CL Tailscale_Devices_CL Parser or Table)" + } + } + ] + } + ] + } + ], + "outputs": { + "workspace-location": "[first(map(filter(basics('getLAWorkspace').value, (filter) => and(contains(toLower(filter.id), toLower(resourceGroup().name)),equals(filter.name,basics('workspace')))), (item) => item.location))]", + "location": "[location()]", + "workspace": "[basics('workspace')]" + } + } +} diff --git a/Solutions/Tailscale (CCF)/Package/mainTemplate.json b/Solutions/Tailscale (CCF)/Package/mainTemplate.json new file mode 100644 index 00000000000..5f88745db6f --- /dev/null +++ b/Solutions/Tailscale (CCF)/Package/mainTemplate.json @@ -0,0 +1,10191 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "author": "noodlemctwoodle - ccfconnectors.county118@passmail.com", + "comments": "Solution template for Tailscale (CCF)" + }, + "parameters": { + "location": { + "type": "string", + "minLength": 1, + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Not used, but needed to pass arm-ttk test `Location-Should-Not-Be-Hardcoded`. We instead use the `workspace-location` which is derived from the LA workspace" + } + }, + "workspace-location": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "[concat('Region to deploy solution resources -- separate from location selection',parameters('location'))]" + } + }, + "workspace": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "Workspace name for Log Analytics where Microsoft Sentinel is setup" + } + }, + "resourceGroupName": { + "type": "string", + "defaultValue": "[resourceGroup().name]", + "metadata": { + "description": "resource group name where Microsoft Sentinel is setup" + } + }, + "subscription": { + "type": "string", + "defaultValue": "[last(split(subscription().id, '/'))]", + "metadata": { + "description": "subscription id where Microsoft Sentinel is setup" + } + }, + "workbook1-name": { + "type": "string", + "defaultValue": "Tailscale Operations (Standard)", + "minLength": 1, + "metadata": { + "description": "Name for the workbook" + } + }, + "workbook2-name": { + "type": "string", + "defaultValue": "Tailscale Operations (Premium)", + "minLength": 1, + "metadata": { + "description": "Name for the workbook" + } + } + }, + "variables": { + "email": "ccfconnectors.county118@passmail.com", + "_email": "[variables('email')]", + "_solutionName": "Tailscale (CCF)", + "_solutionVersion": "3.0.0", + "solutionId": "noodlemctwoodle.azure-sentinel-solution-tailscale-ccf", + "_solutionId": "[variables('solutionId')]", + "workspaceResourceId": "[resourceId('microsoft.OperationalInsights/Workspaces', parameters('workspace'))]", + "dataConnectorCCPVersion": "3.0.0", + "_dataConnectorContentIdConnectorDefinition1": "TailscaleCCF", + "dataConnectorTemplateNameConnectorDefinition1": "[concat(parameters('workspace'),'-dc-',uniquestring(variables('_dataConnectorContentIdConnectorDefinition1')))]", + "_dataConnectorContentIdConnections1": "TailscaleCCFConnections", + "dataConnectorTemplateNameConnections1": "[concat(parameters('workspace'),'-dc-',uniquestring(variables('_dataConnectorContentIdConnections1')))]", + "dataCollectionEndpointId1": "[concat('/subscriptions/',parameters('subscription'),'/resourceGroups/',parameters('resourceGroupName'),'/providers/Microsoft.Insights/dataCollectionEndpoints/',parameters('workspace'))]", + "_dataConnectorContentIdConnectorDefinition2": "TailscalePremiumCCF", + "dataConnectorTemplateNameConnectorDefinition2": "[concat(parameters('workspace'),'-dc-',uniquestring(variables('_dataConnectorContentIdConnectorDefinition2')))]", + "_dataConnectorContentIdConnections2": "TailscalePremiumCCFConnections", + "dataConnectorTemplateNameConnections2": "[concat(parameters('workspace'),'-dc-',uniquestring(variables('_dataConnectorContentIdConnections2')))]", + "dataCollectionEndpointId2": "[concat('/subscriptions/',parameters('subscription'),'/resourceGroups/',parameters('resourceGroupName'),'/providers/Microsoft.Insights/dataCollectionEndpoints/',parameters('workspace'))]", + "analyticRuleObject1": { + "analyticRuleVersion1": "1.0.0", + "_analyticRulecontentId1": "668b43fd-cf28-961a-85af-957850df5027", + "analyticRuleId1": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', '668b43fd-cf28-961a-85af-957850df5027')]", + "analyticRuleTemplateSpecName1": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('668b43fd-cf28-961a-85af-957850df5027')))]", + "_analyticRulecontentProductId1": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','668b43fd-cf28-961a-85af-957850df5027','-', '1.0.0')))]" + }, + "analyticRuleObject2": { + "analyticRuleVersion2": "1.0.0", + "_analyticRulecontentId2": "7237a848-30f2-499b-9ad5-024aea1288bd", + "analyticRuleId2": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', '7237a848-30f2-499b-9ad5-024aea1288bd')]", + "analyticRuleTemplateSpecName2": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('7237a848-30f2-499b-9ad5-024aea1288bd')))]", + "_analyticRulecontentProductId2": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','7237a848-30f2-499b-9ad5-024aea1288bd','-', '1.0.0')))]" + }, + "analyticRuleObject3": { + "analyticRuleVersion3": "1.0.0", + "_analyticRulecontentId3": "1e7249c2-1a9d-05fd-45cb-c859eef5b8ae", + "analyticRuleId3": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', '1e7249c2-1a9d-05fd-45cb-c859eef5b8ae')]", + "analyticRuleTemplateSpecName3": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('1e7249c2-1a9d-05fd-45cb-c859eef5b8ae')))]", + "_analyticRulecontentProductId3": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','1e7249c2-1a9d-05fd-45cb-c859eef5b8ae','-', '1.0.0')))]" + }, + "analyticRuleObject4": { + "analyticRuleVersion4": "1.0.0", + "_analyticRulecontentId4": "6b052c8d-5de8-eab0-1956-69a297765a32", + "analyticRuleId4": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', '6b052c8d-5de8-eab0-1956-69a297765a32')]", + "analyticRuleTemplateSpecName4": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('6b052c8d-5de8-eab0-1956-69a297765a32')))]", + "_analyticRulecontentProductId4": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','6b052c8d-5de8-eab0-1956-69a297765a32','-', '1.0.0')))]" + }, + "analyticRuleObject5": { + "analyticRuleVersion5": "1.0.0", + "_analyticRulecontentId5": "f42f2906-c8e6-23d0-e48c-0620e50d5510", + "analyticRuleId5": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', 'f42f2906-c8e6-23d0-e48c-0620e50d5510')]", + "analyticRuleTemplateSpecName5": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('f42f2906-c8e6-23d0-e48c-0620e50d5510')))]", + "_analyticRulecontentProductId5": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','f42f2906-c8e6-23d0-e48c-0620e50d5510','-', '1.0.0')))]" + }, + "analyticRuleObject6": { + "analyticRuleVersion6": "1.0.0", + "_analyticRulecontentId6": "f817e2fa-6fa0-fc25-5369-cef9b58771af", + "analyticRuleId6": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', 'f817e2fa-6fa0-fc25-5369-cef9b58771af')]", + "analyticRuleTemplateSpecName6": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('f817e2fa-6fa0-fc25-5369-cef9b58771af')))]", + "_analyticRulecontentProductId6": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','f817e2fa-6fa0-fc25-5369-cef9b58771af','-', '1.0.0')))]" + }, + "analyticRuleObject7": { + "analyticRuleVersion7": "1.0.0", + "_analyticRulecontentId7": "b1a2c3d4-1234-5678-90ab-cdef12345001", + "analyticRuleId7": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', 'b1a2c3d4-1234-5678-90ab-cdef12345001')]", + "analyticRuleTemplateSpecName7": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('b1a2c3d4-1234-5678-90ab-cdef12345001')))]", + "_analyticRulecontentProductId7": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','b1a2c3d4-1234-5678-90ab-cdef12345001','-', '1.0.0')))]" + }, + "analyticRuleObject8": { + "analyticRuleVersion8": "1.0.0", + "_analyticRulecontentId8": "c2b3d4e5-2345-6789-01ab-cdef12345002", + "analyticRuleId8": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', 'c2b3d4e5-2345-6789-01ab-cdef12345002')]", + "analyticRuleTemplateSpecName8": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('c2b3d4e5-2345-6789-01ab-cdef12345002')))]", + "_analyticRulecontentProductId8": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','c2b3d4e5-2345-6789-01ab-cdef12345002','-', '1.0.0')))]" + }, + "analyticRuleObject9": { + "analyticRuleVersion9": "1.0.0", + "_analyticRulecontentId9": "d3c4e5f6-3456-7890-12ab-cdef12345003", + "analyticRuleId9": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', 'd3c4e5f6-3456-7890-12ab-cdef12345003')]", + "analyticRuleTemplateSpecName9": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('d3c4e5f6-3456-7890-12ab-cdef12345003')))]", + "_analyticRulecontentProductId9": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','d3c4e5f6-3456-7890-12ab-cdef12345003','-', '1.0.0')))]" + }, + "analyticRuleObject10": { + "analyticRuleVersion10": "1.0.0", + "_analyticRulecontentId10": "b4c5d6e7-1234-5678-90ab-cdef12345010", + "analyticRuleId10": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', 'b4c5d6e7-1234-5678-90ab-cdef12345010')]", + "analyticRuleTemplateSpecName10": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('b4c5d6e7-1234-5678-90ab-cdef12345010')))]", + "_analyticRulecontentProductId10": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','b4c5d6e7-1234-5678-90ab-cdef12345010','-', '1.0.0')))]" + }, + "analyticRuleObject11": { + "analyticRuleVersion11": "1.0.0", + "_analyticRulecontentId11": "c5d6e7f8-2345-6789-01ab-cdef12345011", + "analyticRuleId11": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', 'c5d6e7f8-2345-6789-01ab-cdef12345011')]", + "analyticRuleTemplateSpecName11": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('c5d6e7f8-2345-6789-01ab-cdef12345011')))]", + "_analyticRulecontentProductId11": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','c5d6e7f8-2345-6789-01ab-cdef12345011','-', '1.0.0')))]" + }, + "analyticRuleObject12": { + "analyticRuleVersion12": "1.0.0", + "_analyticRulecontentId12": "f8a9b0c1-4567-8901-23ab-cdef12345020", + "analyticRuleId12": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', 'f8a9b0c1-4567-8901-23ab-cdef12345020')]", + "analyticRuleTemplateSpecName12": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('f8a9b0c1-4567-8901-23ab-cdef12345020')))]", + "_analyticRulecontentProductId12": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','f8a9b0c1-4567-8901-23ab-cdef12345020','-', '1.0.0')))]" + }, + "analyticRuleObject13": { + "analyticRuleVersion13": "1.0.0", + "_analyticRulecontentId13": "e9f0a1b2-3456-7890-12cd-ef1234560040", + "analyticRuleId13": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', 'e9f0a1b2-3456-7890-12cd-ef1234560040')]", + "analyticRuleTemplateSpecName13": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('e9f0a1b2-3456-7890-12cd-ef1234560040')))]", + "_analyticRulecontentProductId13": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','e9f0a1b2-3456-7890-12cd-ef1234560040','-', '1.0.0')))]" + }, + "analyticRuleObject14": { + "analyticRuleVersion14": "1.0.0", + "_analyticRulecontentId14": "f0a1b2c3-4567-8901-23de-f12345670041", + "analyticRuleId14": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', 'f0a1b2c3-4567-8901-23de-f12345670041')]", + "analyticRuleTemplateSpecName14": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('f0a1b2c3-4567-8901-23de-f12345670041')))]", + "_analyticRulecontentProductId14": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','f0a1b2c3-4567-8901-23de-f12345670041','-', '1.0.0')))]" + }, + "analyticRuleObject15": { + "analyticRuleVersion15": "1.0.0", + "_analyticRulecontentId15": "a1b2c3d4-5678-9012-3456-789012340042", + "analyticRuleId15": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', 'a1b2c3d4-5678-9012-3456-789012340042')]", + "analyticRuleTemplateSpecName15": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('a1b2c3d4-5678-9012-3456-789012340042')))]", + "_analyticRulecontentProductId15": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','a1b2c3d4-5678-9012-3456-789012340042','-', '1.0.0')))]" + }, + "analyticRuleObject16": { + "analyticRuleVersion16": "1.0.0", + "_analyticRulecontentId16": "b2c3d4e5-6789-0123-4567-890123450043", + "analyticRuleId16": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', 'b2c3d4e5-6789-0123-4567-890123450043')]", + "analyticRuleTemplateSpecName16": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('b2c3d4e5-6789-0123-4567-890123450043')))]", + "_analyticRulecontentProductId16": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','b2c3d4e5-6789-0123-4567-890123450043','-', '1.0.0')))]" + }, + "analyticRuleObject17": { + "analyticRuleVersion17": "1.0.0", + "_analyticRulecontentId17": "c1d2e3f4-1a2b-3c4d-5e6f-7a8b9c0d1e2f", + "analyticRuleId17": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', 'c1d2e3f4-1a2b-3c4d-5e6f-7a8b9c0d1e2f')]", + "analyticRuleTemplateSpecName17": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('c1d2e3f4-1a2b-3c4d-5e6f-7a8b9c0d1e2f')))]", + "_analyticRulecontentProductId17": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','c1d2e3f4-1a2b-3c4d-5e6f-7a8b9c0d1e2f','-', '1.0.0')))]" + }, + "analyticRuleObject18": { + "analyticRuleVersion18": "1.0.0", + "_analyticRulecontentId18": "d2e3f4a5-2b3c-4d5e-6f7a-8b9c0d1e2f3a", + "analyticRuleId18": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', 'd2e3f4a5-2b3c-4d5e-6f7a-8b9c0d1e2f3a')]", + "analyticRuleTemplateSpecName18": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('d2e3f4a5-2b3c-4d5e-6f7a-8b9c0d1e2f3a')))]", + "_analyticRulecontentProductId18": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','d2e3f4a5-2b3c-4d5e-6f7a-8b9c0d1e2f3a','-', '1.0.0')))]" + }, + "analyticRuleObject19": { + "analyticRuleVersion19": "1.0.0", + "_analyticRulecontentId19": "e3f4a5b6-3c4d-5e6f-7a8b-9c0d1e2f3a4b", + "analyticRuleId19": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', 'e3f4a5b6-3c4d-5e6f-7a8b-9c0d1e2f3a4b')]", + "analyticRuleTemplateSpecName19": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('e3f4a5b6-3c4d-5e6f-7a8b-9c0d1e2f3a4b')))]", + "_analyticRulecontentProductId19": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','e3f4a5b6-3c4d-5e6f-7a8b-9c0d1e2f3a4b','-', '1.0.0')))]" + }, + "analyticRuleObject20": { + "analyticRuleVersion20": "1.0.0", + "_analyticRulecontentId20": "f4a5b6c7-4d5e-6f7a-8b9c-0d1e2f3a4b5c", + "analyticRuleId20": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', 'f4a5b6c7-4d5e-6f7a-8b9c-0d1e2f3a4b5c')]", + "analyticRuleTemplateSpecName20": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('f4a5b6c7-4d5e-6f7a-8b9c-0d1e2f3a4b5c')))]", + "_analyticRulecontentProductId20": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','f4a5b6c7-4d5e-6f7a-8b9c-0d1e2f3a4b5c','-', '1.0.0')))]" + }, + "analyticRuleObject21": { + "analyticRuleVersion21": "1.0.0", + "_analyticRulecontentId21": "a5b6c7d8-5e6f-7a8b-9c0d-1e2f3a4b5c6d", + "analyticRuleId21": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', 'a5b6c7d8-5e6f-7a8b-9c0d-1e2f3a4b5c6d')]", + "analyticRuleTemplateSpecName21": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('a5b6c7d8-5e6f-7a8b-9c0d-1e2f3a4b5c6d')))]", + "_analyticRulecontentProductId21": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','a5b6c7d8-5e6f-7a8b-9c0d-1e2f3a4b5c6d','-', '1.0.0')))]" + }, + "analyticRuleObject22": { + "analyticRuleVersion22": "1.0.0", + "_analyticRulecontentId22": "a1b2c3d4-5678-9012-34ab-cdef12345030", + "analyticRuleId22": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', 'a1b2c3d4-5678-9012-34ab-cdef12345030')]", + "analyticRuleTemplateSpecName22": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('a1b2c3d4-5678-9012-34ab-cdef12345030')))]", + "_analyticRulecontentProductId22": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','a1b2c3d4-5678-9012-34ab-cdef12345030','-', '1.0.0')))]" + }, + "analyticRuleObject23": { + "analyticRuleVersion23": "1.0.0", + "_analyticRulecontentId23": "b2c3d4e5-6789-0123-45ab-cdef12345031", + "analyticRuleId23": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', 'b2c3d4e5-6789-0123-45ab-cdef12345031')]", + "analyticRuleTemplateSpecName23": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('b2c3d4e5-6789-0123-45ab-cdef12345031')))]", + "_analyticRulecontentProductId23": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','b2c3d4e5-6789-0123-45ab-cdef12345031','-', '1.0.0')))]" + }, + "analyticRuleObject24": { + "analyticRuleVersion24": "1.0.0", + "_analyticRulecontentId24": "0a1c8d12-e7d3-4890-8b89-8d6dbc1be2f0", + "analyticRuleId24": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', '0a1c8d12-e7d3-4890-8b89-8d6dbc1be2f0')]", + "analyticRuleTemplateSpecName24": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('0a1c8d12-e7d3-4890-8b89-8d6dbc1be2f0')))]", + "_analyticRulecontentProductId24": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','0a1c8d12-e7d3-4890-8b89-8d6dbc1be2f0','-', '1.0.0')))]" + }, + "huntingQueryObject1": { + "huntingQueryVersion1": "1.0.0", + "_huntingQuerycontentId1": "a91f4d3c-1b7e-4f2a-9c1d-0e3b5f7c8d9a", + "huntingQueryTemplateSpecName1": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-hq-',uniquestring('a91f4d3c-1b7e-4f2a-9c1d-0e3b5f7c8d9a')))]" + }, + "huntingQueryObject2": { + "huntingQueryVersion2": "1.0.0", + "_huntingQuerycontentId2": "b82e5d4d-2c8f-5e3b-ad2e-1f4c6e8d9eab", + "huntingQueryTemplateSpecName2": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-hq-',uniquestring('b82e5d4d-2c8f-5e3b-ad2e-1f4c6e8d9eab')))]" + }, + "huntingQueryObject3": { + "huntingQueryVersion3": "1.0.0", + "_huntingQuerycontentId3": "c73f6e5e-3d9a-6f4c-be3f-2a5d7f9eafbc", + "huntingQueryTemplateSpecName3": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-hq-',uniquestring('c73f6e5e-3d9a-6f4c-be3f-2a5d7f9eafbc')))]" + }, + "huntingQueryObject4": { + "huntingQueryVersion4": "1.0.0", + "_huntingQuerycontentId4": "d64e7f6f-4eab-7a5d-cf4a-3b6e8aafbcad", + "huntingQueryTemplateSpecName4": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-hq-',uniquestring('d64e7f6f-4eab-7a5d-cf4a-3b6e8aafbcad')))]" + }, + "huntingQueryObject5": { + "huntingQueryVersion5": "1.0.0", + "_huntingQuerycontentId5": "e4d5f6a7-4567-8901-23ab-cdef12345004", + "huntingQueryTemplateSpecName5": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-hq-',uniquestring('e4d5f6a7-4567-8901-23ab-cdef12345004')))]" + }, + "huntingQueryObject6": { + "huntingQueryVersion6": "1.0.0", + "_huntingQuerycontentId6": "f5e6a7b8-5678-9012-34ab-cdef12345005", + "huntingQueryTemplateSpecName6": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-hq-',uniquestring('f5e6a7b8-5678-9012-34ab-cdef12345005')))]" + }, + "huntingQueryObject7": { + "huntingQueryVersion7": "1.0.0", + "_huntingQuerycontentId7": "a6f7b8c9-6789-0123-45ab-cdef12345006", + "huntingQueryTemplateSpecName7": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-hq-',uniquestring('a6f7b8c9-6789-0123-45ab-cdef12345006')))]" + }, + "huntingQueryObject8": { + "huntingQueryVersion8": "1.0.0", + "_huntingQuerycontentId8": "e7f8a9b0-3456-7890-12ab-cdef12345012", + "huntingQueryTemplateSpecName8": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-hq-',uniquestring('e7f8a9b0-3456-7890-12ab-cdef12345012')))]" + }, + "huntingQueryObject9": { + "huntingQueryVersion9": "1.0.0", + "_huntingQuerycontentId9": "c3d4e5f6-7890-1234-5678-901234560044", + "huntingQueryTemplateSpecName9": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-hq-',uniquestring('c3d4e5f6-7890-1234-5678-901234560044')))]" + }, + "huntingQueryObject10": { + "huntingQueryVersion10": "1.0.0", + "_huntingQuerycontentId10": "d4e5f6a7-8901-2345-6789-012345670045", + "huntingQueryTemplateSpecName10": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-hq-',uniquestring('d4e5f6a7-8901-2345-6789-012345670045')))]" + }, + "huntingQueryObject11": { + "huntingQueryVersion11": "1.0.0", + "_huntingQuerycontentId11": "e5f6a7b8-9012-3456-7890-123456780046", + "huntingQueryTemplateSpecName11": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-hq-',uniquestring('e5f6a7b8-9012-3456-7890-123456780046')))]" + }, + "huntingQueryObject12": { + "huntingQueryVersion12": "1.0.0", + "_huntingQuerycontentId12": "f6a7b8c9-0123-4567-8901-234567890047", + "huntingQueryTemplateSpecName12": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-hq-',uniquestring('f6a7b8c9-0123-4567-8901-234567890047')))]" + }, + "huntingQueryObject13": { + "huntingQueryVersion13": "1.0.0", + "_huntingQuerycontentId13": "e55f8aaf-5fbc-8b6e-d05b-4c7faabcadbe", + "huntingQueryTemplateSpecName13": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-hq-',uniquestring('e55f8aaf-5fbc-8b6e-d05b-4c7faabcadbe')))]" + }, + "huntingQueryObject14": { + "huntingQueryVersion14": "1.0.0", + "_huntingQuerycontentId14": "f46a9bb0-6acd-9c7f-e16c-5d8abbcdbeca", + "huntingQueryTemplateSpecName14": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-hq-',uniquestring('f46a9bb0-6acd-9c7f-e16c-5d8abbcdbeca')))]" + }, + "huntingQueryObject15": { + "huntingQueryVersion15": "1.0.0", + "_huntingQuerycontentId15": "a37bacc1-7bde-ad8a-f27d-6e9bcdcecadb", + "huntingQueryTemplateSpecName15": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-hq-',uniquestring('a37bacc1-7bde-ad8a-f27d-6e9bcdcecadb')))]" + }, + "huntingQueryObject16": { + "huntingQueryVersion16": "1.0.0", + "_huntingQuerycontentId16": "b28cbdd2-8cef-be9b-a38e-7faceedfdbec", + "huntingQueryTemplateSpecName16": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-hq-',uniquestring('b28cbdd2-8cef-be9b-a38e-7faceedfdbec')))]" + }, + "huntingQueryObject17": { + "huntingQueryVersion17": "1.0.0", + "_huntingQuerycontentId17": "c3d4e5f6-7890-1234-56ab-cdef12345032", + "huntingQueryTemplateSpecName17": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-hq-',uniquestring('c3d4e5f6-7890-1234-56ab-cdef12345032')))]" + }, + "huntingQueryObject18": { + "huntingQueryVersion18": "1.0.0", + "_huntingQuerycontentId18": "20457fba-08e2-42d7-b972-fbe9acf583c8", + "huntingQueryTemplateSpecName18": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-hq-',uniquestring('20457fba-08e2-42d7-b972-fbe9acf583c8')))]" + }, + "huntingQueryObject19": { + "huntingQueryVersion19": "1.0.0", + "_huntingQuerycontentId19": "f8d4e7bc-3450-4c55-84ac-90e6e9c6b8fe", + "huntingQueryTemplateSpecName19": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-hq-',uniquestring('f8d4e7bc-3450-4c55-84ac-90e6e9c6b8fe')))]" + }, + "huntingQueryObject20": { + "huntingQueryVersion20": "1.0.0", + "_huntingQuerycontentId20": "a8978f27-3c85-4c29-a45a-c4a5e43fef2d", + "huntingQueryTemplateSpecName20": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-hq-',uniquestring('a8978f27-3c85-4c29-a45a-c4a5e43fef2d')))]" + }, + "huntingQueryObject21": { + "huntingQueryVersion21": "1.0.0", + "_huntingQuerycontentId21": "622ce88a-0838-4bbe-8a00-ab8ac8377f41", + "huntingQueryTemplateSpecName21": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-hq-',uniquestring('622ce88a-0838-4bbe-8a00-ab8ac8377f41')))]" + }, + "huntingQueryObject22": { + "huntingQueryVersion22": "1.0.0", + "_huntingQuerycontentId22": "daac10bd-d842-4122-90cc-9957256f04e3", + "huntingQueryTemplateSpecName22": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-hq-',uniquestring('daac10bd-d842-4122-90cc-9957256f04e3')))]" + }, + "workbookVersion1": "1.0.0", + "workbookContentId1": "TailscaleStandardOperationsWorkbook", + "workbookId1": "[resourceId('Microsoft.Insights/workbooks', variables('workbookContentId1'))]", + "workbookTemplateSpecName1": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-wb-',uniquestring(variables('_workbookContentId1'))))]", + "_workbookContentId1": "[variables('workbookContentId1')]", + "_workbookcontentProductId1": "[concat(take(variables('_solutionId'),50),'-','wb','-', uniqueString(concat(variables('_solutionId'),'-','Workbook','-',variables('_workbookContentId1'),'-', variables('workbookVersion1'))))]", + "workbookVersion2": "1.0.0", + "workbookContentId2": "TailscalePremiumOperationsWorkbook", + "workbookId2": "[resourceId('Microsoft.Insights/workbooks', variables('workbookContentId2'))]", + "workbookTemplateSpecName2": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-wb-',uniquestring(variables('_workbookContentId2'))))]", + "_workbookContentId2": "[variables('workbookContentId2')]", + "_workbookcontentProductId2": "[concat(take(variables('_solutionId'),50),'-','wb','-', uniqueString(concat(variables('_solutionId'),'-','Workbook','-',variables('_workbookContentId2'),'-', variables('workbookVersion2'))))]", + "parserObject1": { + "_parserName1": "[concat(parameters('workspace'),'/','vimNetworkSessionTailscale')]", + "_parserId1": "[resourceId('Microsoft.OperationalInsights/workspaces/savedSearches', parameters('workspace'), 'vimNetworkSessionTailscale')]", + "parserTemplateSpecName1": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-pr-',uniquestring('vimNetworkSessionTailscale-Parser')))]", + "parserVersion1": "1.0.0", + "parserContentId1": "vimNetworkSessionTailscale-Parser" + }, + "parserObject2": { + "_parserName2": "[concat(parameters('workspace'),'/','ASimNetworkSessionTailscale')]", + "_parserId2": "[resourceId('Microsoft.OperationalInsights/workspaces/savedSearches', parameters('workspace'), 'ASimNetworkSessionTailscale')]", + "parserTemplateSpecName2": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-pr-',uniquestring('ASimNetworkSessionTailscale-Parser')))]", + "parserVersion2": "1.0.0", + "parserContentId2": "ASimNetworkSessionTailscale-Parser" + }, + "_solutioncontentProductId": "[concat(take(variables('_solutionId'),50),'-','sl','-', uniqueString(concat(variables('_solutionId'),'-','Solution','-',variables('_solutionId'),'-', variables('_solutionVersion'))))]" + }, + "resources": [ + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/', variables('dataConnectorTemplateNameConnectorDefinition1'), variables('dataConnectorCCPVersion'))]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "contentId": "[variables('_dataConnectorContentIdConnectorDefinition1')]", + "displayName": "Tailscale Standard (CCF)", + "contentKind": "DataConnector", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('dataConnectorCCPVersion')]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',variables('_dataConnectorContentIdConnectorDefinition1'))]", + "apiVersion": "2022-09-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectorDefinitions", + "location": "[parameters('workspace-location')]", + "kind": "Customizable", + "properties": { + "connectorUiConfig": { + "title": "Tailscale Standard (CCF)", + "publisher": "Community", + "logo": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIGlkPSJMYXllcl8xIiB4PSIwIiB5PSIwIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48c3R5bGU+LnN0MHtvcGFjaXR5Oi4yO2VuYWJsZS1iYWNrZ3JvdW5kOm5ld308L3N0eWxlPjxwYXRoIGQ9Ik02NS42IDEyNy43YzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45UzEwMC45IDAgNjUuNiAwIDEuOCAyOC42IDEuOCA2My45czI4LjYgNjMuOCA2My44IDYzLjgiIGNsYXNzPSJzdDAiLz48cGF0aCBkPSJNNjUuNiAzMTguMWMzNS4zIDAgNjMuOS0yOC42IDYzLjktNjMuOXMtMjguNi02My45LTYzLjktNjMuOVMxLjggMjE5IDEuOCAyNTQuMnMyOC42IDYzLjkgNjMuOCA2My45Ii8+PHBhdGggZD0iTTY1LjYgNTEyYzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45cy0yOC42LTYzLjktNjMuOS02My45LTYzLjggMjguNy02My44IDYzLjlTMzAuNCA1MTIgNjUuNiA1MTIiIGNsYXNzPSJzdDAiLz48cGF0aCBkPSJNMjU3LjIgMzE4LjFjMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlzLTI4LjYtNjMuOS02My45LTYzLjktNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjkgNjMuOSA2My45bTAgMTkzLjljMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlzLTI4LjYtNjMuOS02My45LTYzLjktNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjkgNjMuOSA2My45Ii8+PHBhdGggZD0iTTI1Ny4yIDEyNy43YzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45UzI5Mi41IDAgMjU3LjIgMHMtNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjggNjMuOSA2My44bTE4OS4yIDBjMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlTNDgxLjYgMCA0NDYuNCAwYy0zNS4zIDAtNjMuOSAyOC42LTYzLjkgNjMuOXMyOC42IDYzLjggNjMuOSA2My44IiBjbGFzcz0ic3QwIi8+PHBhdGggZD0iTTQ0Ni40IDMxOC4xYzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45cy0yOC42LTYzLjktNjMuOS02My45LTYzLjkgMjguNi02My45IDYzLjkgMjguNiA2My45IDYzLjkgNjMuOSIvPjxwYXRoIGQ9Ik00NDYuNCA1MTJjMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlzLTI4LjYtNjMuOS02My45LTYzLjktNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjkgNjMuOSA2My45IiBjbGFzcz0ic3QwIi8+PC9zdmc+", + "descriptionMarkdown": "Comprehensive Tailscale telemetry for **Personal (Free) and Standard** tier tailnets. Polls nine endpoints in one Connect:\n\n- `/logging/configuration` - configuration audit events (includes ACL, DNS, tag/group, settings changes)\n- `/devices` - device inventory (hostname, OS, IPs, tags, lastSeen, expiry)\n- `/users` - user inventory (role, status, deviceCount, connection state)\n- `/keys?all=true` - auth keys, API tokens, and OAuth client metadata\n- `/webhooks` - webhook configuration\n- `/dns/nameservers`, `/dns/preferences`, `/dns/searchpaths` - DNS state (merged into single `Tailscale_Dns_CL` table with `ConfigType` discriminator)\n- `/settings` - tailnet settings flags (device approval, key duration, etc.)\n\nSplit-DNS state (per-domain DNS overrides) is captured via the audit log rather than a separate snapshot table - every change is recorded with the full before/after document and actor attribution, which is richer than a periodic snapshot.\n\n**OAuth scopes required on the Tailscale client:** `logs:configuration:read`, `devices:core:read`, `users:read`, `auth_keys:read`, `webhooks:read`, `dns:read`, `feature_settings:read` (or the bundled `all:read`). For Premium and Enterprise tailnets that also need network flow logs and posture integrations, install **Tailscale Premium (CCF)** instead.", + "graphQueries": [ + { + "metricName": "Audit events", + "legend": "Tailscale_Audit_CL", + "baseQuery": "Tailscale_Audit_CL" + }, + { + "metricName": "Device snapshots", + "legend": "Tailscale_Devices_CL", + "baseQuery": "Tailscale_Devices_CL" + }, + { + "metricName": "User snapshots", + "legend": "Tailscale_Users_CL", + "baseQuery": "Tailscale_Users_CL" + }, + { + "metricName": "Auth keys", + "legend": "Tailscale_Keys_CL", + "baseQuery": "Tailscale_Keys_CL" + }, + { + "metricName": "Webhooks", + "legend": "Tailscale_Webhooks_CL", + "baseQuery": "Tailscale_Webhooks_CL" + }, + { + "metricName": "Tailnet settings", + "legend": "Tailscale_Settings_CL", + "baseQuery": "Tailscale_Settings_CL" + }, + { + "metricName": "DNS config snapshots", + "legend": "Tailscale_Dns_CL", + "baseQuery": "Tailscale_Dns_CL" + } + ], + "dataTypes": [ + { + "name": "Tailscale_Audit_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Audit_CL')]" + }, + { + "name": "Tailscale_Devices_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Devices_CL')]" + }, + { + "name": "Tailscale_Users_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Users_CL')]" + }, + { + "name": "Tailscale_Keys_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Keys_CL')]" + }, + { + "name": "Tailscale_Webhooks_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Webhooks_CL')]" + }, + { + "name": "Tailscale_Settings_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Settings_CL')]" + }, + { + "name": "Tailscale_Dns_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Dns_CL')]" + } + ], + "sampleQueries": [ + { + "description": "Recent audit events", + "query": "Tailscale_Audit_CL\n| sort by TimeGenerated desc\n| take 100" + }, + { + "description": "Current device inventory (latest snapshot per device)", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| project DeviceName, User, Os, ClientVersion, LastSeen, Expires, Tags" + }, + { + "description": "Users by role", + "query": "Tailscale_Users_CL\n| summarize arg_max(TimeGenerated, *) by UserId\n| summarize Users = count() by Role" + }, + { + "description": "Auth keys without an expiry", + "query": "Tailscale_Keys_CL\n| summarize arg_max(TimeGenerated, *) by KeyId\n| where isnull(Expires) or Expires == datetime(null)" + }, + { + "description": "Tailnet device-approval status", + "query": "Tailscale_Settings_CL\n| sort by TimeGenerated desc\n| take 1\n| project DevicesApprovalOn, DevicesAutoUpdatesOn, UsersApprovalOn" + }, + { + "description": "Current DNS state (all DNS config in one query)", + "query": "Tailscale_Dns_CL\n| summarize arg_max(TimeGenerated, *) by ConfigType\n| project ConfigType, Nameservers, MagicDNS, SearchPaths" + } + ], + "connectivityCriteria": [ + { + "type": "HasDataConnectors" + } + ], + "availability": { + "status": 1, + "isPreview": true + }, + "permissions": { + "resourceProvider": [ + { + "provider": "Microsoft.OperationalInsights/workspaces", + "permissionsDisplayText": "Read/Write on the workspace", + "providerDisplayName": "Workspace", + "scope": "Workspace", + "requiredPermissions": { + "write": true, + "read": true, + "delete": true + } + } + ] + }, + "instructionSteps": [ + { + "title": "Connect Tailscale", + "description": "Generate an OAuth client at https://login.tailscale.com/admin/settings/oauth with these **Read** scopes: Logs > Configuration, General > DNS, General > Users, Devices > Core, Keys > Auth Keys, Keys > Webhooks, Settings > Feature Settings (or tick `all:read` to grant all read scopes at once). Find your tailnet name on the Keys page.", + "instructions": [ + { + "type": "Textbox", + "parameters": { + "label": "Tailscale tailnet", + "placeholder": "tail-XXXX.ts.net", + "type": "text", + "name": "tailnetName" + } + }, + { + "type": "Textbox", + "parameters": { + "label": "OAuth Client ID", + "placeholder": "k...", + "type": "text", + "name": "clientId" + } + }, + { + "type": "Textbox", + "parameters": { + "label": "OAuth Client Secret", + "placeholder": "tskey-client-...", + "type": "password", + "name": "clientSecret" + } + }, + { + "type": "ConnectionToggleButton", + "parameters": { + "connectLabel": "Connect", + "disconnectLabel": "Disconnect" + } + } + ] + } + ], + "id": "TailscaleCCF" + } + } + }, + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', variables('_dataConnectorContentIdConnectorDefinition1')))]", + "apiVersion": "2022-01-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "properties": { + "parentId": "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectorDefinitions', variables('_dataConnectorContentIdConnectorDefinition1'))]", + "contentId": "[variables('_dataConnectorContentIdConnectorDefinition1')]", + "kind": "DataConnector", + "version": "[variables('dataConnectorCCPVersion')]", + "source": { + "sourceId": "[variables('_solutionId')]", + "name": "[variables('_solutionName')]", + "kind": "Solution" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + }, + "dependencies": { + "criteria": [ + { + "version": "[variables('dataConnectorCCPVersion')]", + "contentId": "[variables('_dataConnectorContentIdConnections1')]", + "kind": "ResourcesDataConnector" + } + ] + } + } + }, + { + "name": "TailscaleDCR", + "apiVersion": "2022-06-01", + "type": "Microsoft.Insights/dataCollectionRules", + "location": "[parameters('workspace-location')]", + "kind": "WorkspaceTransforms", + "properties": { + "dataCollectionEndpointId": "[variables('dataCollectionEndpointId1')]", + "streamDeclarations": { + "Custom-Tailscale_Audit_CL": { + "columns": [ + { + "name": "eventTime", + "type": "datetime" + }, + { + "name": "eventGroupID", + "type": "string" + }, + { + "name": "type", + "type": "string" + }, + { + "name": "actionDetails", + "type": "string" + }, + { + "name": "actor", + "type": "dynamic" + }, + { + "name": "action", + "type": "string" + }, + { + "name": "target", + "type": "dynamic" + }, + { + "name": "origin", + "type": "dynamic" + }, + { + "name": "new", + "type": "dynamic" + }, + { + "name": "old", + "type": "dynamic" + } + ] + }, + "Custom-Tailscale_Devices_CL": { + "columns": [ + { + "name": "id", + "type": "string" + }, + { + "name": "name", + "type": "string" + }, + { + "name": "hostname", + "type": "string" + }, + { + "name": "user", + "type": "string" + }, + { + "name": "os", + "type": "string" + }, + { + "name": "clientVersion", + "type": "string" + }, + { + "name": "updateAvailable", + "type": "boolean" + }, + { + "name": "authorized", + "type": "boolean" + }, + { + "name": "isExternal", + "type": "boolean" + }, + { + "name": "created", + "type": "datetime" + }, + { + "name": "lastSeen", + "type": "datetime" + }, + { + "name": "expires", + "type": "datetime" + }, + { + "name": "keyExpiryDisabled", + "type": "boolean" + }, + { + "name": "blocksIncomingConnections", + "type": "boolean" + }, + { + "name": "addresses", + "type": "dynamic" + }, + { + "name": "tags", + "type": "dynamic" + }, + { + "name": "enabledRoutes", + "type": "dynamic" + }, + { + "name": "advertisedRoutes", + "type": "dynamic" + }, + { + "name": "clientConnectivity", + "type": "dynamic" + }, + { + "name": "machineKey", + "type": "string" + }, + { + "name": "nodeKey", + "type": "string" + }, + { + "name": "distro", + "type": "string" + }, + { + "name": "sshEnabled", + "type": "boolean" + }, + { + "name": "connectedToControl", + "type": "boolean" + }, + { + "name": "tailnetLockKey", + "type": "string" + }, + { + "name": "tailnetLockError", + "type": "string" + } + ] + }, + "Custom-Tailscale_Users_CL": { + "columns": [ + { + "name": "id", + "type": "string" + }, + { + "name": "displayName", + "type": "string" + }, + { + "name": "loginName", + "type": "string" + }, + { + "name": "tailnetId", + "type": "string" + }, + { + "name": "type", + "type": "string" + }, + { + "name": "role", + "type": "string" + }, + { + "name": "status", + "type": "string" + }, + { + "name": "deviceCount", + "type": "int" + }, + { + "name": "created", + "type": "datetime" + }, + { + "name": "lastSeen", + "type": "datetime" + }, + { + "name": "currentlyConnected", + "type": "boolean" + }, + { + "name": "profilePicUrl", + "type": "string" + } + ] + }, + "Custom-Tailscale_Keys_CL": { + "columns": [ + { + "name": "id", + "type": "string" + }, + { + "name": "description", + "type": "string" + }, + { + "name": "userId", + "type": "string" + }, + { + "name": "created", + "type": "datetime" + }, + { + "name": "expires", + "type": "datetime" + }, + { + "name": "revoked", + "type": "datetime" + }, + { + "name": "capabilities", + "type": "dynamic" + }, + { + "name": "keyType", + "type": "string" + }, + { + "name": "expirySeconds", + "type": "int" + } + ] + }, + "Custom-Tailscale_Webhooks_CL": { + "columns": [ + { + "name": "endpointId", + "type": "string" + }, + { + "name": "endpointUrl", + "type": "string" + }, + { + "name": "providerType", + "type": "string" + }, + { + "name": "creatorLoginName", + "type": "string" + }, + { + "name": "created", + "type": "datetime" + }, + { + "name": "lastModified", + "type": "datetime" + }, + { + "name": "subscriptions", + "type": "dynamic" + } + ] + }, + "Custom-Tailscale_DnsNameservers_CL": { + "columns": [ + { + "name": "dns", + "type": "dynamic" + } + ] + }, + "Custom-Tailscale_Settings_CL": { + "columns": [ + { + "name": "devicesApprovalOn", + "type": "boolean" + }, + { + "name": "devicesAutoUpdatesOn", + "type": "boolean" + }, + { + "name": "devicesKeyDurationDays", + "type": "int" + }, + { + "name": "usersApprovalOn", + "type": "boolean" + }, + { + "name": "usersRoleAllowedToJoinExternalTailnets", + "type": "string" + }, + { + "name": "networkFlowLoggingOn", + "type": "boolean" + }, + { + "name": "regionalRoutingOn", + "type": "boolean" + }, + { + "name": "postureIdentityCollectionOn", + "type": "boolean" + } + ] + }, + "Custom-Tailscale_DnsPreferences_CL": { + "columns": [ + { + "name": "magicDNS", + "type": "boolean" + } + ] + }, + "Custom-Tailscale_DnsSearchPaths_CL": { + "columns": [ + { + "name": "searchPaths", + "type": "dynamic" + } + ] + } + }, + "destinations": { + "logAnalytics": [ + { + "workspaceResourceId": "[variables('workspaceResourceId')]", + "name": "sentinelWorkspace" + } + ] + }, + "dataFlows": [ + { + "streams": [ + "Custom-Tailscale_Audit_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = eventTime | extend EventTime = eventTime | extend EventGroupID = eventGroupID | extend EventType = type | extend ActionDetails = actionDetails | extend Actor = actor | extend Action = action | extend Target = target | extend Origin = origin | extend New = new | extend Old = old | project TimeGenerated, EventTime, EventGroupID, EventType, ActionDetails, Actor, Action, Target, Origin, New, Old", + "outputStream": "Custom-Tailscale_Audit_CL" + }, + { + "streams": [ + "Custom-Tailscale_Devices_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend DeviceId = id | extend DeviceName = name | extend Hostname = hostname | extend User = user | extend Os = os | extend Distro = distro | extend ClientVersion = clientVersion | extend UpdateAvailable = updateAvailable | extend Authorized = authorized | extend IsExternal = isExternal | extend Created = created | extend LastSeen = lastSeen | extend Expires = expires | extend KeyExpiryDisabled = keyExpiryDisabled | extend BlocksIncomingConnections = blocksIncomingConnections | extend SshEnabled = sshEnabled | extend ConnectedToControl = connectedToControl | extend Addresses = addresses | extend Tags = tags | extend EnabledRoutes = enabledRoutes | extend AdvertisedRoutes = advertisedRoutes | extend ClientConnectivity = clientConnectivity | extend MachineKey = machineKey | extend NodeKey = nodeKey | extend TailnetLockKey = tailnetLockKey | extend TailnetLockError = tailnetLockError | project TimeGenerated, DeviceId, DeviceName, Hostname, User, Os, Distro, ClientVersion, UpdateAvailable, Authorized, IsExternal, Created, LastSeen, Expires, KeyExpiryDisabled, BlocksIncomingConnections, SshEnabled, ConnectedToControl, Addresses, Tags, EnabledRoutes, AdvertisedRoutes, ClientConnectivity, MachineKey, NodeKey, TailnetLockKey, TailnetLockError", + "outputStream": "Custom-Tailscale_Devices_CL" + }, + { + "streams": [ + "Custom-Tailscale_Users_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend UserId = id | extend DisplayName = displayName | extend LoginName = loginName | extend TailnetId = tailnetId | extend UserType = type | extend Role = role | extend Status = status | extend DeviceCount = deviceCount | extend Created = created | extend LastSeen = lastSeen | extend CurrentlyConnected = currentlyConnected | extend ProfilePicUrl = profilePicUrl | project TimeGenerated, UserId, DisplayName, LoginName, TailnetId, UserType, Role, Status, DeviceCount, Created, LastSeen, CurrentlyConnected, ProfilePicUrl", + "outputStream": "Custom-Tailscale_Users_CL" + }, + { + "streams": [ + "Custom-Tailscale_Keys_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend KeyId = id | extend Description = description | extend UserId = userId | extend Created = created | extend Expires = expires | extend Revoked = revoked | extend Capabilities = capabilities | extend KeyType = keyType | extend ExpirySeconds = expirySeconds | project TimeGenerated, KeyId, Description, UserId, Created, Expires, Revoked, Capabilities, KeyType, ExpirySeconds", + "outputStream": "Custom-Tailscale_Keys_CL" + }, + { + "streams": [ + "Custom-Tailscale_Webhooks_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend EndpointId = endpointId | extend EndpointUrl = endpointUrl | extend ProviderType = providerType | extend CreatorLoginName = creatorLoginName | extend Created = created | extend LastModified = lastModified | extend Subscriptions = subscriptions | project TimeGenerated, EndpointId, EndpointUrl, ProviderType, CreatorLoginName, Created, LastModified, Subscriptions", + "outputStream": "Custom-Tailscale_Webhooks_CL" + }, + { + "streams": [ + "Custom-Tailscale_DnsNameservers_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend ConfigType = 'nameservers' | extend Nameservers = dns | project TimeGenerated, ConfigType, Nameservers", + "outputStream": "Custom-Tailscale_Dns_CL" + }, + { + "streams": [ + "Custom-Tailscale_Settings_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend DevicesApprovalOn = devicesApprovalOn | extend DevicesAutoUpdatesOn = devicesAutoUpdatesOn | extend DevicesKeyDurationDays = devicesKeyDurationDays | extend UsersApprovalOn = usersApprovalOn | extend UsersRoleAllowedToJoinExternalTailnets = usersRoleAllowedToJoinExternalTailnets | extend NetworkFlowLoggingOn = networkFlowLoggingOn | extend RegionalRoutingOn = regionalRoutingOn | extend PostureIdentityCollectionOn = postureIdentityCollectionOn | project TimeGenerated, DevicesApprovalOn, DevicesAutoUpdatesOn, DevicesKeyDurationDays, UsersApprovalOn, UsersRoleAllowedToJoinExternalTailnets, NetworkFlowLoggingOn, RegionalRoutingOn, PostureIdentityCollectionOn", + "outputStream": "Custom-Tailscale_Settings_CL" + }, + { + "streams": [ + "Custom-Tailscale_DnsPreferences_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend ConfigType = 'preferences' | extend MagicDNS = magicDNS | project TimeGenerated, ConfigType, MagicDNS", + "outputStream": "Custom-Tailscale_Dns_CL" + }, + { + "streams": [ + "Custom-Tailscale_DnsSearchPaths_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend ConfigType = 'searchpaths' | extend SearchPaths = searchPaths | project TimeGenerated, ConfigType, SearchPaths", + "outputStream": "Custom-Tailscale_Dns_CL" + } + ] + } + }, + { + "name": "Tailscale_Audit_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "location": "[parameters('workspace-location')]", + "kind": null, + "properties": { + "schema": { + "name": "Tailscale_Audit_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "EventTime", + "type": "datetime" + }, + { + "name": "EventGroupID", + "type": "string" + }, + { + "name": "EventType", + "type": "string" + }, + { + "name": "ActionDetails", + "type": "string" + }, + { + "name": "Actor", + "type": "dynamic" + }, + { + "name": "Action", + "type": "string" + }, + { + "name": "Target", + "type": "dynamic" + }, + { + "name": "Origin", + "type": "dynamic" + }, + { + "name": "New", + "type": "dynamic" + }, + { + "name": "Old", + "type": "dynamic" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Devices_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "location": "[parameters('workspace-location')]", + "kind": null, + "properties": { + "schema": { + "name": "Tailscale_Devices_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "DeviceId", + "type": "string" + }, + { + "name": "DeviceName", + "type": "string" + }, + { + "name": "Hostname", + "type": "string" + }, + { + "name": "User", + "type": "string" + }, + { + "name": "Os", + "type": "string" + }, + { + "name": "ClientVersion", + "type": "string" + }, + { + "name": "UpdateAvailable", + "type": "boolean" + }, + { + "name": "Authorized", + "type": "boolean" + }, + { + "name": "IsExternal", + "type": "boolean" + }, + { + "name": "Created", + "type": "datetime" + }, + { + "name": "LastSeen", + "type": "datetime" + }, + { + "name": "Expires", + "type": "datetime" + }, + { + "name": "KeyExpiryDisabled", + "type": "boolean" + }, + { + "name": "BlocksIncomingConnections", + "type": "boolean" + }, + { + "name": "Addresses", + "type": "dynamic" + }, + { + "name": "Tags", + "type": "dynamic" + }, + { + "name": "EnabledRoutes", + "type": "dynamic" + }, + { + "name": "AdvertisedRoutes", + "type": "dynamic" + }, + { + "name": "ClientConnectivity", + "type": "dynamic" + }, + { + "name": "MachineKey", + "type": "string" + }, + { + "name": "NodeKey", + "type": "string" + }, + { + "name": "Distro", + "type": "string" + }, + { + "name": "SshEnabled", + "type": "boolean" + }, + { + "name": "ConnectedToControl", + "type": "boolean" + }, + { + "name": "TailnetLockKey", + "type": "string" + }, + { + "name": "TailnetLockError", + "type": "string" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Users_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "location": "[parameters('workspace-location')]", + "kind": null, + "properties": { + "schema": { + "name": "Tailscale_Users_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "UserId", + "type": "string" + }, + { + "name": "DisplayName", + "type": "string" + }, + { + "name": "LoginName", + "type": "string" + }, + { + "name": "TailnetId", + "type": "string" + }, + { + "name": "UserType", + "type": "string" + }, + { + "name": "Role", + "type": "string" + }, + { + "name": "Status", + "type": "string" + }, + { + "name": "DeviceCount", + "type": "int" + }, + { + "name": "Created", + "type": "datetime" + }, + { + "name": "LastSeen", + "type": "datetime" + }, + { + "name": "CurrentlyConnected", + "type": "boolean" + }, + { + "name": "ProfilePicUrl", + "type": "string" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Keys_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "location": "[parameters('workspace-location')]", + "kind": null, + "properties": { + "schema": { + "name": "Tailscale_Keys_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "KeyId", + "type": "string" + }, + { + "name": "Description", + "type": "string" + }, + { + "name": "UserId", + "type": "string" + }, + { + "name": "Created", + "type": "datetime" + }, + { + "name": "Expires", + "type": "datetime" + }, + { + "name": "Revoked", + "type": "datetime" + }, + { + "name": "Capabilities", + "type": "dynamic" + }, + { + "name": "KeyType", + "type": "string" + }, + { + "name": "ExpirySeconds", + "type": "int" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Webhooks_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "location": "[parameters('workspace-location')]", + "kind": null, + "properties": { + "schema": { + "name": "Tailscale_Webhooks_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "EndpointId", + "type": "string" + }, + { + "name": "EndpointUrl", + "type": "string" + }, + { + "name": "ProviderType", + "type": "string" + }, + { + "name": "CreatorLoginName", + "type": "string" + }, + { + "name": "Created", + "type": "datetime" + }, + { + "name": "LastModified", + "type": "datetime" + }, + { + "name": "Subscriptions", + "type": "dynamic" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Settings_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "location": "[parameters('workspace-location')]", + "kind": null, + "properties": { + "schema": { + "name": "Tailscale_Settings_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "DevicesApprovalOn", + "type": "boolean" + }, + { + "name": "DevicesAutoUpdatesOn", + "type": "boolean" + }, + { + "name": "DevicesKeyDurationDays", + "type": "int" + }, + { + "name": "UsersApprovalOn", + "type": "boolean" + }, + { + "name": "UsersRoleAllowedToJoinExternalTailnets", + "type": "string" + }, + { + "name": "NetworkFlowLoggingOn", + "type": "boolean" + }, + { + "name": "RegionalRoutingOn", + "type": "boolean" + }, + { + "name": "PostureIdentityCollectionOn", + "type": "boolean" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Dns_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "location": "[parameters('workspace-location')]", + "kind": null, + "properties": { + "schema": { + "name": "Tailscale_Dns_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "ConfigType", + "type": "string" + }, + { + "name": "Nameservers", + "type": "dynamic" + }, + { + "name": "MagicDNS", + "type": "boolean" + }, + { + "name": "SearchPaths", + "type": "dynamic" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Network_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "location": "[parameters('workspace-location')]", + "kind": null, + "properties": { + "schema": { + "name": "Tailscale_Network_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "NodeId", + "type": "string" + }, + { + "name": "FlowStart", + "type": "datetime" + }, + { + "name": "FlowEnd", + "type": "datetime" + }, + { + "name": "SrcNode", + "type": "dynamic" + }, + { + "name": "SrcUser", + "type": "string" + }, + { + "name": "SrcNodeName", + "type": "string" + }, + { + "name": "SrcOs", + "type": "string" + }, + { + "name": "SrcTags", + "type": "dynamic" + }, + { + "name": "SrcAddresses", + "type": "dynamic" + }, + { + "name": "DstNodes", + "type": "dynamic" + }, + { + "name": "DstCount", + "type": "int" + }, + { + "name": "DstNodeId", + "type": "string" + }, + { + "name": "DstNodeName", + "type": "string" + }, + { + "name": "DstUser", + "type": "string" + }, + { + "name": "DstOs", + "type": "string" + }, + { + "name": "DstTags", + "type": "dynamic" + }, + { + "name": "DstAddresses", + "type": "dynamic" + }, + { + "name": "VirtualTraffic", + "type": "dynamic" + }, + { + "name": "SubnetTraffic", + "type": "dynamic" + }, + { + "name": "ExitTraffic", + "type": "dynamic" + }, + { + "name": "PhysicalTraffic", + "type": "dynamic" + }, + { + "name": "HasVirtualTraffic", + "type": "boolean" + }, + { + "name": "HasSubnetTraffic", + "type": "boolean" + }, + { + "name": "HasExitTraffic", + "type": "boolean" + }, + { + "name": "HasPhysicalTraffic", + "type": "boolean" + }, + { + "name": "IsRelayed", + "type": "boolean" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_PostureIntegrations_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "location": "[parameters('workspace-location')]", + "kind": null, + "properties": { + "schema": { + "name": "Tailscale_PostureIntegrations_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "IntegrationId", + "type": "string" + }, + { + "name": "Provider", + "type": "string" + }, + { + "name": "CloudId", + "type": "string" + }, + { + "name": "ClientId", + "type": "string" + }, + { + "name": "TenantId_Provider", + "type": "string" + }, + { + "name": "ConfigOverwrites", + "type": "dynamic" + }, + { + "name": "Status", + "type": "dynamic" + } + ] + }, + "retentionInDays": 90 + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "contentProductId": "[concat(take(variables('_solutionId'), 50),'-','dc','-', uniqueString(concat(variables('_solutionId'),'-','DataConnector','-',variables('_dataConnectorContentIdConnectorDefinition1'),'-', variables('dataConnectorCCPVersion'))))]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "version": "[variables('dataConnectorCCPVersion')]" + } + }, + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',variables('_dataConnectorContentIdConnectorDefinition1'))]", + "apiVersion": "2022-09-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectorDefinitions", + "location": "[parameters('workspace-location')]", + "kind": "Customizable", + "properties": { + "connectorUiConfig": { + "title": "Tailscale Standard (CCF)", + "publisher": "Community", + "logo": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIGlkPSJMYXllcl8xIiB4PSIwIiB5PSIwIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48c3R5bGU+LnN0MHtvcGFjaXR5Oi4yO2VuYWJsZS1iYWNrZ3JvdW5kOm5ld308L3N0eWxlPjxwYXRoIGQ9Ik02NS42IDEyNy43YzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45UzEwMC45IDAgNjUuNiAwIDEuOCAyOC42IDEuOCA2My45czI4LjYgNjMuOCA2My44IDYzLjgiIGNsYXNzPSJzdDAiLz48cGF0aCBkPSJNNjUuNiAzMTguMWMzNS4zIDAgNjMuOS0yOC42IDYzLjktNjMuOXMtMjguNi02My45LTYzLjktNjMuOVMxLjggMjE5IDEuOCAyNTQuMnMyOC42IDYzLjkgNjMuOCA2My45Ii8+PHBhdGggZD0iTTY1LjYgNTEyYzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45cy0yOC42LTYzLjktNjMuOS02My45LTYzLjggMjguNy02My44IDYzLjlTMzAuNCA1MTIgNjUuNiA1MTIiIGNsYXNzPSJzdDAiLz48cGF0aCBkPSJNMjU3LjIgMzE4LjFjMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlzLTI4LjYtNjMuOS02My45LTYzLjktNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjkgNjMuOSA2My45bTAgMTkzLjljMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlzLTI4LjYtNjMuOS02My45LTYzLjktNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjkgNjMuOSA2My45Ii8+PHBhdGggZD0iTTI1Ny4yIDEyNy43YzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45UzI5Mi41IDAgMjU3LjIgMHMtNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjggNjMuOSA2My44bTE4OS4yIDBjMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlTNDgxLjYgMCA0NDYuNCAwYy0zNS4zIDAtNjMuOSAyOC42LTYzLjkgNjMuOXMyOC42IDYzLjggNjMuOSA2My44IiBjbGFzcz0ic3QwIi8+PHBhdGggZD0iTTQ0Ni40IDMxOC4xYzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45cy0yOC42LTYzLjktNjMuOS02My45LTYzLjkgMjguNi02My45IDYzLjkgMjguNiA2My45IDYzLjkgNjMuOSIvPjxwYXRoIGQ9Ik00NDYuNCA1MTJjMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlzLTI4LjYtNjMuOS02My45LTYzLjktNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjkgNjMuOSA2My45IiBjbGFzcz0ic3QwIi8+PC9zdmc+", + "descriptionMarkdown": "Comprehensive Tailscale telemetry for **Personal (Free) and Standard** tier tailnets. Polls nine endpoints in one Connect:\n\n- `/logging/configuration` - configuration audit events (includes ACL, DNS, tag/group, settings changes)\n- `/devices` - device inventory (hostname, OS, IPs, tags, lastSeen, expiry)\n- `/users` - user inventory (role, status, deviceCount, connection state)\n- `/keys?all=true` - auth keys, API tokens, and OAuth client metadata\n- `/webhooks` - webhook configuration\n- `/dns/nameservers`, `/dns/preferences`, `/dns/searchpaths` - DNS state (merged into single `Tailscale_Dns_CL` table with `ConfigType` discriminator)\n- `/settings` - tailnet settings flags (device approval, key duration, etc.)\n\nSplit-DNS state (per-domain DNS overrides) is captured via the audit log rather than a separate snapshot table - every change is recorded with the full before/after document and actor attribution, which is richer than a periodic snapshot.\n\n**OAuth scopes required on the Tailscale client:** `logs:configuration:read`, `devices:core:read`, `users:read`, `auth_keys:read`, `webhooks:read`, `dns:read`, `feature_settings:read` (or the bundled `all:read`). For Premium and Enterprise tailnets that also need network flow logs and posture integrations, install **Tailscale Premium (CCF)** instead.", + "graphQueries": [ + { + "metricName": "Audit events", + "legend": "Tailscale_Audit_CL", + "baseQuery": "Tailscale_Audit_CL" + }, + { + "metricName": "Device snapshots", + "legend": "Tailscale_Devices_CL", + "baseQuery": "Tailscale_Devices_CL" + }, + { + "metricName": "User snapshots", + "legend": "Tailscale_Users_CL", + "baseQuery": "Tailscale_Users_CL" + }, + { + "metricName": "Auth keys", + "legend": "Tailscale_Keys_CL", + "baseQuery": "Tailscale_Keys_CL" + }, + { + "metricName": "Webhooks", + "legend": "Tailscale_Webhooks_CL", + "baseQuery": "Tailscale_Webhooks_CL" + }, + { + "metricName": "Tailnet settings", + "legend": "Tailscale_Settings_CL", + "baseQuery": "Tailscale_Settings_CL" + }, + { + "metricName": "DNS config snapshots", + "legend": "Tailscale_Dns_CL", + "baseQuery": "Tailscale_Dns_CL" + } + ], + "dataTypes": [ + { + "name": "Tailscale_Audit_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Audit_CL')]" + }, + { + "name": "Tailscale_Devices_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Devices_CL')]" + }, + { + "name": "Tailscale_Users_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Users_CL')]" + }, + { + "name": "Tailscale_Keys_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Keys_CL')]" + }, + { + "name": "Tailscale_Webhooks_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Webhooks_CL')]" + }, + { + "name": "Tailscale_Settings_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Settings_CL')]" + }, + { + "name": "Tailscale_Dns_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Dns_CL')]" + } + ], + "sampleQueries": [ + { + "description": "Recent audit events", + "query": "Tailscale_Audit_CL\n| sort by TimeGenerated desc\n| take 100" + }, + { + "description": "Current device inventory (latest snapshot per device)", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| project DeviceName, User, Os, ClientVersion, LastSeen, Expires, Tags" + }, + { + "description": "Users by role", + "query": "Tailscale_Users_CL\n| summarize arg_max(TimeGenerated, *) by UserId\n| summarize Users = count() by Role" + }, + { + "description": "Auth keys without an expiry", + "query": "Tailscale_Keys_CL\n| summarize arg_max(TimeGenerated, *) by KeyId\n| where isnull(Expires) or Expires == datetime(null)" + }, + { + "description": "Tailnet device-approval status", + "query": "Tailscale_Settings_CL\n| sort by TimeGenerated desc\n| take 1\n| project DevicesApprovalOn, DevicesAutoUpdatesOn, UsersApprovalOn" + }, + { + "description": "Current DNS state (all DNS config in one query)", + "query": "Tailscale_Dns_CL\n| summarize arg_max(TimeGenerated, *) by ConfigType\n| project ConfigType, Nameservers, MagicDNS, SearchPaths" + } + ], + "connectivityCriteria": [ + { + "type": "HasDataConnectors" + } + ], + "availability": { + "status": 1, + "isPreview": true + }, + "permissions": { + "resourceProvider": [ + { + "provider": "Microsoft.OperationalInsights/workspaces", + "permissionsDisplayText": "Read/Write on the workspace", + "providerDisplayName": "Workspace", + "scope": "Workspace", + "requiredPermissions": { + "write": true, + "read": true, + "delete": true + } + } + ] + }, + "instructionSteps": [ + { + "title": "Connect Tailscale", + "description": "Generate an OAuth client at https://login.tailscale.com/admin/settings/oauth with these **Read** scopes: Logs > Configuration, General > DNS, General > Users, Devices > Core, Keys > Auth Keys, Keys > Webhooks, Settings > Feature Settings (or tick `all:read` to grant all read scopes at once). Find your tailnet name on the Keys page.", + "instructions": [ + { + "type": "Textbox", + "parameters": { + "label": "Tailscale tailnet", + "placeholder": "tail-XXXX.ts.net", + "type": "text", + "name": "tailnetName" + } + }, + { + "type": "Textbox", + "parameters": { + "label": "OAuth Client ID", + "placeholder": "k...", + "type": "text", + "name": "clientId" + } + }, + { + "type": "Textbox", + "parameters": { + "label": "OAuth Client Secret", + "placeholder": "tskey-client-...", + "type": "password", + "name": "clientSecret" + } + }, + { + "type": "ConnectionToggleButton", + "parameters": { + "connectLabel": "Connect", + "disconnectLabel": "Disconnect" + } + } + ] + } + ], + "id": "TailscaleCCF" + } + } + }, + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', variables('_dataConnectorContentIdConnectorDefinition1')))]", + "apiVersion": "2022-01-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "properties": { + "parentId": "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectorDefinitions', variables('_dataConnectorContentIdConnectorDefinition1'))]", + "contentId": "[variables('_dataConnectorContentIdConnectorDefinition1')]", + "kind": "DataConnector", + "version": "[variables('dataConnectorCCPVersion')]", + "source": { + "sourceId": "[variables('_solutionId')]", + "name": "[variables('_solutionName')]", + "kind": "Solution" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + }, + "dependencies": { + "criteria": [ + { + "version": "[variables('dataConnectorCCPVersion')]", + "contentId": "[variables('_dataConnectorContentIdConnections1')]", + "kind": "ResourcesDataConnector" + } + ] + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/', variables('dataConnectorTemplateNameConnections1'), variables('dataConnectorCCPVersion'))]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "contentId": "[variables('_dataConnectorContentIdConnections1')]", + "displayName": "Tailscale Standard (CCF)", + "contentKind": "ResourcesDataConnector", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('dataConnectorCCPVersion')]", + "parameters": { + "guidValue": { + "defaultValue": "[[newGuid()]", + "type": "securestring" + }, + "innerWorkspace": { + "defaultValue": "[parameters('workspace')]", + "type": "securestring" + }, + "connectorDefinitionName": { + "defaultValue": "Tailscale Standard (CCF)", + "type": "securestring", + "minLength": 1 + }, + "workspace": { + "defaultValue": "[parameters('workspace')]", + "type": "securestring" + }, + "dcrConfig": { + "defaultValue": { + "dataCollectionEndpoint": "data collection Endpoint", + "dataCollectionRuleImmutableId": "data collection rule immutableId" + }, + "type": "object" + }, + "tailnetName": { + "defaultValue": "tailnetName", + "type": "securestring", + "minLength": 1 + }, + "clientId": { + "defaultValue": "clientId", + "type": "securestring", + "minLength": 1 + }, + "clientSecret": { + "defaultValue": "clientSecret", + "type": "securestring", + "minLength": 1 + } + }, + "variables": { + "_dataConnectorContentIdConnections1": "[variables('_dataConnectorContentIdConnections1')]" + }, + "resources": [ + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', variables('_dataConnectorContentIdConnections1')))]", + "apiVersion": "2022-01-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "properties": { + "parentId": "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectors', variables('_dataConnectorContentIdConnections1'))]", + "contentId": "[variables('_dataConnectorContentIdConnections1')]", + "kind": "ResourcesDataConnector", + "version": "[variables('dataConnectorCCPVersion')]", + "source": { + "sourceId": "[variables('_solutionId')]", + "name": "[variables('_solutionName')]", + "kind": "Solution" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + }, + { + "name": "[[concat(parameters('innerWorkspace'),'/Microsoft.SecurityInsights/', 'TailscaleConfigAuditPoller', parameters('guidValue'))]", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscaleCCF", + "dataType": "Tailscale_Audit_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Audit_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/logging/configuration')]", + "httpMethod": "GET", + "queryWindowInMin": 5, + "queryTimeFormat": "yyyy-MM-ddTHH:mm:ssZ", + "startTimeAttributeName": "start", + "endTimeAttributeName": "end", + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$.logs" + ] + }, + "isActive": true + } + }, + { + "name": "[[concat(parameters('innerWorkspace'),'/Microsoft.SecurityInsights/', 'TailscaleDevicesPoller', parameters('guidValue'))]", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscaleCCF", + "dataType": "Tailscale_Devices_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Devices_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/devices?fields=all')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$.devices" + ] + }, + "isActive": true + } + }, + { + "name": "[[concat(parameters('innerWorkspace'),'/Microsoft.SecurityInsights/', 'TailscaleUsersPoller', parameters('guidValue'))]", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscaleCCF", + "dataType": "Tailscale_Users_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Users_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/users')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$.users" + ] + }, + "isActive": true + } + }, + { + "name": "[[concat(parameters('innerWorkspace'),'/Microsoft.SecurityInsights/', 'TailscaleKeysPoller', parameters('guidValue'))]", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscaleCCF", + "dataType": "Tailscale_Keys_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Keys_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/keys?all=true')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$.keys" + ] + }, + "isActive": true + } + }, + { + "name": "[[concat(parameters('innerWorkspace'),'/Microsoft.SecurityInsights/', 'TailscaleWebhooksPoller', parameters('guidValue'))]", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscaleCCF", + "dataType": "Tailscale_Webhooks_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Webhooks_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/webhooks')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$.webhooks" + ] + }, + "isActive": true + } + }, + { + "name": "[[concat(parameters('innerWorkspace'),'/Microsoft.SecurityInsights/', 'TailscaleDnsNameserversPoller', parameters('guidValue'))]", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscaleCCF", + "dataType": "Tailscale_Dns_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_DnsNameservers_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/dns/nameservers')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$" + ] + }, + "isActive": true + } + }, + { + "name": "[[concat(parameters('innerWorkspace'),'/Microsoft.SecurityInsights/', 'TailscaleSettingsPoller', parameters('guidValue'))]", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscaleCCF", + "dataType": "Tailscale_Settings_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Settings_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/settings')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$" + ] + }, + "isActive": true + } + }, + { + "name": "[[concat(parameters('innerWorkspace'),'/Microsoft.SecurityInsights/', 'TailscaleDnsPreferencesPoller', parameters('guidValue'))]", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscaleCCF", + "dataType": "Tailscale_Dns_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_DnsPreferences_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/dns/preferences')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$" + ] + }, + "isActive": true + } + }, + { + "name": "[[concat(parameters('innerWorkspace'),'/Microsoft.SecurityInsights/', 'TailscaleDnsSearchPathsPoller', parameters('guidValue'))]", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscaleCCF", + "dataType": "Tailscale_Dns_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_DnsSearchPaths_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/dns/searchpaths')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$" + ] + }, + "isActive": true + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "contentProductId": "[concat(take(variables('_solutionId'), 50),'-','rdc','-', uniqueString(concat(variables('_solutionId'),'-','ResourcesDataConnector','-',variables('_dataConnectorContentIdConnections1'),'-', variables('dataConnectorCCPVersion'))))]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "version": "[variables('dataConnectorCCPVersion')]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/', variables('dataConnectorTemplateNameConnectorDefinition2'), variables('dataConnectorCCPVersion'))]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "contentId": "[variables('_dataConnectorContentIdConnectorDefinition2')]", + "displayName": "Tailscale Premium (CCF)", + "contentKind": "DataConnector", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('dataConnectorCCPVersion')]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',variables('_dataConnectorContentIdConnectorDefinition2'))]", + "apiVersion": "2022-09-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectorDefinitions", + "location": "[parameters('workspace-location')]", + "kind": "Customizable", + "properties": { + "connectorUiConfig": { + "title": "Tailscale Premium (CCF)", + "publisher": "Community", + "logo": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIGlkPSJMYXllcl8xIiB4PSIwIiB5PSIwIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48c3R5bGU+LnN0MHtvcGFjaXR5Oi4yO2VuYWJsZS1iYWNrZ3JvdW5kOm5ld308L3N0eWxlPjxwYXRoIGQ9Ik02NS42IDEyNy43YzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45UzEwMC45IDAgNjUuNiAwIDEuOCAyOC42IDEuOCA2My45czI4LjYgNjMuOCA2My44IDYzLjgiIGNsYXNzPSJzdDAiLz48cGF0aCBkPSJNNjUuNiAzMTguMWMzNS4zIDAgNjMuOS0yOC42IDYzLjktNjMuOXMtMjguNi02My45LTYzLjktNjMuOVMxLjggMjE5IDEuOCAyNTQuMnMyOC42IDYzLjkgNjMuOCA2My45Ii8+PHBhdGggZD0iTTY1LjYgNTEyYzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45cy0yOC42LTYzLjktNjMuOS02My45LTYzLjggMjguNy02My44IDYzLjlTMzAuNCA1MTIgNjUuNiA1MTIiIGNsYXNzPSJzdDAiLz48cGF0aCBkPSJNMjU3LjIgMzE4LjFjMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlzLTI4LjYtNjMuOS02My45LTYzLjktNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjkgNjMuOSA2My45bTAgMTkzLjljMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlzLTI4LjYtNjMuOS02My45LTYzLjktNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjkgNjMuOSA2My45Ii8+PHBhdGggZD0iTTI1Ny4yIDEyNy43YzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45UzI5Mi41IDAgMjU3LjIgMHMtNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjggNjMuOSA2My44bTE4OS4yIDBjMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlTNDgxLjYgMCA0NDYuNCAwYy0zNS4zIDAtNjMuOSAyOC42LTYzLjkgNjMuOXMyOC42IDYzLjggNjMuOSA2My44IiBjbGFzcz0ic3QwIi8+PHBhdGggZD0iTTQ0Ni40IDMxOC4xYzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45cy0yOC42LTYzLjktNjMuOS02My45LTYzLjkgMjguNi02My45IDYzLjkgMjguNiA2My45IDYzLjkgNjMuOSIvPjxwYXRoIGQ9Ik00NDYuNCA1MTJjMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlzLTI4LjYtNjMuOS02My45LTYzLjktNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjkgNjMuOSA2My45IiBjbGFzcz0ic3QwIi8+PC9zdmc+", + "descriptionMarkdown": "Comprehensive Tailscale telemetry for **Premium and Enterprise** tier tailnets. Polls every endpoint the Standard connector polls, **plus** Premium-only network flow logs and posture-integration inventory. Eleven endpoints in one Connect:\n\n- `/logging/configuration` - configuration audit events\n- `/logging/network` - **Premium** network flow logs (per-node traffic with src/dst/protocol/bytes)\n- `/devices` - device inventory\n- `/users` - user inventory\n- `/keys?all=true` - auth keys + API tokens + OAuth clients\n- `/webhooks` - webhook configuration\n- `/dns/nameservers`, `/dns/preferences`, `/dns/searchpaths` - DNS state (merged into single `Tailscale_Dns_CL` table with `ConfigType` discriminator)\n- `/settings` - tailnet settings flags\n- `/posture/integrations` - **Premium** MDM/EDR integration inventory (Jamf, Kandji, Intune, Kolide, Microsoft Defender for Endpoint, CrowdStrike Falcon, SentinelOne, etc.)\n\n**OAuth scopes required:** `logs:configuration:read`, `logs:network:read`, `devices:core:read`, `users:read`, `auth_keys:read`, `webhooks:read`, `dns:read`, `feature_settings:read` (or the bundled `all:read`).\n\n**If your tailnet is Personal (Free) or Standard tier, install `Tailscale Standard (CCF)` instead - this Premium connector's network and posture pollers will return 403 on lower tiers.**", + "graphQueries": [ + { + "metricName": "Audit", + "legend": "Tailscale_Audit_CL", + "baseQuery": "Tailscale_Audit_CL" + }, + { + "metricName": "Devices", + "legend": "Tailscale_Devices_CL", + "baseQuery": "Tailscale_Devices_CL" + }, + { + "metricName": "Users", + "legend": "Tailscale_Users_CL", + "baseQuery": "Tailscale_Users_CL" + }, + { + "metricName": "Keys", + "legend": "Tailscale_Keys_CL", + "baseQuery": "Tailscale_Keys_CL" + }, + { + "metricName": "Webhooks", + "legend": "Tailscale_Webhooks_CL", + "baseQuery": "Tailscale_Webhooks_CL" + }, + { + "metricName": "Settings", + "legend": "Tailscale_Settings_CL", + "baseQuery": "Tailscale_Settings_CL" + }, + { + "metricName": "Network", + "legend": "Tailscale_Network_CL", + "baseQuery": "Tailscale_Network_CL" + }, + { + "metricName": "PostureIntegrations", + "legend": "Tailscale_PostureIntegrations_CL", + "baseQuery": "Tailscale_PostureIntegrations_CL" + }, + { + "metricName": "DNS config snapshots", + "legend": "Tailscale_Dns_CL", + "baseQuery": "Tailscale_Dns_CL" + } + ], + "dataTypes": [ + { + "name": "Tailscale_Audit_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Audit_CL')]" + }, + { + "name": "Tailscale_Devices_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Devices_CL')]" + }, + { + "name": "Tailscale_Users_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Users_CL')]" + }, + { + "name": "Tailscale_Keys_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Keys_CL')]" + }, + { + "name": "Tailscale_Webhooks_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Webhooks_CL')]" + }, + { + "name": "Tailscale_Settings_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Settings_CL')]" + }, + { + "name": "Tailscale_Network_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Network_CL')]" + }, + { + "name": "Tailscale_PostureIntegrations_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_PostureIntegrations_CL')]" + }, + { + "name": "Tailscale_Dns_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Dns_CL')]" + } + ], + "sampleQueries": [ + { + "description": "Recent network flows", + "query": "Tailscale_Network_CL\n| sort by TimeGenerated desc\n| take 100" + }, + { + "description": "Top exit-node egress destinations", + "query": "Tailscale_Network_CL\n| where array_length(ExitTraffic) > 0\n| mv-expand t = ExitTraffic\n| extend Bytes = tolong(t.txBytes) + tolong(t.rxBytes), ExitDst = tostring(t.dst)\n| summarize TotalBytes = sum(Bytes) by ExitDst\n| top 25 by TotalBytes" + }, + { + "description": "Currently configured posture integrations", + "query": "Tailscale_PostureIntegrations_CL\n| summarize arg_max(TimeGenerated, *) by IntegrationId\n| project IntegrationId, Provider, ClientId, Status" + }, + { + "description": "Device inventory by OS", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| summarize Devices = count() by Os" + }, + { + "description": "Recent audit events", + "query": "Tailscale_Audit_CL\n| sort by TimeGenerated desc\n| take 100" + }, + { + "description": "Current DNS state (all DNS config in one query)", + "query": "Tailscale_Dns_CL\n| summarize arg_max(TimeGenerated, *) by ConfigType\n| project ConfigType, Nameservers, MagicDNS, SearchPaths" + } + ], + "connectivityCriteria": [ + { + "type": "HasDataConnectors" + } + ], + "availability": { + "status": 1, + "isPreview": true + }, + "permissions": { + "resourceProvider": [ + { + "provider": "Microsoft.OperationalInsights/workspaces", + "permissionsDisplayText": "Read/Write on the workspace", + "providerDisplayName": "Workspace", + "scope": "Workspace", + "requiredPermissions": { + "write": true, + "read": true, + "delete": true + } + } + ] + }, + "instructionSteps": [ + { + "title": "Connect Tailscale (Premium)", + "description": "Generate an OAuth client at https://login.tailscale.com/admin/settings/oauth with these **Read** scopes: Logs > Configuration, Logs > Network (Premium), General > DNS, General > Users, Devices > Core, Keys > Auth Keys, Keys > Webhooks, Settings > Feature Settings (or tick `all:read`). Find your tailnet name on the Keys page.", + "instructions": [ + { + "type": "Textbox", + "parameters": { + "label": "Tailscale tailnet", + "placeholder": "tail-XXXX.ts.net", + "type": "text", + "name": "tailnetName" + } + }, + { + "type": "Textbox", + "parameters": { + "label": "OAuth Client ID", + "placeholder": "k...", + "type": "text", + "name": "clientId" + } + }, + { + "type": "Textbox", + "parameters": { + "label": "OAuth Client Secret", + "placeholder": "tskey-client-...", + "type": "password", + "name": "clientSecret" + } + }, + { + "type": "ConnectionToggleButton", + "parameters": { + "connectLabel": "Connect", + "disconnectLabel": "Disconnect" + } + } + ] + } + ], + "id": "TailscalePremiumCCF" + } + } + }, + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', variables('_dataConnectorContentIdConnectorDefinition2')))]", + "apiVersion": "2022-01-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "properties": { + "parentId": "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectorDefinitions', variables('_dataConnectorContentIdConnectorDefinition2'))]", + "contentId": "[variables('_dataConnectorContentIdConnectorDefinition2')]", + "kind": "DataConnector", + "version": "[variables('dataConnectorCCPVersion')]", + "source": { + "sourceId": "[variables('_solutionId')]", + "name": "[variables('_solutionName')]", + "kind": "Solution" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + }, + "dependencies": { + "criteria": [ + { + "version": "[variables('dataConnectorCCPVersion')]", + "contentId": "[variables('_dataConnectorContentIdConnections2')]", + "kind": "ResourcesDataConnector" + } + ] + } + } + }, + { + "name": "TailscalePremiumDCR", + "apiVersion": "2022-06-01", + "type": "Microsoft.Insights/dataCollectionRules", + "location": "[parameters('workspace-location')]", + "kind": "WorkspaceTransforms", + "properties": { + "dataCollectionEndpointId": "[variables('dataCollectionEndpointId2')]", + "streamDeclarations": { + "Custom-Tailscale_Audit_CL": { + "columns": [ + { + "name": "eventTime", + "type": "datetime" + }, + { + "name": "eventGroupID", + "type": "string" + }, + { + "name": "type", + "type": "string" + }, + { + "name": "actionDetails", + "type": "string" + }, + { + "name": "actor", + "type": "dynamic" + }, + { + "name": "action", + "type": "string" + }, + { + "name": "target", + "type": "dynamic" + }, + { + "name": "origin", + "type": "dynamic" + }, + { + "name": "new", + "type": "dynamic" + }, + { + "name": "old", + "type": "dynamic" + } + ] + }, + "Custom-Tailscale_Devices_CL": { + "columns": [ + { + "name": "id", + "type": "string" + }, + { + "name": "name", + "type": "string" + }, + { + "name": "hostname", + "type": "string" + }, + { + "name": "user", + "type": "string" + }, + { + "name": "os", + "type": "string" + }, + { + "name": "clientVersion", + "type": "string" + }, + { + "name": "updateAvailable", + "type": "boolean" + }, + { + "name": "authorized", + "type": "boolean" + }, + { + "name": "isExternal", + "type": "boolean" + }, + { + "name": "created", + "type": "datetime" + }, + { + "name": "lastSeen", + "type": "datetime" + }, + { + "name": "expires", + "type": "datetime" + }, + { + "name": "keyExpiryDisabled", + "type": "boolean" + }, + { + "name": "blocksIncomingConnections", + "type": "boolean" + }, + { + "name": "addresses", + "type": "dynamic" + }, + { + "name": "tags", + "type": "dynamic" + }, + { + "name": "enabledRoutes", + "type": "dynamic" + }, + { + "name": "advertisedRoutes", + "type": "dynamic" + }, + { + "name": "clientConnectivity", + "type": "dynamic" + }, + { + "name": "machineKey", + "type": "string" + }, + { + "name": "nodeKey", + "type": "string" + }, + { + "name": "distro", + "type": "string" + }, + { + "name": "sshEnabled", + "type": "boolean" + }, + { + "name": "connectedToControl", + "type": "boolean" + }, + { + "name": "tailnetLockKey", + "type": "string" + }, + { + "name": "tailnetLockError", + "type": "string" + } + ] + }, + "Custom-Tailscale_Users_CL": { + "columns": [ + { + "name": "id", + "type": "string" + }, + { + "name": "displayName", + "type": "string" + }, + { + "name": "loginName", + "type": "string" + }, + { + "name": "tailnetId", + "type": "string" + }, + { + "name": "type", + "type": "string" + }, + { + "name": "role", + "type": "string" + }, + { + "name": "status", + "type": "string" + }, + { + "name": "deviceCount", + "type": "int" + }, + { + "name": "created", + "type": "datetime" + }, + { + "name": "lastSeen", + "type": "datetime" + }, + { + "name": "currentlyConnected", + "type": "boolean" + }, + { + "name": "profilePicUrl", + "type": "string" + } + ] + }, + "Custom-Tailscale_Keys_CL": { + "columns": [ + { + "name": "id", + "type": "string" + }, + { + "name": "description", + "type": "string" + }, + { + "name": "userId", + "type": "string" + }, + { + "name": "created", + "type": "datetime" + }, + { + "name": "expires", + "type": "datetime" + }, + { + "name": "revoked", + "type": "datetime" + }, + { + "name": "capabilities", + "type": "dynamic" + }, + { + "name": "keyType", + "type": "string" + }, + { + "name": "expirySeconds", + "type": "int" + } + ] + }, + "Custom-Tailscale_Webhooks_CL": { + "columns": [ + { + "name": "endpointId", + "type": "string" + }, + { + "name": "endpointUrl", + "type": "string" + }, + { + "name": "providerType", + "type": "string" + }, + { + "name": "creatorLoginName", + "type": "string" + }, + { + "name": "created", + "type": "datetime" + }, + { + "name": "lastModified", + "type": "datetime" + }, + { + "name": "subscriptions", + "type": "dynamic" + } + ] + }, + "Custom-Tailscale_DnsConfig_CL": { + "columns": [ + { + "name": "dns", + "type": "dynamic" + }, + { + "name": "magicDNS", + "type": "boolean" + }, + { + "name": "searchPaths", + "type": "dynamic" + } + ] + }, + "Custom-Tailscale_Settings_CL": { + "columns": [ + { + "name": "devicesApprovalOn", + "type": "boolean" + }, + { + "name": "devicesAutoUpdatesOn", + "type": "boolean" + }, + { + "name": "devicesKeyDurationDays", + "type": "int" + }, + { + "name": "usersApprovalOn", + "type": "boolean" + }, + { + "name": "usersRoleAllowedToJoinExternalTailnets", + "type": "string" + }, + { + "name": "networkFlowLoggingOn", + "type": "boolean" + }, + { + "name": "regionalRoutingOn", + "type": "boolean" + }, + { + "name": "postureIdentityCollectionOn", + "type": "boolean" + } + ] + }, + "Custom-Tailscale_Network_CL": { + "columns": [ + { + "name": "nodeId", + "type": "string" + }, + { + "name": "logged", + "type": "datetime" + }, + { + "name": "start", + "type": "datetime" + }, + { + "name": "end", + "type": "datetime" + }, + { + "name": "srcNode", + "type": "dynamic" + }, + { + "name": "dstNodes", + "type": "dynamic" + }, + { + "name": "virtualTraffic", + "type": "dynamic" + }, + { + "name": "subnetTraffic", + "type": "dynamic" + }, + { + "name": "exitTraffic", + "type": "dynamic" + }, + { + "name": "physicalTraffic", + "type": "dynamic" + } + ] + }, + "Custom-Tailscale_PostureIntegrations_CL": { + "columns": [ + { + "name": "id", + "type": "string" + }, + { + "name": "provider", + "type": "string" + }, + { + "name": "cloudId", + "type": "string" + }, + { + "name": "clientId", + "type": "string" + }, + { + "name": "tenantId", + "type": "string" + }, + { + "name": "configOverwrites", + "type": "dynamic" + }, + { + "name": "status", + "type": "dynamic" + } + ] + } + }, + "destinations": { + "logAnalytics": [ + { + "workspaceResourceId": "[variables('workspaceResourceId')]", + "name": "sentinelWorkspace" + } + ] + }, + "dataFlows": [ + { + "streams": [ + "Custom-Tailscale_Audit_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = eventTime | extend EventTime = eventTime | extend EventGroupID = eventGroupID | extend EventType = type | extend ActionDetails = actionDetails | extend Actor = actor | extend Action = action | extend Target = target | extend Origin = origin | extend New = new | extend Old = old | project TimeGenerated, EventTime, EventGroupID, EventType, ActionDetails, Actor, Action, Target, Origin, New, Old", + "outputStream": "Custom-Tailscale_Audit_CL" + }, + { + "streams": [ + "Custom-Tailscale_Devices_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend DeviceId = id | extend DeviceName = name | extend Hostname = hostname | extend User = user | extend Os = os | extend Distro = distro | extend ClientVersion = clientVersion | extend UpdateAvailable = updateAvailable | extend Authorized = authorized | extend IsExternal = isExternal | extend Created = created | extend LastSeen = lastSeen | extend Expires = expires | extend KeyExpiryDisabled = keyExpiryDisabled | extend BlocksIncomingConnections = blocksIncomingConnections | extend SshEnabled = sshEnabled | extend ConnectedToControl = connectedToControl | extend Addresses = addresses | extend Tags = tags | extend EnabledRoutes = enabledRoutes | extend AdvertisedRoutes = advertisedRoutes | extend ClientConnectivity = clientConnectivity | extend MachineKey = machineKey | extend NodeKey = nodeKey | extend TailnetLockKey = tailnetLockKey | extend TailnetLockError = tailnetLockError | project TimeGenerated, DeviceId, DeviceName, Hostname, User, Os, Distro, ClientVersion, UpdateAvailable, Authorized, IsExternal, Created, LastSeen, Expires, KeyExpiryDisabled, BlocksIncomingConnections, SshEnabled, ConnectedToControl, Addresses, Tags, EnabledRoutes, AdvertisedRoutes, ClientConnectivity, MachineKey, NodeKey, TailnetLockKey, TailnetLockError", + "outputStream": "Custom-Tailscale_Devices_CL" + }, + { + "streams": [ + "Custom-Tailscale_Users_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend UserId = id | extend DisplayName = displayName | extend LoginName = loginName | extend TailnetId = tailnetId | extend UserType = type | extend Role = role | extend Status = status | extend DeviceCount = deviceCount | extend Created = created | extend LastSeen = lastSeen | extend CurrentlyConnected = currentlyConnected | extend ProfilePicUrl = profilePicUrl | project TimeGenerated, UserId, DisplayName, LoginName, TailnetId, UserType, Role, Status, DeviceCount, Created, LastSeen, CurrentlyConnected, ProfilePicUrl", + "outputStream": "Custom-Tailscale_Users_CL" + }, + { + "streams": [ + "Custom-Tailscale_Keys_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend KeyId = id | extend Description = description | extend UserId = userId | extend Created = created | extend Expires = expires | extend Revoked = revoked | extend Capabilities = capabilities | extend KeyType = keyType | extend ExpirySeconds = expirySeconds | project TimeGenerated, KeyId, Description, UserId, Created, Expires, Revoked, Capabilities, KeyType, ExpirySeconds", + "outputStream": "Custom-Tailscale_Keys_CL" + }, + { + "streams": [ + "Custom-Tailscale_Webhooks_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend EndpointId = endpointId | extend EndpointUrl = endpointUrl | extend ProviderType = providerType | extend CreatorLoginName = creatorLoginName | extend Created = created | extend LastModified = lastModified | extend Subscriptions = subscriptions | project TimeGenerated, EndpointId, EndpointUrl, ProviderType, CreatorLoginName, Created, LastModified, Subscriptions", + "outputStream": "Custom-Tailscale_Webhooks_CL" + }, + { + "streams": [ + "Custom-Tailscale_DnsConfig_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend ConfigType = case(isnotnull(dns), 'nameservers', isnotnull(searchPaths), 'searchpaths', 'preferences') | extend Nameservers = dns | extend MagicDNS = magicDNS | extend SearchPaths = searchPaths | project TimeGenerated, ConfigType, Nameservers, MagicDNS, SearchPaths", + "outputStream": "Custom-Tailscale_Dns_CL" + }, + { + "streams": [ + "Custom-Tailscale_Settings_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend DevicesApprovalOn = devicesApprovalOn | extend DevicesAutoUpdatesOn = devicesAutoUpdatesOn | extend DevicesKeyDurationDays = devicesKeyDurationDays | extend UsersApprovalOn = usersApprovalOn | extend UsersRoleAllowedToJoinExternalTailnets = usersRoleAllowedToJoinExternalTailnets | extend NetworkFlowLoggingOn = networkFlowLoggingOn | extend RegionalRoutingOn = regionalRoutingOn | extend PostureIdentityCollectionOn = postureIdentityCollectionOn | project TimeGenerated, DevicesApprovalOn, DevicesAutoUpdatesOn, DevicesKeyDurationDays, UsersApprovalOn, UsersRoleAllowedToJoinExternalTailnets, NetworkFlowLoggingOn, RegionalRoutingOn, PostureIdentityCollectionOn", + "outputStream": "Custom-Tailscale_Settings_CL" + }, + { + "streams": [ + "Custom-Tailscale_Network_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = logged | extend NodeId = nodeId | extend FlowStart = start | extend FlowEnd = end | extend SrcNode = srcNode | extend SrcUser = tostring(srcNode.user) | extend SrcNodeName = tostring(srcNode.name) | extend SrcOs = tostring(srcNode.os) | extend SrcTags = srcNode.tags | extend SrcAddresses = srcNode.addresses | extend DstNodes = dstNodes | extend DstCount = toint(array_length(dstNodes)) | extend DstNodeId = tostring(dstNodes[0].nodeId) | extend DstNodeName = tostring(dstNodes[0].name) | extend DstUser = tostring(dstNodes[0].user) | extend DstOs = tostring(dstNodes[0].os) | extend DstTags = dstNodes[0].tags | extend DstAddresses = dstNodes[0].addresses | extend VirtualTraffic = virtualTraffic | extend SubnetTraffic = subnetTraffic | extend ExitTraffic = exitTraffic | extend PhysicalTraffic = physicalTraffic | extend HasVirtualTraffic = array_length(virtualTraffic) > 0 | extend HasSubnetTraffic = array_length(subnetTraffic) > 0 | extend HasExitTraffic = array_length(exitTraffic) > 0 | extend HasPhysicalTraffic = array_length(physicalTraffic) > 0 | extend IsRelayed = tostring(physicalTraffic) contains '127.3.3.40' | project TimeGenerated, NodeId, FlowStart, FlowEnd, SrcNode, SrcUser, SrcNodeName, SrcOs, SrcTags, SrcAddresses, DstNodes, DstCount, DstNodeId, DstNodeName, DstUser, DstOs, DstTags, DstAddresses, VirtualTraffic, SubnetTraffic, ExitTraffic, PhysicalTraffic, HasVirtualTraffic, HasSubnetTraffic, HasExitTraffic, HasPhysicalTraffic, IsRelayed", + "outputStream": "Custom-Tailscale_Network_CL" + }, + { + "streams": [ + "Custom-Tailscale_PostureIntegrations_CL" + ], + "destinations": [ + "sentinelWorkspace" + ], + "transformKql": "source | extend TimeGenerated = now() | extend IntegrationId = id | extend Provider = provider | extend CloudId = cloudId | extend ClientId = clientId | extend TenantId_Provider = tenantId | extend ConfigOverwrites = configOverwrites | extend Status = status | project TimeGenerated, IntegrationId, Provider, CloudId, ClientId, TenantId_Provider, ConfigOverwrites, Status", + "outputStream": "Custom-Tailscale_PostureIntegrations_CL" + } + ] + } + }, + { + "name": "Tailscale_Audit_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "location": "[parameters('workspace-location')]", + "kind": null, + "properties": { + "schema": { + "name": "Tailscale_Audit_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "EventTime", + "type": "datetime" + }, + { + "name": "EventGroupID", + "type": "string" + }, + { + "name": "EventType", + "type": "string" + }, + { + "name": "ActionDetails", + "type": "string" + }, + { + "name": "Actor", + "type": "dynamic" + }, + { + "name": "Action", + "type": "string" + }, + { + "name": "Target", + "type": "dynamic" + }, + { + "name": "Origin", + "type": "dynamic" + }, + { + "name": "New", + "type": "dynamic" + }, + { + "name": "Old", + "type": "dynamic" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Devices_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "location": "[parameters('workspace-location')]", + "kind": null, + "properties": { + "schema": { + "name": "Tailscale_Devices_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "DeviceId", + "type": "string" + }, + { + "name": "DeviceName", + "type": "string" + }, + { + "name": "Hostname", + "type": "string" + }, + { + "name": "User", + "type": "string" + }, + { + "name": "Os", + "type": "string" + }, + { + "name": "ClientVersion", + "type": "string" + }, + { + "name": "UpdateAvailable", + "type": "boolean" + }, + { + "name": "Authorized", + "type": "boolean" + }, + { + "name": "IsExternal", + "type": "boolean" + }, + { + "name": "Created", + "type": "datetime" + }, + { + "name": "LastSeen", + "type": "datetime" + }, + { + "name": "Expires", + "type": "datetime" + }, + { + "name": "KeyExpiryDisabled", + "type": "boolean" + }, + { + "name": "BlocksIncomingConnections", + "type": "boolean" + }, + { + "name": "Addresses", + "type": "dynamic" + }, + { + "name": "Tags", + "type": "dynamic" + }, + { + "name": "EnabledRoutes", + "type": "dynamic" + }, + { + "name": "AdvertisedRoutes", + "type": "dynamic" + }, + { + "name": "ClientConnectivity", + "type": "dynamic" + }, + { + "name": "MachineKey", + "type": "string" + }, + { + "name": "NodeKey", + "type": "string" + }, + { + "name": "Distro", + "type": "string" + }, + { + "name": "SshEnabled", + "type": "boolean" + }, + { + "name": "ConnectedToControl", + "type": "boolean" + }, + { + "name": "TailnetLockKey", + "type": "string" + }, + { + "name": "TailnetLockError", + "type": "string" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Users_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "location": "[parameters('workspace-location')]", + "kind": null, + "properties": { + "schema": { + "name": "Tailscale_Users_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "UserId", + "type": "string" + }, + { + "name": "DisplayName", + "type": "string" + }, + { + "name": "LoginName", + "type": "string" + }, + { + "name": "TailnetId", + "type": "string" + }, + { + "name": "UserType", + "type": "string" + }, + { + "name": "Role", + "type": "string" + }, + { + "name": "Status", + "type": "string" + }, + { + "name": "DeviceCount", + "type": "int" + }, + { + "name": "Created", + "type": "datetime" + }, + { + "name": "LastSeen", + "type": "datetime" + }, + { + "name": "CurrentlyConnected", + "type": "boolean" + }, + { + "name": "ProfilePicUrl", + "type": "string" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Keys_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "location": "[parameters('workspace-location')]", + "kind": null, + "properties": { + "schema": { + "name": "Tailscale_Keys_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "KeyId", + "type": "string" + }, + { + "name": "Description", + "type": "string" + }, + { + "name": "UserId", + "type": "string" + }, + { + "name": "Created", + "type": "datetime" + }, + { + "name": "Expires", + "type": "datetime" + }, + { + "name": "Revoked", + "type": "datetime" + }, + { + "name": "Capabilities", + "type": "dynamic" + }, + { + "name": "KeyType", + "type": "string" + }, + { + "name": "ExpirySeconds", + "type": "int" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Webhooks_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "location": "[parameters('workspace-location')]", + "kind": null, + "properties": { + "schema": { + "name": "Tailscale_Webhooks_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "EndpointId", + "type": "string" + }, + { + "name": "EndpointUrl", + "type": "string" + }, + { + "name": "ProviderType", + "type": "string" + }, + { + "name": "CreatorLoginName", + "type": "string" + }, + { + "name": "Created", + "type": "datetime" + }, + { + "name": "LastModified", + "type": "datetime" + }, + { + "name": "Subscriptions", + "type": "dynamic" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Settings_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "location": "[parameters('workspace-location')]", + "kind": null, + "properties": { + "schema": { + "name": "Tailscale_Settings_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "DevicesApprovalOn", + "type": "boolean" + }, + { + "name": "DevicesAutoUpdatesOn", + "type": "boolean" + }, + { + "name": "DevicesKeyDurationDays", + "type": "int" + }, + { + "name": "UsersApprovalOn", + "type": "boolean" + }, + { + "name": "UsersRoleAllowedToJoinExternalTailnets", + "type": "string" + }, + { + "name": "NetworkFlowLoggingOn", + "type": "boolean" + }, + { + "name": "RegionalRoutingOn", + "type": "boolean" + }, + { + "name": "PostureIdentityCollectionOn", + "type": "boolean" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Network_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "location": "[parameters('workspace-location')]", + "kind": null, + "properties": { + "schema": { + "name": "Tailscale_Network_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "NodeId", + "type": "string" + }, + { + "name": "FlowStart", + "type": "datetime" + }, + { + "name": "FlowEnd", + "type": "datetime" + }, + { + "name": "SrcNode", + "type": "dynamic" + }, + { + "name": "SrcUser", + "type": "string" + }, + { + "name": "SrcNodeName", + "type": "string" + }, + { + "name": "SrcOs", + "type": "string" + }, + { + "name": "SrcTags", + "type": "dynamic" + }, + { + "name": "SrcAddresses", + "type": "dynamic" + }, + { + "name": "DstNodes", + "type": "dynamic" + }, + { + "name": "DstCount", + "type": "int" + }, + { + "name": "DstNodeId", + "type": "string" + }, + { + "name": "DstNodeName", + "type": "string" + }, + { + "name": "DstUser", + "type": "string" + }, + { + "name": "DstOs", + "type": "string" + }, + { + "name": "DstTags", + "type": "dynamic" + }, + { + "name": "DstAddresses", + "type": "dynamic" + }, + { + "name": "VirtualTraffic", + "type": "dynamic" + }, + { + "name": "SubnetTraffic", + "type": "dynamic" + }, + { + "name": "ExitTraffic", + "type": "dynamic" + }, + { + "name": "PhysicalTraffic", + "type": "dynamic" + }, + { + "name": "HasVirtualTraffic", + "type": "boolean" + }, + { + "name": "HasSubnetTraffic", + "type": "boolean" + }, + { + "name": "HasExitTraffic", + "type": "boolean" + }, + { + "name": "HasPhysicalTraffic", + "type": "boolean" + }, + { + "name": "IsRelayed", + "type": "boolean" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_PostureIntegrations_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "location": "[parameters('workspace-location')]", + "kind": null, + "properties": { + "schema": { + "name": "Tailscale_PostureIntegrations_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "IntegrationId", + "type": "string" + }, + { + "name": "Provider", + "type": "string" + }, + { + "name": "CloudId", + "type": "string" + }, + { + "name": "ClientId", + "type": "string" + }, + { + "name": "TenantId_Provider", + "type": "string" + }, + { + "name": "ConfigOverwrites", + "type": "dynamic" + }, + { + "name": "Status", + "type": "dynamic" + } + ] + }, + "retentionInDays": 90 + } + }, + { + "name": "Tailscale_Dns_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "location": "[parameters('workspace-location')]", + "kind": null, + "properties": { + "schema": { + "name": "Tailscale_Dns_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "ConfigType", + "type": "string" + }, + { + "name": "Nameservers", + "type": "dynamic" + }, + { + "name": "MagicDNS", + "type": "boolean" + }, + { + "name": "SearchPaths", + "type": "dynamic" + } + ] + }, + "retentionInDays": 90 + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "contentProductId": "[concat(take(variables('_solutionId'), 50),'-','dc','-', uniqueString(concat(variables('_solutionId'),'-','DataConnector','-',variables('_dataConnectorContentIdConnectorDefinition2'),'-', variables('dataConnectorCCPVersion'))))]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "version": "[variables('dataConnectorCCPVersion')]" + } + }, + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',variables('_dataConnectorContentIdConnectorDefinition2'))]", + "apiVersion": "2022-09-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectorDefinitions", + "location": "[parameters('workspace-location')]", + "kind": "Customizable", + "properties": { + "connectorUiConfig": { + "title": "Tailscale Premium (CCF)", + "publisher": "Community", + "logo": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIGlkPSJMYXllcl8xIiB4PSIwIiB5PSIwIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48c3R5bGU+LnN0MHtvcGFjaXR5Oi4yO2VuYWJsZS1iYWNrZ3JvdW5kOm5ld308L3N0eWxlPjxwYXRoIGQ9Ik02NS42IDEyNy43YzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45UzEwMC45IDAgNjUuNiAwIDEuOCAyOC42IDEuOCA2My45czI4LjYgNjMuOCA2My44IDYzLjgiIGNsYXNzPSJzdDAiLz48cGF0aCBkPSJNNjUuNiAzMTguMWMzNS4zIDAgNjMuOS0yOC42IDYzLjktNjMuOXMtMjguNi02My45LTYzLjktNjMuOVMxLjggMjE5IDEuOCAyNTQuMnMyOC42IDYzLjkgNjMuOCA2My45Ii8+PHBhdGggZD0iTTY1LjYgNTEyYzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45cy0yOC42LTYzLjktNjMuOS02My45LTYzLjggMjguNy02My44IDYzLjlTMzAuNCA1MTIgNjUuNiA1MTIiIGNsYXNzPSJzdDAiLz48cGF0aCBkPSJNMjU3LjIgMzE4LjFjMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlzLTI4LjYtNjMuOS02My45LTYzLjktNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjkgNjMuOSA2My45bTAgMTkzLjljMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlzLTI4LjYtNjMuOS02My45LTYzLjktNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjkgNjMuOSA2My45Ii8+PHBhdGggZD0iTTI1Ny4yIDEyNy43YzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45UzI5Mi41IDAgMjU3LjIgMHMtNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjggNjMuOSA2My44bTE4OS4yIDBjMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlTNDgxLjYgMCA0NDYuNCAwYy0zNS4zIDAtNjMuOSAyOC42LTYzLjkgNjMuOXMyOC42IDYzLjggNjMuOSA2My44IiBjbGFzcz0ic3QwIi8+PHBhdGggZD0iTTQ0Ni40IDMxOC4xYzM1LjMgMCA2My45LTI4LjYgNjMuOS02My45cy0yOC42LTYzLjktNjMuOS02My45LTYzLjkgMjguNi02My45IDYzLjkgMjguNiA2My45IDYzLjkgNjMuOSIvPjxwYXRoIGQ9Ik00NDYuNCA1MTJjMzUuMyAwIDYzLjktMjguNiA2My45LTYzLjlzLTI4LjYtNjMuOS02My45LTYzLjktNjMuOSAyOC42LTYzLjkgNjMuOSAyOC42IDYzLjkgNjMuOSA2My45IiBjbGFzcz0ic3QwIi8+PC9zdmc+", + "descriptionMarkdown": "Comprehensive Tailscale telemetry for **Premium and Enterprise** tier tailnets. Polls every endpoint the Standard connector polls, **plus** Premium-only network flow logs and posture-integration inventory. Eleven endpoints in one Connect:\n\n- `/logging/configuration` - configuration audit events\n- `/logging/network` - **Premium** network flow logs (per-node traffic with src/dst/protocol/bytes)\n- `/devices` - device inventory\n- `/users` - user inventory\n- `/keys?all=true` - auth keys + API tokens + OAuth clients\n- `/webhooks` - webhook configuration\n- `/dns/nameservers`, `/dns/preferences`, `/dns/searchpaths` - DNS state (merged into single `Tailscale_Dns_CL` table with `ConfigType` discriminator)\n- `/settings` - tailnet settings flags\n- `/posture/integrations` - **Premium** MDM/EDR integration inventory (Jamf, Kandji, Intune, Kolide, Microsoft Defender for Endpoint, CrowdStrike Falcon, SentinelOne, etc.)\n\n**OAuth scopes required:** `logs:configuration:read`, `logs:network:read`, `devices:core:read`, `users:read`, `auth_keys:read`, `webhooks:read`, `dns:read`, `feature_settings:read` (or the bundled `all:read`).\n\n**If your tailnet is Personal (Free) or Standard tier, install `Tailscale Standard (CCF)` instead - this Premium connector's network and posture pollers will return 403 on lower tiers.**", + "graphQueries": [ + { + "metricName": "Audit", + "legend": "Tailscale_Audit_CL", + "baseQuery": "Tailscale_Audit_CL" + }, + { + "metricName": "Devices", + "legend": "Tailscale_Devices_CL", + "baseQuery": "Tailscale_Devices_CL" + }, + { + "metricName": "Users", + "legend": "Tailscale_Users_CL", + "baseQuery": "Tailscale_Users_CL" + }, + { + "metricName": "Keys", + "legend": "Tailscale_Keys_CL", + "baseQuery": "Tailscale_Keys_CL" + }, + { + "metricName": "Webhooks", + "legend": "Tailscale_Webhooks_CL", + "baseQuery": "Tailscale_Webhooks_CL" + }, + { + "metricName": "Settings", + "legend": "Tailscale_Settings_CL", + "baseQuery": "Tailscale_Settings_CL" + }, + { + "metricName": "Network", + "legend": "Tailscale_Network_CL", + "baseQuery": "Tailscale_Network_CL" + }, + { + "metricName": "PostureIntegrations", + "legend": "Tailscale_PostureIntegrations_CL", + "baseQuery": "Tailscale_PostureIntegrations_CL" + }, + { + "metricName": "DNS config snapshots", + "legend": "Tailscale_Dns_CL", + "baseQuery": "Tailscale_Dns_CL" + } + ], + "dataTypes": [ + { + "name": "Tailscale_Audit_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Audit_CL')]" + }, + { + "name": "Tailscale_Devices_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Devices_CL')]" + }, + { + "name": "Tailscale_Users_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Users_CL')]" + }, + { + "name": "Tailscale_Keys_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Keys_CL')]" + }, + { + "name": "Tailscale_Webhooks_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Webhooks_CL')]" + }, + { + "name": "Tailscale_Settings_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Settings_CL')]" + }, + { + "name": "Tailscale_Network_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Network_CL')]" + }, + { + "name": "Tailscale_PostureIntegrations_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_PostureIntegrations_CL')]" + }, + { + "name": "Tailscale_Dns_CL", + "lastDataReceivedQuery": "[format('{0} | summarize Time=max(TimeGenerated)', 'Tailscale_Dns_CL')]" + } + ], + "sampleQueries": [ + { + "description": "Recent network flows", + "query": "Tailscale_Network_CL\n| sort by TimeGenerated desc\n| take 100" + }, + { + "description": "Top exit-node egress destinations", + "query": "Tailscale_Network_CL\n| where array_length(ExitTraffic) > 0\n| mv-expand t = ExitTraffic\n| extend Bytes = tolong(t.txBytes) + tolong(t.rxBytes), ExitDst = tostring(t.dst)\n| summarize TotalBytes = sum(Bytes) by ExitDst\n| top 25 by TotalBytes" + }, + { + "description": "Currently configured posture integrations", + "query": "Tailscale_PostureIntegrations_CL\n| summarize arg_max(TimeGenerated, *) by IntegrationId\n| project IntegrationId, Provider, ClientId, Status" + }, + { + "description": "Device inventory by OS", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| summarize Devices = count() by Os" + }, + { + "description": "Recent audit events", + "query": "Tailscale_Audit_CL\n| sort by TimeGenerated desc\n| take 100" + }, + { + "description": "Current DNS state (all DNS config in one query)", + "query": "Tailscale_Dns_CL\n| summarize arg_max(TimeGenerated, *) by ConfigType\n| project ConfigType, Nameservers, MagicDNS, SearchPaths" + } + ], + "connectivityCriteria": [ + { + "type": "HasDataConnectors" + } + ], + "availability": { + "status": 1, + "isPreview": true + }, + "permissions": { + "resourceProvider": [ + { + "provider": "Microsoft.OperationalInsights/workspaces", + "permissionsDisplayText": "Read/Write on the workspace", + "providerDisplayName": "Workspace", + "scope": "Workspace", + "requiredPermissions": { + "write": true, + "read": true, + "delete": true + } + } + ] + }, + "instructionSteps": [ + { + "title": "Connect Tailscale (Premium)", + "description": "Generate an OAuth client at https://login.tailscale.com/admin/settings/oauth with these **Read** scopes: Logs > Configuration, Logs > Network (Premium), General > DNS, General > Users, Devices > Core, Keys > Auth Keys, Keys > Webhooks, Settings > Feature Settings (or tick `all:read`). Find your tailnet name on the Keys page.", + "instructions": [ + { + "type": "Textbox", + "parameters": { + "label": "Tailscale tailnet", + "placeholder": "tail-XXXX.ts.net", + "type": "text", + "name": "tailnetName" + } + }, + { + "type": "Textbox", + "parameters": { + "label": "OAuth Client ID", + "placeholder": "k...", + "type": "text", + "name": "clientId" + } + }, + { + "type": "Textbox", + "parameters": { + "label": "OAuth Client Secret", + "placeholder": "tskey-client-...", + "type": "password", + "name": "clientSecret" + } + }, + { + "type": "ConnectionToggleButton", + "parameters": { + "connectLabel": "Connect", + "disconnectLabel": "Disconnect" + } + } + ] + } + ], + "id": "TailscalePremiumCCF" + } + } + }, + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', variables('_dataConnectorContentIdConnectorDefinition2')))]", + "apiVersion": "2022-01-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "properties": { + "parentId": "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectorDefinitions', variables('_dataConnectorContentIdConnectorDefinition2'))]", + "contentId": "[variables('_dataConnectorContentIdConnectorDefinition2')]", + "kind": "DataConnector", + "version": "[variables('dataConnectorCCPVersion')]", + "source": { + "sourceId": "[variables('_solutionId')]", + "name": "[variables('_solutionName')]", + "kind": "Solution" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + }, + "dependencies": { + "criteria": [ + { + "version": "[variables('dataConnectorCCPVersion')]", + "contentId": "[variables('_dataConnectorContentIdConnections2')]", + "kind": "ResourcesDataConnector" + } + ] + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/', variables('dataConnectorTemplateNameConnections2'), variables('dataConnectorCCPVersion'))]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "contentId": "[variables('_dataConnectorContentIdConnections2')]", + "displayName": "Tailscale Premium (CCF)", + "contentKind": "ResourcesDataConnector", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('dataConnectorCCPVersion')]", + "parameters": { + "guidValue": { + "defaultValue": "[[newGuid()]", + "type": "securestring" + }, + "innerWorkspace": { + "defaultValue": "[parameters('workspace')]", + "type": "securestring" + }, + "connectorDefinitionName": { + "defaultValue": "Tailscale Premium (CCF)", + "type": "securestring", + "minLength": 1 + }, + "workspace": { + "defaultValue": "[parameters('workspace')]", + "type": "securestring" + }, + "dcrConfig": { + "defaultValue": { + "dataCollectionEndpoint": "data collection Endpoint", + "dataCollectionRuleImmutableId": "data collection rule immutableId" + }, + "type": "object" + }, + "tailnetName": { + "defaultValue": "tailnetName", + "type": "securestring", + "minLength": 1 + }, + "clientId": { + "defaultValue": "clientId", + "type": "securestring", + "minLength": 1 + }, + "clientSecret": { + "defaultValue": "clientSecret", + "type": "securestring", + "minLength": 1 + } + }, + "variables": { + "_dataConnectorContentIdConnections2": "[variables('_dataConnectorContentIdConnections2')]" + }, + "resources": [ + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', variables('_dataConnectorContentIdConnections2')))]", + "apiVersion": "2022-01-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "properties": { + "parentId": "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectors', variables('_dataConnectorContentIdConnections2'))]", + "contentId": "[variables('_dataConnectorContentIdConnections2')]", + "kind": "ResourcesDataConnector", + "version": "[variables('dataConnectorCCPVersion')]", + "source": { + "sourceId": "[variables('_solutionId')]", + "name": "[variables('_solutionName')]", + "kind": "Solution" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + }, + { + "name": "[[concat(parameters('innerWorkspace'),'/Microsoft.SecurityInsights/', 'TailscalePremiumConfigAuditPoller', parameters('guidValue'))]", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscalePremiumCCF", + "dataType": "Tailscale_Audit_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Audit_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/logging/configuration')]", + "httpMethod": "GET", + "queryWindowInMin": 5, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + }, + "queryTimeFormat": "yyyy-MM-ddTHH:mm:ssZ", + "startTimeAttributeName": "start", + "endTimeAttributeName": "end" + }, + "response": { + "eventsJsonPaths": [ + "$.logs" + ] + }, + "isActive": true + } + }, + { + "name": "[[concat(parameters('innerWorkspace'),'/Microsoft.SecurityInsights/', 'TailscalePremiumNetworkPoller', parameters('guidValue'))]", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscalePremiumCCF", + "dataType": "Tailscale_Network_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Network_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/logging/network')]", + "httpMethod": "GET", + "queryWindowInMin": 5, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + }, + "queryTimeFormat": "yyyy-MM-ddTHH:mm:ssZ", + "startTimeAttributeName": "start", + "endTimeAttributeName": "end" + }, + "response": { + "eventsJsonPaths": [ + "$.logs" + ] + }, + "isActive": true + } + }, + { + "name": "[[concat(parameters('innerWorkspace'),'/Microsoft.SecurityInsights/', 'TailscalePremiumDevicesPoller', parameters('guidValue'))]", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscalePremiumCCF", + "dataType": "Tailscale_Devices_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Devices_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/devices?fields=all')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$.devices" + ] + }, + "isActive": true + } + }, + { + "name": "[[concat(parameters('innerWorkspace'),'/Microsoft.SecurityInsights/', 'TailscalePremiumUsersPoller', parameters('guidValue'))]", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscalePremiumCCF", + "dataType": "Tailscale_Users_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Users_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/users')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$.users" + ] + }, + "isActive": true + } + }, + { + "name": "[[concat(parameters('innerWorkspace'),'/Microsoft.SecurityInsights/', 'TailscalePremiumKeysPoller', parameters('guidValue'))]", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscalePremiumCCF", + "dataType": "Tailscale_Keys_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Keys_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/keys?all=true')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$.keys" + ] + }, + "isActive": true + } + }, + { + "name": "[[concat(parameters('innerWorkspace'),'/Microsoft.SecurityInsights/', 'TailscalePremiumWebhooksPoller', parameters('guidValue'))]", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscalePremiumCCF", + "dataType": "Tailscale_Webhooks_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Webhooks_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/webhooks')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$.webhooks" + ] + }, + "isActive": true + } + }, + { + "name": "[[concat(parameters('innerWorkspace'),'/Microsoft.SecurityInsights/', 'TailscalePremiumDnsNameserversPoller', parameters('guidValue'))]", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscalePremiumCCF", + "dataType": "Tailscale_Dns_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_DnsConfig_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/dns/nameservers')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$" + ] + }, + "isActive": true + } + }, + { + "name": "[[concat(parameters('innerWorkspace'),'/Microsoft.SecurityInsights/', 'TailscalePremiumDnsPreferencesPoller', parameters('guidValue'))]", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscalePremiumCCF", + "dataType": "Tailscale_Dns_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_DnsConfig_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/dns/preferences')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$" + ] + }, + "isActive": true + } + }, + { + "name": "[[concat(parameters('innerWorkspace'),'/Microsoft.SecurityInsights/', 'TailscalePremiumDnsSearchPathsPoller', parameters('guidValue'))]", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscalePremiumCCF", + "dataType": "Tailscale_Dns_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_DnsConfig_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/dns/searchpaths')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$" + ] + }, + "isActive": true + } + }, + { + "name": "[[concat(parameters('innerWorkspace'),'/Microsoft.SecurityInsights/', 'TailscalePremiumSettingsPoller', parameters('guidValue'))]", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscalePremiumCCF", + "dataType": "Tailscale_Settings_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_Settings_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/settings')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$" + ] + }, + "isActive": true + } + }, + { + "name": "[[concat(parameters('innerWorkspace'),'/Microsoft.SecurityInsights/', 'TailscalePremiumPostureIntegrationsPoller', parameters('guidValue'))]", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "TailscalePremiumCCF", + "dataType": "Tailscale_PostureIntegrations_CL", + "dcrConfig": { + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]", + "streamName": "Custom-Tailscale_PostureIntegrations_CL" + }, + "auth": { + "type": "OAuth2", + "ClientId": "[[parameters('clientId')]", + "ClientSecret": "[[parameters('clientSecret')]", + "GrantType": "client_credentials", + "TokenEndpoint": "https://api.tailscale.com/api/v2/oauth/token", + "TokenEndpointHeaders": { + "Content-Type": "application/x-www-form-urlencoded" + } + }, + "request": { + "apiEndpoint": "[[concat('https://api.tailscale.com/api/v2/tailnet/', parameters('tailnetName'), '/posture/integrations')]", + "httpMethod": "GET", + "queryWindowInMin": 60, + "rateLimitQps": 1, + "retryCount": 3, + "timeoutInSeconds": 60, + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$.integrations" + ] + }, + "isActive": true + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "contentProductId": "[concat(take(variables('_solutionId'), 50),'-','rdc','-', uniqueString(concat(variables('_solutionId'),'-','ResourcesDataConnector','-',variables('_dataConnectorContentIdConnections2'),'-', variables('dataConnectorCCPVersion'))))]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "version": "[variables('dataConnectorCCPVersion')]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject1').analyticRuleTemplateSpecName1]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleNewAPIaccesstokenorOAuthclientcreated_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject1').analyticRuleVersion1]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject1')._analyticRulecontentId1]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Identifies when a new API access token or OAuth client is created in the tailnet. These grant programmatic access - verify the actor and intent.", + "displayName": "Tailscale: New API access token or OAuth client created", + "enabled": false, + "query": "Tailscale_Audit_CL\n | where Action == \"CREATE\"\n | where tostring(Target.type) in (\"API_KEY\", \"OAUTH_CLIENT\")\n | extend ActorLogin = tostring(Actor.loginName)\n | extend TargetName = tostring(Target.name)\n | extend TargetId = tostring(Target.id)\n | project TimeGenerated, ActorLogin, Action, TargetName, TargetId, Origin, New\n", + "queryFrequency": "PT15M", + "queryPeriod": "PT15M", + "severity": "Medium", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Audit_CL" + ], + "connectorId": "TailscaleCCF" + } + ], + "tactics": [ + "Persistence", + "CredentialAccess" + ], + "techniques": [ + "T1098", + "T1136" + ], + "entityMappings": [ + { + "entityType": "Account", + "fieldMappings": [ + { + "identifier": "FullName", + "columnName": "ActorLogin" + } + ] + } + ], + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "lookbackDuration": "5h", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject1').analyticRuleId1,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 1", + "parentId": "[variables('analyticRuleObject1').analyticRuleId1]", + "contentId": "[variables('analyticRuleObject1')._analyticRulecontentId1]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject1').analyticRuleVersion1]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject1')._analyticRulecontentId1]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale: New API access token or OAuth client created", + "contentProductId": "[variables('analyticRuleObject1')._analyticRulecontentProductId1]", + "id": "[variables('analyticRuleObject1')._analyticRulecontentProductId1]", + "version": "[variables('analyticRuleObject1').analyticRuleVersion1]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject2').analyticRuleTemplateSpecName2]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleOAuthClientCreatedWithWriteScopes_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject2').analyticRuleVersion2]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject2')._analyticRulecontentId2]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Identifies creation of a Tailscale OAuth client or API access key whose granted scopes include WRITE permissions (anything matching :write). Tokens with write scopes are high-value adversary targets.", + "displayName": "Tailscale: OAuth client or API key created with write scopes", + "enabled": false, + "query": "Tailscale_Audit_CL\n| where EventType == \"CONFIG\"\n| where Action == \"CREATE\"\n| where tostring(Target.type) in (\"API_KEY\", \"OAUTH_CLIENT\")\n| extend Scopes = extract(@\"scopes\\s*-\\s*(.+)$\", 1, ActionDetails)\n| where Scopes contains \":write\"\n| extend WriteScopes = extract_all(@\"([a-zA-Z_]+:write)\", Scopes)\n| extend ActorLogin = tostring(Actor.loginName)\n| extend ActorType = tostring(Actor.type)\n| extend TargetName = tostring(Target.name)\n| extend TargetId = tostring(Target.id)\n| extend TargetType = tostring(Target.type)\n| project TimeGenerated, ActorLogin, ActorType, Action, TargetType, TargetName, TargetId, WriteScopes, Scopes, Origin, ActionDetails\n", + "queryFrequency": "PT15M", + "queryPeriod": "PT15M", + "severity": "High", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Audit_CL" + ], + "connectorId": "TailscaleCCF" + }, + { + "dataTypes": [ + "Tailscale_Audit_CL" + ], + "connectorId": "TailscalePremiumCCF" + } + ], + "tactics": [ + "Persistence", + "PrivilegeEscalation" + ], + "techniques": [ + "T1098", + "T1136" + ], + "entityMappings": [ + { + "entityType": "Account", + "fieldMappings": [ + { + "identifier": "FullName", + "columnName": "ActorLogin" + } + ] + } + ], + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "lookbackDuration": "5h", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject2').analyticRuleId2,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 2", + "parentId": "[variables('analyticRuleObject2').analyticRuleId2]", + "contentId": "[variables('analyticRuleObject2')._analyticRulecontentId2]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject2').analyticRuleVersion2]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject2')._analyticRulecontentId2]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale: OAuth client or API key created with write scopes", + "contentProductId": "[variables('analyticRuleObject2')._analyticRulecontentProductId2]", + "id": "[variables('analyticRuleObject2')._analyticRulecontentProductId2]", + "version": "[variables('analyticRuleObject2').analyticRuleVersion2]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject3').analyticRuleTemplateSpecName3]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscalePolicyfileACLmodified_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject3').analyticRuleVersion3]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject3')._analyticRulecontentId3]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Identifies when the tailnet ACL/policy file is modified. Review the diff - incorrect ACLs can silently expand blast radius across the tailnet.", + "displayName": "Tailscale: Policy file (ACL) modified", + "enabled": false, + "query": "Tailscale_Audit_CL\n| where Action == \"UPDATE\"\n| where tostring(Target.type) == \"TAILNET\"\n| where tostring(Target.property) == \"ACL\"\n| extend ActorLogin = tostring(Actor.loginName)\n| project TimeGenerated, ActorLogin, Action, Target, Origin, Old, New\n", + "queryFrequency": "PT15M", + "queryPeriod": "PT15M", + "severity": "Medium", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Audit_CL" + ], + "connectorId": "TailscaleCCF" + } + ], + "tactics": [ + "DefenseEvasion", + "Persistence" + ], + "techniques": [ + "T1556" + ], + "entityMappings": [ + { + "entityType": "Account", + "fieldMappings": [ + { + "identifier": "FullName", + "columnName": "ActorLogin" + } + ] + } + ], + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "lookbackDuration": "5h", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject3').analyticRuleId3,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 3", + "parentId": "[variables('analyticRuleObject3').analyticRuleId3]", + "contentId": "[variables('analyticRuleObject3')._analyticRulecontentId3]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject3').analyticRuleVersion3]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject3')._analyticRulecontentId3]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale: Policy file (ACL) modified", + "contentProductId": "[variables('analyticRuleObject3')._analyticRulecontentProductId3]", + "id": "[variables('analyticRuleObject3')._analyticRulecontentProductId3]", + "version": "[variables('analyticRuleObject3').analyticRuleVersion3]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject4').analyticRuleTemplateSpecName4]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleAuthkeycreated_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject4').analyticRuleVersion4]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject4')._analyticRulecontentId4]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Identifies when a new Tailscale auth key is generated. Auth keys allow unattended device enrollment into the tailnet - confirm it was expected and revoke if not.", + "displayName": "Tailscale: Auth key created", + "enabled": false, + "query": "Tailscale_Audit_CL\n | where Action == \"CREATE\"\n | where tostring(Target.type) == \"AUTH_KEY\"\n | extend ActorLogin = tostring(Actor.loginName)\n | extend KeyDescription = tostring(New.description)\n | extend Reusable = tostring(New.reusable)\n | extend Ephemeral = tostring(New.ephemeral)\n | project TimeGenerated, ActorLogin, KeyDescription, Reusable, Ephemeral, Origin, New\n", + "queryFrequency": "PT15M", + "queryPeriod": "PT15M", + "severity": "Low", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Audit_CL" + ], + "connectorId": "TailscaleCCF" + } + ], + "tactics": [ + "Persistence" + ], + "techniques": [ + "T1098" + ], + "entityMappings": [ + { + "entityType": "Account", + "fieldMappings": [ + { + "identifier": "FullName", + "columnName": "ActorLogin" + } + ] + } + ], + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "lookbackDuration": "5h", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject4').analyticRuleId4,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 4", + "parentId": "[variables('analyticRuleObject4').analyticRuleId4]", + "contentId": "[variables('analyticRuleObject4')._analyticRulecontentId4]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject4').analyticRuleVersion4]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject4')._analyticRulecontentId4]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale: Auth key created", + "contentProductId": "[variables('analyticRuleObject4')._analyticRulecontentProductId4]", + "id": "[variables('analyticRuleObject4')._analyticRulecontentProductId4]", + "version": "[variables('analyticRuleObject4').analyticRuleVersion4]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject5').analyticRuleTemplateSpecName5]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleExitnodeadvertisedorapproved_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject5').analyticRuleVersion5]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject5')._analyticRulecontentId5]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Identifies when a device starts advertising itself as an exit node, or when an admin approves one. Validate the device and operator - rogue exit nodes can intercept tailnet egress.", + "displayName": "Tailscale: Exit node advertised or approved", + "enabled": false, + "query": "Tailscale_Audit_CL\n | where Action contains \"EXIT\" or tostring(New.advertisedExitNode) == \"true\" or tostring(New.allowedExitNode) == \"true\"\n | extend ActorLogin = tostring(Actor.loginName)\n | extend NodeName = tostring(Target.name)\n | extend NodeId = tostring(Target.id)\n | project TimeGenerated, ActorLogin, Action, NodeName, NodeId, Origin, New, Old\n", + "queryFrequency": "PT30M", + "queryPeriod": "PT30M", + "severity": "Low", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Audit_CL" + ], + "connectorId": "TailscaleCCF" + } + ], + "tactics": [ + "CommandAndControl", + "Exfiltration" + ], + "techniques": [ + "T1090" + ], + "entityMappings": [ + { + "entityType": "Account", + "fieldMappings": [ + { + "identifier": "FullName", + "columnName": "ActorLogin" + } + ] + }, + { + "entityType": "Host", + "fieldMappings": [ + { + "identifier": "HostName", + "columnName": "NodeName" + } + ] + } + ], + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "lookbackDuration": "5h", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject5').analyticRuleId5,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 5", + "parentId": "[variables('analyticRuleObject5').analyticRuleId5]", + "contentId": "[variables('analyticRuleObject5')._analyticRulecontentId5]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject5').analyticRuleVersion5]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject5')._analyticRulecontentId5]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale: Exit node advertised or approved", + "contentProductId": "[variables('analyticRuleObject5')._analyticRulecontentProductId5]", + "id": "[variables('analyticRuleObject5')._analyticRulecontentProductId5]", + "version": "[variables('analyticRuleObject5').analyticRuleVersion5]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject6').analyticRuleTemplateSpecName6]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleMasscredentialrevocationinshortwindow_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject6').analyticRuleVersion6]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject6')._analyticRulecontentId6]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Identifies when five or more API keys, OAuth clients, or auth keys are revoked or deleted within one hour. May be routine rotation, or a typical cleanup pattern after credential compromise.", + "displayName": "Tailscale: Mass credential revocation in short window", + "enabled": false, + "query": "Tailscale_Audit_CL\n | where Action in (\"REVOKE\", \"DELETE\")\n | where tostring(Target.type) in (\"API_KEY\", \"OAUTH_CLIENT\", \"AUTH_KEY\")\n | extend ActorLogin = tostring(Actor.loginName)\n | summarize\n RevokedCount = count(),\n TargetTypes = make_set(tostring(Target.type)),\n TargetIds = make_set(tostring(Target.id)),\n FirstEvent = min(TimeGenerated),\n LastEvent = max(TimeGenerated)\n by ActorLogin, bin(TimeGenerated, 1h)\n | where RevokedCount >= 5\n | project TimeGenerated = LastEvent, ActorLogin, RevokedCount, TargetTypes, TargetIds, FirstEvent, LastEvent\n", + "queryFrequency": "PT1H", + "queryPeriod": "PT1H", + "severity": "High", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Audit_CL" + ], + "connectorId": "TailscaleCCF" + } + ], + "tactics": [ + "DefenseEvasion", + "Impact" + ], + "techniques": [ + "T1070" + ], + "entityMappings": [ + { + "entityType": "Account", + "fieldMappings": [ + { + "identifier": "FullName", + "columnName": "ActorLogin" + } + ] + } + ], + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "lookbackDuration": "5h", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject6').analyticRuleId6,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 6", + "parentId": "[variables('analyticRuleObject6').analyticRuleId6]", + "contentId": "[variables('analyticRuleObject6')._analyticRulecontentId6]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject6').analyticRuleVersion6]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject6')._analyticRulecontentId6]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale: Mass credential revocation in short window", + "contentProductId": "[variables('analyticRuleObject6')._analyticRulecontentProductId6]", + "id": "[variables('analyticRuleObject6')._analyticRulecontentProductId6]", + "version": "[variables('analyticRuleObject6').analyticRuleVersion6]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject7').analyticRuleTemplateSpecName7]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleDeviceKeyExpiringSoon_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject7').analyticRuleVersion7]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject7')._analyticRulecontentId7]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Identifies tailnet devices whose machine key expires within the next 7 days and where key expiry is not disabled. Surface proactively so renewal can be scheduled rather than forced during an outage.", + "displayName": "Tailscale: Device key expiring within 7 days", + "enabled": false, + "query": "Tailscale_Devices_CL\n| where TimeGenerated > ago(6h)\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where KeyExpiryDisabled == false\n| where isnotnull(Expires)\n| where Expires between (now() .. now() + 7d)\n| extend DaysToExpiry = datetime_diff('day', Expires, now())\n| project TimeGenerated, DeviceName, Hostname, User, Os, DaysToExpiry, Expires, LastSeen\n", + "queryFrequency": "PT6H", + "queryPeriod": "PT6H", + "severity": "Medium", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Devices_CL" + ], + "connectorId": "TailscaleCCF" + } + ], + "tactics": [ + "InitialAccess" + ], + "techniques": [ + "T1078" + ], + "entityMappings": [ + { + "entityType": "Host", + "fieldMappings": [ + { + "identifier": "HostName", + "columnName": "Hostname" + } + ] + }, + { + "entityType": "Account", + "fieldMappings": [ + { + "identifier": "FullName", + "columnName": "User" + } + ] + } + ], + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "lookbackDuration": "1d", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject7').analyticRuleId7,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 7", + "parentId": "[variables('analyticRuleObject7').analyticRuleId7]", + "contentId": "[variables('analyticRuleObject7')._analyticRulecontentId7]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject7').analyticRuleVersion7]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject7')._analyticRulecontentId7]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale: Device key expiring within 7 days", + "contentProductId": "[variables('analyticRuleObject7')._analyticRulecontentProductId7]", + "id": "[variables('analyticRuleObject7')._analyticRulecontentProductId7]", + "version": "[variables('analyticRuleObject7').analyticRuleVersion7]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject8').analyticRuleTemplateSpecName8]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleDeviceAdvertisingSubnetRoutes_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject8').analyticRuleVersion8]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject8')._analyticRulecontentId8]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "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.", + "displayName": "Tailscale: Device started advertising subnet routes", + "enabled": false, + "query": "let recent =\n Tailscale_Devices_CL\n | where TimeGenerated > ago(1h)\n | summarize arg_max(TimeGenerated, *) by DeviceId\n | where array_length(AdvertisedRoutes) > 0;\nlet baseline =\n Tailscale_Devices_CL\n | where TimeGenerated between (ago(1d + 1h) .. ago(1h))\n | summarize arg_max(TimeGenerated, *) by DeviceId\n | where array_length(AdvertisedRoutes) > 0\n | distinct DeviceId;\nrecent\n| join kind=leftanti baseline on DeviceId\n| project TimeGenerated, DeviceId, DeviceName, Hostname, User, AdvertisedRoutes, EnabledRoutes, LastSeen\n", + "queryFrequency": "PT1H", + "queryPeriod": "P1D", + "severity": "Medium", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Devices_CL" + ], + "connectorId": "TailscaleCCF" + } + ], + "tactics": [ + "LateralMovement", + "Persistence" + ], + "techniques": [ + "T1021", + "T1556" + ], + "entityMappings": [ + { + "entityType": "Host", + "fieldMappings": [ + { + "identifier": "HostName", + "columnName": "Hostname" + } + ] + }, + { + "entityType": "Account", + "fieldMappings": [ + { + "identifier": "FullName", + "columnName": "User" + } + ] + } + ], + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "lookbackDuration": "1d", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject8').analyticRuleId8,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 8", + "parentId": "[variables('analyticRuleObject8').analyticRuleId8]", + "contentId": "[variables('analyticRuleObject8')._analyticRulecontentId8]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject8').analyticRuleVersion8]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject8')._analyticRulecontentId8]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale: Device started advertising subnet routes", + "contentProductId": "[variables('analyticRuleObject8')._analyticRulecontentProductId8]", + "id": "[variables('analyticRuleObject8')._analyticRulecontentProductId8]", + "version": "[variables('analyticRuleObject8').analyticRuleVersion8]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject9').analyticRuleTemplateSpecName9]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleUserRoleElevated_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject9').analyticRuleVersion9]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject9')._analyticRulecontentId9]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Identifies when a user's tailnet role changes from a lower-privilege role to admin, network-admin, or owner between consecutive snapshots. Privilege escalation is a high-value attacker objective and warrants prompt review.", + "displayName": "Tailscale: User role elevated to admin or owner", + "enabled": false, + "query": "let elevated = dynamic([\"admin\", \"network-admin\", \"owner\"]);\nlet recent =\n Tailscale_Users_CL\n | where TimeGenerated > ago(1h)\n | summarize arg_max(TimeGenerated, *) by UserId\n | where Role in (elevated)\n | project UserId, LoginName, RoleNow = Role;\nlet prior =\n Tailscale_Users_CL\n | where TimeGenerated between (ago(2d) .. ago(1h))\n | summarize arg_max(TimeGenerated, *) by UserId\n | project UserId, RolePrior = Role;\nrecent\n| join kind=inner prior on UserId\n| where RoleNow != RolePrior\n| where RolePrior !in (elevated)\n| project TimeGenerated = now(), UserId, LoginName, RolePrior, RoleNow\n", + "queryFrequency": "PT1H", + "queryPeriod": "P2D", + "severity": "High", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Users_CL" + ], + "connectorId": "TailscaleCCF" + } + ], + "tactics": [ + "PrivilegeEscalation", + "Persistence" + ], + "techniques": [ + "T1078", + "T1098" + ], + "entityMappings": [ + { + "entityType": "Account", + "fieldMappings": [ + { + "identifier": "FullName", + "columnName": "LoginName" + } + ] + } + ], + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "lookbackDuration": "1d", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject9').analyticRuleId9,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 9", + "parentId": "[variables('analyticRuleObject9').analyticRuleId9]", + "contentId": "[variables('analyticRuleObject9')._analyticRulecontentId9]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject9').analyticRuleVersion9]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject9')._analyticRulecontentId9]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale: User role elevated to admin or owner", + "contentProductId": "[variables('analyticRuleObject9')._analyticRulecontentProductId9]", + "id": "[variables('analyticRuleObject9')._analyticRulecontentProductId9]", + "version": "[variables('analyticRuleObject9').analyticRuleVersion9]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject10').analyticRuleTemplateSpecName10]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleSplitDnsModified_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject10').analyticRuleVersion10]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject10')._analyticRulecontentId10]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Identifies when the tailnet split-DNS configuration is modified. Split-DNS overrides per-domain resolution within the tailnet - an attacker adding a new domain mapping or changing the resolver IP can hijack DNS for that domain.", + "displayName": "Tailscale: Split-DNS configuration modified", + "enabled": false, + "query": "Tailscale_Audit_CL\n| where Action == \"UPDATE\"\n| where tostring(Target.type) == \"TAILNET\"\n| where tostring(Target.property) == \"DNS_SPLIT_DNS\"\n| extend ActorLogin = tostring(Actor.loginName)\n| extend OldDomains = bag_keys(Old)\n| extend NewDomains = bag_keys(New)\n| extend AddedDomains = set_difference(NewDomains, OldDomains)\n| extend RemovedDomains = set_difference(OldDomains, NewDomains)\n| project TimeGenerated, ActorLogin, AddedDomains, RemovedDomains, Old, New, Origin\n", + "queryFrequency": "PT15M", + "queryPeriod": "PT15M", + "severity": "High", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Audit_CL" + ], + "connectorId": "TailscaleCCF" + } + ], + "tactics": [ + "DefenseEvasion", + "CommandAndControl" + ], + "techniques": [ + "T1556", + "T1568" + ], + "entityMappings": [ + { + "entityType": "Account", + "fieldMappings": [ + { + "identifier": "FullName", + "columnName": "ActorLogin" + } + ] + } + ], + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "lookbackDuration": "1d", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject10').analyticRuleId10,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 10", + "parentId": "[variables('analyticRuleObject10').analyticRuleId10]", + "contentId": "[variables('analyticRuleObject10')._analyticRulecontentId10]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject10').analyticRuleVersion10]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject10')._analyticRulecontentId10]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale: Split-DNS configuration modified", + "contentProductId": "[variables('analyticRuleObject10')._analyticRulecontentProductId10]", + "id": "[variables('analyticRuleObject10')._analyticRulecontentProductId10]", + "version": "[variables('analyticRuleObject10').analyticRuleVersion10]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject11').analyticRuleTemplateSpecName11]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleDnsNameserversModified_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject11').analyticRuleVersion11]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject11')._analyticRulecontentId11]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Identifies when the tailnet's global DNS nameserver list is modified. Adding an attacker-controlled resolver as a tailnet-wide nameserver enables broad DNS hijacking for every device using MagicDNS resolution.", + "displayName": "Tailscale: DNS nameservers modified", + "enabled": false, + "query": "Tailscale_Audit_CL\n| where Action == \"UPDATE\"\n| where tostring(Target.type) == \"TAILNET\"\n| where tostring(Target.property) == \"DNS_NAMESERVERS\"\n| extend ActorLogin = tostring(Actor.loginName)\n| extend OldServers = Old.dns\n| extend NewServers = New.dns\n| extend Added = set_difference(NewServers, OldServers)\n| extend Removed = set_difference(OldServers, NewServers)\n| project TimeGenerated, ActorLogin, OldServers, NewServers, Added, Removed, Origin\n", + "queryFrequency": "PT15M", + "queryPeriod": "PT15M", + "severity": "High", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Audit_CL" + ], + "connectorId": "TailscaleCCF" + } + ], + "tactics": [ + "DefenseEvasion", + "CommandAndControl" + ], + "techniques": [ + "T1556", + "T1568" + ], + "entityMappings": [ + { + "entityType": "Account", + "fieldMappings": [ + { + "identifier": "FullName", + "columnName": "ActorLogin" + } + ] + } + ], + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "lookbackDuration": "1d", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject11').analyticRuleId11,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 11", + "parentId": "[variables('analyticRuleObject11').analyticRuleId11]", + "contentId": "[variables('analyticRuleObject11')._analyticRulecontentId11]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject11').analyticRuleVersion11]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject11')._analyticRulecontentId11]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale: DNS nameservers modified", + "contentProductId": "[variables('analyticRuleObject11')._analyticRulecontentProductId11]", + "id": "[variables('analyticRuleObject11')._analyticRulecontentProductId11]", + "version": "[variables('analyticRuleObject11').analyticRuleVersion11]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject12').analyticRuleTemplateSpecName12]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleMagicDnsDisabled_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject12').analyticRuleVersion12]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject12')._analyticRulecontentId12]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Identifies when MagicDNS is turned off on the tailnet. Disabling MagicDNS changes DNS behaviour for every device and is occasionally a precursor to wider DNS hijacking - verify the change was intentional.", + "displayName": "Tailscale: MagicDNS disabled", + "enabled": false, + "query": "Tailscale_Audit_CL\n| where Action == \"UPDATE\"\n| where tostring(Target.type) == \"TAILNET\"\n| where tostring(Target.property) == \"MAGICDNS_PREFERENCES\"\n| extend ActorLogin = tostring(Actor.loginName)\n| extend OldMagicDns = tobool(Old.magicDNS)\n| extend NewMagicDns = tobool(New.magicDNS)\n| where OldMagicDns == true and NewMagicDns == false\n| project TimeGenerated, ActorLogin, OldMagicDns, NewMagicDns, Origin\n", + "queryFrequency": "PT15M", + "queryPeriod": "PT15M", + "severity": "Medium", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Audit_CL" + ], + "connectorId": "TailscaleCCF" + } + ], + "tactics": [ + "DefenseEvasion" + ], + "techniques": [ + "T1556" + ], + "entityMappings": [ + { + "entityType": "Account", + "fieldMappings": [ + { + "identifier": "FullName", + "columnName": "ActorLogin" + } + ] + } + ], + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "lookbackDuration": "1d", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject12').analyticRuleId12,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 12", + "parentId": "[variables('analyticRuleObject12').analyticRuleId12]", + "contentId": "[variables('analyticRuleObject12')._analyticRulecontentId12]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject12').analyticRuleVersion12]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject12')._analyticRulecontentId12]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale: MagicDNS disabled", + "contentProductId": "[variables('analyticRuleObject12')._analyticRulecontentProductId12]", + "id": "[variables('analyticRuleObject12')._analyticRulecontentProductId12]", + "version": "[variables('analyticRuleObject12').analyticRuleVersion12]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject13').analyticRuleTemplateSpecName13]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleTailnetLockValidationFailed_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject13').analyticRuleVersion13]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject13')._analyticRulecontentId13]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Identifies tailnet devices with a non-empty TailnetLockError, indicating the device failed tailnet-lock cryptographic validation. Suspicious - likely an unsigned node attempting to join.", + "displayName": "Tailscale: Tailnet lock validation failed", + "enabled": false, + "query": "Tailscale_Devices_CL\n| where TimeGenerated > ago(1h)\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where isnotempty(TailnetLockError)\n| project TimeGenerated, DeviceName, Hostname, User, Os, ClientVersion, TailnetLockError, TailnetLockKey, LastSeen, Authorized\n", + "queryFrequency": "PT1H", + "queryPeriod": "PT1H", + "severity": "High", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Devices_CL" + ], + "connectorId": "TailscaleCCF" + } + ], + "tactics": [ + "DefenseEvasion", + "InitialAccess" + ], + "techniques": [ + "T1556", + "T1078" + ], + "entityMappings": [ + { + "entityType": "Host", + "fieldMappings": [ + { + "identifier": "HostName", + "columnName": "Hostname" + } + ] + }, + { + "entityType": "Account", + "fieldMappings": [ + { + "identifier": "FullName", + "columnName": "User" + } + ] + } + ], + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "lookbackDuration": "1d", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject13').analyticRuleId13,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 13", + "parentId": "[variables('analyticRuleObject13').analyticRuleId13]", + "contentId": "[variables('analyticRuleObject13')._analyticRulecontentId13]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject13').analyticRuleVersion13]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject13')._analyticRulecontentId13]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale: Tailnet lock validation failed", + "contentProductId": "[variables('analyticRuleObject13')._analyticRulecontentProductId13]", + "id": "[variables('analyticRuleObject13')._analyticRulecontentProductId13]", + "version": "[variables('analyticRuleObject13').analyticRuleVersion13]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject14').analyticRuleTemplateSpecName14]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleDeviceSshNewlyEnabled_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject14').analyticRuleVersion14]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject14')._analyticRulecontentId14]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Identifies when Tailscale SSH is enabled on a device that previously did not have it. SSH provides authenticated shell access over the tailnet using Tailscale identity, broadening attack surface if unexpected. Verify and confirm the SSH ACL covers it.", + "displayName": "Tailscale: Device Tailscale SSH newly enabled", + "enabled": false, + "query": "let recent =\n Tailscale_Devices_CL\n | where TimeGenerated > ago(1h)\n | summarize arg_max(TimeGenerated, *) by DeviceId\n | where SshEnabled == true\n | project DeviceId, DeviceName, Hostname, User, Os, ClientVersion, LastSeen;\nlet prior =\n Tailscale_Devices_CL\n | where TimeGenerated between (ago(2d) .. ago(1h))\n | summarize arg_max(TimeGenerated, *) by DeviceId\n | where SshEnabled == true\n | distinct DeviceId;\nrecent\n| join kind=leftanti prior on DeviceId\n| project TimeGenerated = now(), DeviceName, Hostname, User, Os, ClientVersion, LastSeen\n", + "queryFrequency": "PT1H", + "queryPeriod": "P2D", + "severity": "Medium", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Devices_CL" + ], + "connectorId": "TailscaleCCF" + } + ], + "tactics": [ + "Persistence", + "LateralMovement" + ], + "techniques": [ + "T1021", + "T1098" + ], + "entityMappings": [ + { + "entityType": "Host", + "fieldMappings": [ + { + "identifier": "HostName", + "columnName": "Hostname" + } + ] + }, + { + "entityType": "Account", + "fieldMappings": [ + { + "identifier": "FullName", + "columnName": "User" + } + ] + } + ], + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "lookbackDuration": "1d", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject14').analyticRuleId14,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 14", + "parentId": "[variables('analyticRuleObject14').analyticRuleId14]", + "contentId": "[variables('analyticRuleObject14')._analyticRulecontentId14]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject14').analyticRuleVersion14]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject14')._analyticRulecontentId14]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale: Device Tailscale SSH newly enabled", + "contentProductId": "[variables('analyticRuleObject14')._analyticRulecontentProductId14]", + "id": "[variables('analyticRuleObject14')._analyticRulecontentProductId14]", + "version": "[variables('analyticRuleObject14').analyticRuleVersion14]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject15').analyticRuleTemplateSpecName15]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleUnauthorizedDeviceConnected_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject15').analyticRuleVersion15]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject15')._analyticRulecontentId15]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Identifies devices actively connected to the Tailscale control plane (ConnectedToControl=true) but not yet authorized by an admin (Authorized=false). Often benign onboarding but can indicate rogue joins.", + "displayName": "Tailscale: Unauthorized device connected to control plane", + "enabled": false, + "query": "Tailscale_Devices_CL\n| where TimeGenerated > ago(1h)\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where Authorized == false and ConnectedToControl == true\n| project TimeGenerated, DeviceName, Hostname, User, Os, ClientVersion, Created, LastSeen, MachineKey, NodeKey\n", + "queryFrequency": "PT1H", + "queryPeriod": "PT1H", + "severity": "High", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Devices_CL" + ], + "connectorId": "TailscaleCCF" + } + ], + "tactics": [ + "InitialAccess", + "Persistence" + ], + "techniques": [ + "T1078", + "T1098" + ], + "entityMappings": [ + { + "entityType": "Host", + "fieldMappings": [ + { + "identifier": "HostName", + "columnName": "Hostname" + } + ] + }, + { + "entityType": "Account", + "fieldMappings": [ + { + "identifier": "FullName", + "columnName": "User" + } + ] + } + ], + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "lookbackDuration": "1d", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject15').analyticRuleId15,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 15", + "parentId": "[variables('analyticRuleObject15').analyticRuleId15]", + "contentId": "[variables('analyticRuleObject15')._analyticRulecontentId15]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject15').analyticRuleVersion15]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject15')._analyticRulecontentId15]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale: Unauthorized device connected to control plane", + "contentProductId": "[variables('analyticRuleObject15')._analyticRulecontentProductId15]", + "id": "[variables('analyticRuleObject15')._analyticRulecontentProductId15]", + "version": "[variables('analyticRuleObject15').analyticRuleVersion15]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject16').analyticRuleTemplateSpecName16]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleExternalDeviceAdded_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject16').analyticRuleVersion16]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject16')._analyticRulecontentId16]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "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.", + "displayName": "Tailscale: External (shared-in) device added", + "enabled": false, + "query": "let recent =\n Tailscale_Devices_CL\n | where TimeGenerated > ago(1h)\n | summarize arg_max(TimeGenerated, *) by DeviceId\n | where IsExternal == true\n | project DeviceId, DeviceName, Hostname, User, Os, ClientVersion, Created, LastSeen, Tags;\nlet prior =\n Tailscale_Devices_CL\n | where TimeGenerated between (ago(2d) .. ago(1h))\n | summarize arg_max(TimeGenerated, *) by DeviceId\n | where IsExternal == true\n | distinct DeviceId;\nrecent\n| join kind=leftanti prior on DeviceId\n| project TimeGenerated = now(), DeviceName, Hostname, User, Os, ClientVersion, Created, LastSeen, Tags\n", + "queryFrequency": "PT1H", + "queryPeriod": "P2D", + "severity": "Medium", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Devices_CL" + ], + "connectorId": "TailscaleCCF" + } + ], + "tactics": [ + "InitialAccess" + ], + "techniques": [ + "T1078" + ], + "entityMappings": [ + { + "entityType": "Host", + "fieldMappings": [ + { + "identifier": "HostName", + "columnName": "Hostname" + } + ] + }, + { + "entityType": "Account", + "fieldMappings": [ + { + "identifier": "FullName", + "columnName": "User" + } + ] + } + ], + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "lookbackDuration": "1d", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject16').analyticRuleId16,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 16", + "parentId": "[variables('analyticRuleObject16').analyticRuleId16]", + "contentId": "[variables('analyticRuleObject16')._analyticRulecontentId16]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject16').analyticRuleVersion16]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject16')._analyticRulecontentId16]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale: External (shared-in) device added", + "contentProductId": "[variables('analyticRuleObject16')._analyticRulecontentProductId16]", + "id": "[variables('analyticRuleObject16')._analyticRulecontentProductId16]", + "version": "[variables('analyticRuleObject16').analyticRuleVersion16]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject17').analyticRuleTemplateSpecName17]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscalePremiumUnexpectedExitNodeEgress_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject17').analyticRuleVersion17]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject17')._analyticRulecontentId17]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Identifies when a node sends traffic via an exit node not used in the prior 7-day baseline. First-seen exit destinations from a node may indicate routing-policy drift, data exfiltration, or compromise.", + "displayName": "Tailscale Premium: Unexpected exit-node egress", + "enabled": false, + "query": "let baseline = 7d;\nlet recent = 1h;\nlet recentEgress =\n Tailscale_Network_CL\n | where TimeGenerated > ago(recent)\n | where HasExitTraffic\n | mv-expand t = ExitTraffic\n | extend Src = tostring(t.src), ExitDst = tostring(t.dst), Bytes = tolong(t.txBytes) + tolong(t.rxBytes)\n | summarize FirstSeen = min(TimeGenerated), TotalBytes = sum(Bytes) by NodeId, SrcNodeName, SrcUser, SrcOs, Src, ExitDst;\nlet baselineEgress =\n Tailscale_Network_CL\n | where TimeGenerated between (ago(baseline + recent) .. ago(recent))\n | where HasExitTraffic\n | mv-expand t = ExitTraffic\n | extend Src = tostring(t.src), ExitDst = tostring(t.dst)\n | distinct NodeId, Src, ExitDst;\nrecentEgress\n| join kind=leftanti baselineEgress on NodeId, Src, ExitDst\n| extend TotalMB = round(TotalBytes / 1024.0 / 1024.0, 2)\n", + "queryFrequency": "PT1H", + "queryPeriod": "PT1H", + "severity": "Medium", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Network_CL" + ], + "connectorId": "TailscalePremiumCCF" + } + ], + "tactics": [ + "CommandAndControl", + "Exfiltration" + ], + "techniques": [ + "T1090", + "T1041" + ], + "entityMappings": [ + { + "entityType": "Host", + "fieldMappings": [ + { + "identifier": "HostName", + "columnName": "SrcNodeName" + } + ] + }, + { + "entityType": "Account", + "fieldMappings": [ + { + "identifier": "FullName", + "columnName": "SrcUser" + } + ] + }, + { + "entityType": "IP", + "fieldMappings": [ + { + "identifier": "Address", + "columnName": "Src" + } + ] + } + ], + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "lookbackDuration": "5h", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject17').analyticRuleId17,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 17", + "parentId": "[variables('analyticRuleObject17').analyticRuleId17]", + "contentId": "[variables('analyticRuleObject17')._analyticRulecontentId17]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject17').analyticRuleVersion17]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject17')._analyticRulecontentId17]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale Premium: Unexpected exit-node egress", + "contentProductId": "[variables('analyticRuleObject17')._analyticRulecontentProductId17]", + "id": "[variables('analyticRuleObject17')._analyticRulecontentProductId17]", + "version": "[variables('analyticRuleObject17').analyticRuleVersion17]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject18').analyticRuleTemplateSpecName18]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscalePremiumLargeOutboundTransfer_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject18').analyticRuleVersion18]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject18')._analyticRulecontentId18]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "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.", + "displayName": "Tailscale Premium: Large outbound transfer over tailnet", + "enabled": false, + "query": "let bytesThreshold = 100 * 1024 * 1024;\nTailscale_Network_CL\n| where TimeGenerated > ago(1h)\n| where HasVirtualTraffic\n| mv-expand t = VirtualTraffic\n| 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)\n| summarize TotalBytes = sum(Bytes), TotalPackets = sum(Pkts) by NodeId, SrcNodeName, SrcUser, DstNodeName, DstUser, Src, Dst, Proto\n| where TotalBytes > bytesThreshold\n| extend TotalMB = round(TotalBytes / 1024.0 / 1024.0, 2)\n| order by TotalBytes desc\n", + "queryFrequency": "PT1H", + "queryPeriod": "PT1H", + "severity": "Medium", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Network_CL" + ], + "connectorId": "TailscalePremiumCCF" + } + ], + "tactics": [ + "Exfiltration", + "Collection" + ], + "techniques": [ + "T1041", + "T1020" + ], + "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": { + "lookbackDuration": "5h", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject18').analyticRuleId18,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 18", + "parentId": "[variables('analyticRuleObject18').analyticRuleId18]", + "contentId": "[variables('analyticRuleObject18')._analyticRulecontentId18]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject18').analyticRuleVersion18]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject18')._analyticRulecontentId18]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale Premium: Large outbound transfer over tailnet", + "contentProductId": "[variables('analyticRuleObject18')._analyticRulecontentProductId18]", + "id": "[variables('analyticRuleObject18')._analyticRulecontentProductId18]", + "version": "[variables('analyticRuleObject18').analyticRuleVersion18]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject19').analyticRuleTemplateSpecName19]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscalePremiumBeaconingDetected_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject19').analyticRuleVersion19]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject19')._analyticRulecontentId19]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Identifies when flows between a src-dst pair recur at a regular interval (80%+ of inter-flow gaps cluster on the same delta over 10+ flows). Signature of C2 beaconing or scheduled exfiltration. Requires Tailscale Premium or Enterprise.", + "displayName": "Tailscale Premium: Network flow beaconing detected", + "enabled": false, + "query": "let lookback = 2d;\nlet minFlows = 10;\nlet beaconPercentThreshold = 80.0;\nTailscale_Network_CL\n| where TimeGenerated > ago(lookback)\n| where HasVirtualTraffic\n| mv-expand t = VirtualTraffic\n| extend Src = tostring(t.src), Dst = tostring(t.dst), Proto = toint(t.proto)\n| project TimeGenerated, Src, Dst, Proto, SrcNodeName, SrcUser, DstNodeName, DstUser\n| sort by Src asc, Dst asc, Proto asc, TimeGenerated asc\n| serialize\n| extend NextTime = next(TimeGenerated), NextSrc = next(Src), NextDst = next(Dst), NextProto = next(Proto)\n| where Src == NextSrc and Dst == NextDst and Proto == NextProto\n| extend DeltaSec = datetime_diff('second', NextTime, TimeGenerated)\n| where DeltaSec > 5\n| summarize DeltaCount = count() by Src, Dst, Proto, DeltaSec, SrcNodeName, SrcUser, DstNodeName, DstUser\n| summarize (MostFrequentDeltaCount, MostFrequentDeltaSec) = arg_max(DeltaCount, DeltaSec), TotalFlows = sum(DeltaCount) by Src, Dst, Proto, SrcNodeName, SrcUser, DstNodeName, DstUser\n| where TotalFlows >= minFlows\n| extend BeaconPercent = round(MostFrequentDeltaCount * 100.0 / TotalFlows, 1)\n| where BeaconPercent >= beaconPercentThreshold\n", + "queryFrequency": "PT1H", + "queryPeriod": "P2D", + "severity": "Medium", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Network_CL" + ], + "connectorId": "TailscalePremiumCCF" + } + ], + "tactics": [ + "CommandAndControl", + "Exfiltration" + ], + "techniques": [ + "T1071", + "T1095", + "T1029" + ], + "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": { + "lookbackDuration": "1d", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject19').analyticRuleId19,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 19", + "parentId": "[variables('analyticRuleObject19').analyticRuleId19]", + "contentId": "[variables('analyticRuleObject19')._analyticRulecontentId19]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject19').analyticRuleVersion19]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject19')._analyticRulecontentId19]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale Premium: Network flow beaconing detected", + "contentProductId": "[variables('analyticRuleObject19')._analyticRulecontentProductId19]", + "id": "[variables('analyticRuleObject19')._analyticRulecontentProductId19]", + "version": "[variables('analyticRuleObject19').analyticRuleVersion19]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject20').analyticRuleTemplateSpecName20]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscalePremiumMassFanOut_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject20').analyticRuleVersion20]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject20')._analyticRulecontentId20]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Identifies when a single node initiates flows to 25 or more unique destinations within a 15-minute window. Sudden fan-out is consistent with port scanning, lateral discovery, or worm-style propagation. Requires Tailscale Premium or Enterprise.", + "displayName": "Tailscale Premium: Mass fan-out from single node", + "enabled": false, + "query": "let dstThreshold = 25;\nTailscale_Network_CL\n| where TimeGenerated > ago(15m)\n| where HasVirtualTraffic\n| mv-expand t = VirtualTraffic\n| extend Src = tostring(t.src), Dst = tostring(t.dst)\n| summarize UniqueDestinations = dcount(Dst), TopDestinations = make_set(Dst, 25) by NodeId, SrcNodeName, SrcUser, SrcOs, SrcTags=tostring(SrcTags), Src\n| where UniqueDestinations >= dstThreshold\n| order by UniqueDestinations desc\n", + "queryFrequency": "PT15M", + "queryPeriod": "PT15M", + "severity": "High", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Network_CL" + ], + "connectorId": "TailscalePremiumCCF" + } + ], + "tactics": [ + "Discovery", + "LateralMovement" + ], + "techniques": [ + "T1018", + "T1021", + "T1046" + ], + "entityMappings": [ + { + "entityType": "Host", + "fieldMappings": [ + { + "identifier": "HostName", + "columnName": "SrcNodeName" + } + ] + }, + { + "entityType": "Account", + "fieldMappings": [ + { + "identifier": "FullName", + "columnName": "SrcUser" + } + ] + }, + { + "entityType": "IP", + "fieldMappings": [ + { + "identifier": "Address", + "columnName": "Src" + } + ] + } + ], + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "lookbackDuration": "5h", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject20').analyticRuleId20,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 20", + "parentId": "[variables('analyticRuleObject20').analyticRuleId20]", + "contentId": "[variables('analyticRuleObject20')._analyticRulecontentId20]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject20').analyticRuleVersion20]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject20')._analyticRulecontentId20]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale Premium: Mass fan-out from single node", + "contentProductId": "[variables('analyticRuleObject20')._analyticRulecontentProductId20]", + "id": "[variables('analyticRuleObject20')._analyticRulecontentProductId20]", + "version": "[variables('analyticRuleObject20').analyticRuleVersion20]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject21').analyticRuleTemplateSpecName21]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscalePremiumSubnetRouterThroughputAnomaly_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject21').analyticRuleVersion21]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject21')._analyticRulecontentId21]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Identifies when a subnet router (gateway node bridging the tailnet to an on-prem or cloud subnet) handles 3x or more its 7-day baseline traffic in the last hour. Spikes can indicate exfiltration or scanning. Requires Tailscale Premium or Enterprise.", + "displayName": "Tailscale Premium: Subnet router throughput anomaly", + "enabled": false, + "query": "let baselineDays = 7d;\nlet recent = 1h;\nlet multiplier = 3.0;\nlet recentTraffic =\n Tailscale_Network_CL\n | where TimeGenerated > ago(recent)\n | where HasSubnetTraffic\n | mv-expand t = SubnetTraffic\n | extend Bytes = tolong(t.txBytes) + tolong(t.rxBytes)\n | summarize RecentBytes = sum(Bytes) by NodeId, SrcNodeName, SrcUser, SrcOs, SrcTags=tostring(SrcTags);\nlet baseline =\n Tailscale_Network_CL\n | where TimeGenerated between (ago(baselineDays + recent) .. ago(recent))\n | where HasSubnetTraffic\n | mv-expand t = SubnetTraffic\n | extend Bytes = tolong(t.txBytes) + tolong(t.rxBytes)\n | summarize TotalBaselineBytes = sum(Bytes) by NodeId\n | extend BaselineHourlyBytes = TotalBaselineBytes / 168.0;\nrecentTraffic\n| join kind=inner baseline on NodeId\n| where RecentBytes > BaselineHourlyBytes * multiplier\n| extend Multiplier = round(RecentBytes / BaselineHourlyBytes, 1), RecentMB = round(RecentBytes / 1024.0 / 1024.0, 2), BaselineHourlyMB = round(BaselineHourlyBytes / 1024.0 / 1024.0, 2)\n| project NodeId, SrcNodeName, SrcUser, SrcOs, SrcTags, RecentMB, BaselineHourlyMB, Multiplier\n| order by Multiplier desc\n", + "queryFrequency": "PT1H", + "queryPeriod": "P8D", + "severity": "Low", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Network_CL" + ], + "connectorId": "TailscalePremiumCCF" + } + ], + "tactics": [ + "Exfiltration", + "CommandAndControl" + ], + "techniques": [ + "T1572", + "T1041" + ], + "entityMappings": [ + { + "entityType": "Host", + "fieldMappings": [ + { + "identifier": "HostName", + "columnName": "SrcNodeName" + } + ] + }, + { + "entityType": "Account", + "fieldMappings": [ + { + "identifier": "FullName", + "columnName": "SrcUser" + } + ] + } + ], + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "lookbackDuration": "1d", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject21').analyticRuleId21,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 21", + "parentId": "[variables('analyticRuleObject21').analyticRuleId21]", + "contentId": "[variables('analyticRuleObject21')._analyticRulecontentId21]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject21').analyticRuleVersion21]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject21')._analyticRulecontentId21]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale Premium: Subnet router throughput anomaly", + "contentProductId": "[variables('analyticRuleObject21')._analyticRulecontentProductId21]", + "id": "[variables('analyticRuleObject21')._analyticRulecontentProductId21]", + "version": "[variables('analyticRuleObject21').analyticRuleVersion21]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject22').analyticRuleTemplateSpecName22]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscalePremiumPostureIntegrationDisabled_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject22').analyticRuleVersion22]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject22')._analyticRulecontentId22]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Identifies when a device-posture integration is disabled or removed from the tailnet. Posture integrations enforce device compliance - removal weakens fleet posture and is a possible defense-evasion step.", + "displayName": "Tailscale Premium: Posture integration disabled or removed", + "enabled": false, + "query": "Tailscale_Audit_CL\n| where Action in (\"DELETE\", \"UPDATE\")\n| where tostring(Target.type) == \"POSTURE_INTEGRATION\"\n or tostring(Target.type) startswith \"POSTURE\"\n| extend ActorLogin = tostring(Actor.loginName)\n| extend Provider = tostring(Target.name)\n| project TimeGenerated, ActorLogin, Action, Provider, Target, Old, New, Origin\n", + "queryFrequency": "PT15M", + "queryPeriod": "PT15M", + "severity": "High", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Audit_CL" + ], + "connectorId": "TailscalePremiumCCF" + } + ], + "tactics": [ + "DefenseEvasion", + "Persistence" + ], + "techniques": [ + "T1562", + "T1556" + ], + "entityMappings": [ + { + "entityType": "Account", + "fieldMappings": [ + { + "identifier": "FullName", + "columnName": "ActorLogin" + } + ] + } + ], + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "lookbackDuration": "1d", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject22').analyticRuleId22,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 22", + "parentId": "[variables('analyticRuleObject22').analyticRuleId22]", + "contentId": "[variables('analyticRuleObject22')._analyticRulecontentId22]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject22').analyticRuleVersion22]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject22')._analyticRulecontentId22]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale Premium: Posture integration disabled or removed", + "contentProductId": "[variables('analyticRuleObject22')._analyticRulecontentProductId22]", + "id": "[variables('analyticRuleObject22')._analyticRulecontentProductId22]", + "version": "[variables('analyticRuleObject22').analyticRuleVersion22]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject23').analyticRuleTemplateSpecName23]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscalePremiumNewPostureIntegration_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject23').analyticRuleVersion23]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject23')._analyticRulecontentId23]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Identifies when a new device-posture integration is added to the tailnet (Jamf, Kandji, Intune, Kolide, Defender for Endpoint, CrowdStrike, SentinelOne). Verify the addition was sanctioned.", + "displayName": "Tailscale Premium: New posture integration added", + "enabled": false, + "query": "Tailscale_Audit_CL\n| where Action == \"CREATE\"\n| where tostring(Target.type) == \"POSTURE_INTEGRATION\"\n or tostring(Target.type) startswith \"POSTURE\"\n| extend ActorLogin = tostring(Actor.loginName)\n| extend Provider = tostring(Target.name)\n| project TimeGenerated, ActorLogin, Provider, Target, New, Origin\n", + "queryFrequency": "PT15M", + "queryPeriod": "PT15M", + "severity": "Medium", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Audit_CL" + ], + "connectorId": "TailscalePremiumCCF" + } + ], + "tactics": [ + "Persistence" + ], + "techniques": [ + "T1098" + ], + "entityMappings": [ + { + "entityType": "Account", + "fieldMappings": [ + { + "identifier": "FullName", + "columnName": "ActorLogin" + } + ] + } + ], + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "lookbackDuration": "1d", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject23').analyticRuleId23,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 23", + "parentId": "[variables('analyticRuleObject23').analyticRuleId23]", + "contentId": "[variables('analyticRuleObject23')._analyticRulecontentId23]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject23').analyticRuleVersion23]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject23')._analyticRulecontentId23]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale Premium: New posture integration added", + "contentProductId": "[variables('analyticRuleObject23')._analyticRulecontentProductId23]", + "id": "[variables('analyticRuleObject23')._analyticRulecontentProductId23]", + "version": "[variables('analyticRuleObject23').analyticRuleVersion23]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject24').analyticRuleTemplateSpecName24]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscalePremiumDerpRelaySurge_AnalyticalRules Analytics Rule with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject24').analyticRuleVersion24]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject24')._analyticRulecontentId24]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Identifies when a source node has more than 75 percent of its recent flows falling back to a DERP relay (Tailscale IsRelayed flag, traffic via 127.3.3.40). Operational signal useful for spotting policy drift.", + "displayName": "Tailscale Premium: DERP relay traffic surge", + "enabled": false, + "query": "let minFlows = 20;\nlet relayedPctThreshold = 75.0;\nTailscale_Network_CL\n| where TimeGenerated > ago(15m)\n| summarize\n TotalFlows = count(),\n RelayedFlows = countif(IsRelayed)\n by SrcNodeName, SrcUser, SrcOs, SrcTags=tostring(SrcTags)\n| where TotalFlows >= minFlows\n| extend RelayedPct = round(100.0 * RelayedFlows / TotalFlows, 1)\n| where RelayedPct > relayedPctThreshold\n| project SrcNodeName, SrcUser, SrcOs, SrcTags, TotalFlows, RelayedFlows, RelayedPct\n| order by RelayedPct desc\n", + "queryFrequency": "PT15M", + "queryPeriod": "PT15M", + "severity": "Low", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "Tailscale_Network_CL" + ], + "connectorId": "TailscalePremiumCCF" + } + ], + "tactics": [ + "CommandAndControl" + ], + "techniques": [ + "T1572" + ], + "entityMappings": [ + { + "entityType": "Host", + "fieldMappings": [ + { + "identifier": "HostName", + "columnName": "SrcNodeName" + } + ] + }, + { + "entityType": "Account", + "fieldMappings": [ + { + "identifier": "FullName", + "columnName": "SrcUser" + } + ] + } + ], + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "lookbackDuration": "6h", + "groupByEntities": [], + "matchingMethod": "AllEntities", + "reopenClosedIncident": false, + "enabled": true + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject24').analyticRuleId24,'/'))))]", + "properties": { + "description": "Tailscale (CCF) Analytics Rule 24", + "parentId": "[variables('analyticRuleObject24').analyticRuleId24]", + "contentId": "[variables('analyticRuleObject24')._analyticRulecontentId24]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject24').analyticRuleVersion24]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject24')._analyticRulecontentId24]", + "contentKind": "AnalyticsRule", + "displayName": "Tailscale Premium: DERP relay traffic surge", + "contentProductId": "[variables('analyticRuleObject24')._analyticRulecontentProductId24]", + "id": "[variables('analyticRuleObject24')._analyticRulecontentProductId24]", + "version": "[variables('analyticRuleObject24').analyticRuleVersion24]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('huntingQueryObject1').huntingQueryTemplateSpecName1]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleFirstSeenActor_HuntingQueries Hunting Query with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('huntingQueryObject1').huntingQueryVersion1]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.OperationalInsights/savedSearches", + "apiVersion": "2025-07-01", + "name": "Tailscale_(CCF)_Hunting_Query_1", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Tailscale: First-seen actor making configuration changes", + "category": "Hunting Queries", + "query": "let lookback = 14d;\nlet baselineWindow = 14d;\nTailscale_Audit_CL\n| where TimeGenerated > ago(lookback)\n| extend ActorLogin = tostring(Actor.loginName)\n| where isnotempty(ActorLogin)\n| summarize FirstSeen = min(TimeGenerated), Actions = make_set(Action), Targets = make_set(tostring(Target.type)), TotalEvents = count() by ActorLogin\n| join kind=leftanti (\n Tailscale_Audit_CL\n | where TimeGenerated between (ago(lookback + baselineWindow) .. ago(lookback))\n | extend ActorLogin = tostring(Actor.loginName)\n | distinct ActorLogin\n) on ActorLogin\n| order by FirstSeen asc\n", + "version": 2, + "tags": [ + { + "name": "description", + "value": "Identifies actors making their first configuration change in the lookback window. New legitimate admins look identical to compromised credentials - review whether each surfaced actor is expected to have admin rights." + }, + { + "name": "tactics", + "value": "InitialAccess,Persistence" + }, + { + "name": "techniques", + "value": "T1078" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject1')._huntingQuerycontentId1),'/'))))]", + "properties": { + "description": "Tailscale (CCF) Hunting Query 1", + "parentId": "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject1')._huntingQuerycontentId1)]", + "contentId": "[variables('huntingQueryObject1')._huntingQuerycontentId1]", + "kind": "HuntingQuery", + "version": "[variables('huntingQueryObject1').huntingQueryVersion1]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('huntingQueryObject1')._huntingQuerycontentId1]", + "contentKind": "HuntingQuery", + "displayName": "Tailscale: First-seen actor making configuration changes", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject1')._huntingQuerycontentId1,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject1')._huntingQuerycontentId1,'-', '1.0.0')))]", + "version": "1.0.0" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('huntingQueryObject2').huntingQueryTemplateSpecName2]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleACLPolicyChurn_HuntingQueries Hunting Query with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('huntingQueryObject2').huntingQueryVersion2]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.OperationalInsights/savedSearches", + "apiVersion": "2025-07-01", + "name": "Tailscale_(CCF)_Hunting_Query_2", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Tailscale: ACL policy churn", + "category": "Hunting Queries", + "query": "let bucket = 30m;\nlet churnThreshold = 3;\nTailscale_Audit_CL\n| where Action == \"UPDATE\"\n| where tostring(Target.type) == \"TAILNET\"\n| where tostring(Target.property) == \"ACL\"\n| extend ActorLogin = tostring(Actor.loginName)\n| summarize EditCount = count(), Editors = make_set(ActorLogin), FirstEdit = min(TimeGenerated), LastEdit = max(TimeGenerated)\n by bin(TimeGenerated, bucket)\n| where EditCount >= churnThreshold\n| order by EditCount desc\n", + "version": 2, + "tags": [ + { + "name": "description", + "value": "Identifies short windows where the tailnet ACL policy file was rewritten multiple times. Iterative policy edits during an incident can indicate a defender adjusting rules or an attacker probing." + }, + { + "name": "tactics", + "value": "DefenseEvasion,PrivilegeEscalation" + }, + { + "name": "techniques", + "value": "T1098,T1556" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject2')._huntingQuerycontentId2),'/'))))]", + "properties": { + "description": "Tailscale (CCF) Hunting Query 2", + "parentId": "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject2')._huntingQuerycontentId2)]", + "contentId": "[variables('huntingQueryObject2')._huntingQuerycontentId2]", + "kind": "HuntingQuery", + "version": "[variables('huntingQueryObject2').huntingQueryVersion2]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('huntingQueryObject2')._huntingQuerycontentId2]", + "contentKind": "HuntingQuery", + "displayName": "Tailscale: ACL policy churn", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject2')._huntingQuerycontentId2,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject2')._huntingQuerycontentId2,'-', '1.0.0')))]", + "version": "1.0.0" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('huntingQueryObject3').huntingQueryTemplateSpecName3]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleOffHoursConfigChanges_HuntingQueries Hunting Query with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('huntingQueryObject3').huntingQueryVersion3]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.OperationalInsights/savedSearches", + "apiVersion": "2025-07-01", + "name": "Tailscale_(CCF)_Hunting_Query_3", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Tailscale: Off-hours configuration changes", + "category": "Hunting Queries", + "query": "let businessStartUtc = 8;\nlet businessEndUtc = 18;\nTailscale_Audit_CL\n| extend HourOfDay = datetime_part(\"hour\", TimeGenerated), DayOfWeek = dayofweek(TimeGenerated)\n| where DayOfWeek in (0d, 6d) // Sunday and Saturday\n or HourOfDay < businessStartUtc\n or HourOfDay >= businessEndUtc\n| extend ActorLogin = tostring(Actor.loginName)\n| extend TargetType = tostring(Target.type)\n| project TimeGenerated, ActorLogin, Action, TargetType, Target, Origin\n| order by TimeGenerated desc\n", + "version": 2, + "tags": [ + { + "name": "description", + "value": "Identifies configuration audit events that occurred outside typical business hours (Monday-Friday 08:00-18:00 UTC). Useful for spotting impromptu maintenance, account compromise, or insider activity." + }, + { + "name": "tactics", + "value": "InitialAccess,Persistence" + }, + { + "name": "techniques", + "value": "T1078" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject3')._huntingQuerycontentId3),'/'))))]", + "properties": { + "description": "Tailscale (CCF) Hunting Query 3", + "parentId": "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject3')._huntingQuerycontentId3)]", + "contentId": "[variables('huntingQueryObject3')._huntingQuerycontentId3]", + "kind": "HuntingQuery", + "version": "[variables('huntingQueryObject3').huntingQueryVersion3]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('huntingQueryObject3')._huntingQuerycontentId3]", + "contentKind": "HuntingQuery", + "displayName": "Tailscale: Off-hours configuration changes", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject3')._huntingQuerycontentId3,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject3')._huntingQuerycontentId3,'-', '1.0.0')))]", + "version": "1.0.0" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('huntingQueryObject4').huntingQueryTemplateSpecName4]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleAuthKeySprawl_HuntingQueries Hunting Query with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('huntingQueryObject4').huntingQueryVersion4]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.OperationalInsights/savedSearches", + "apiVersion": "2025-07-01", + "name": "Tailscale_(CCF)_Hunting_Query_4", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Tailscale: Auth key sprawl", + "category": "Hunting Queries", + "query": "let bucket = 1h;\nlet sprawlThreshold = 5;\nTailscale_Audit_CL\n| where Action == \"CREATE\"\n| where tostring(Target.type) == \"AUTH_KEY\"\n| extend ActorLogin = tostring(Actor.loginName)\n| summarize KeysCreated = count(), KeyIDs = make_set(tostring(Target.id)), Reusable = make_set(tostring(New.reusable)), Ephemeral = make_set(tostring(New.ephemeral))\n by bin(TimeGenerated, bucket), ActorLogin\n| where KeysCreated >= sprawlThreshold\n| order by KeysCreated desc\n", + "version": 2, + "tags": [ + { + "name": "description", + "value": "Identifies actors creating multiple auth keys in a short window. A single admin creating many keys for unattended enrollment is normal during a rollout; same pattern can also indicate token-spraying." + }, + { + "name": "tactics", + "value": "Persistence,CredentialAccess" + }, + { + "name": "techniques", + "value": "T1098,T1136" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject4')._huntingQuerycontentId4),'/'))))]", + "properties": { + "description": "Tailscale (CCF) Hunting Query 4", + "parentId": "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject4')._huntingQuerycontentId4)]", + "contentId": "[variables('huntingQueryObject4')._huntingQuerycontentId4]", + "kind": "HuntingQuery", + "version": "[variables('huntingQueryObject4').huntingQueryVersion4]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('huntingQueryObject4')._huntingQuerycontentId4]", + "contentKind": "HuntingQuery", + "displayName": "Tailscale: Auth key sprawl", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject4')._huntingQuerycontentId4,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject4')._huntingQuerycontentId4,'-', '1.0.0')))]", + "version": "1.0.0" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('huntingQueryObject5').huntingQueryTemplateSpecName5]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleDormantDevices_HuntingQueries Hunting Query with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('huntingQueryObject5').huntingQueryVersion5]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.OperationalInsights/savedSearches", + "apiVersion": "2025-07-01", + "name": "Tailscale_(CCF)_Hunting_Query_5", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Tailscale: Devices not seen in 30+ days", + "category": "Hunting Queries", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where LastSeen < ago(30d)\n| extend DaysSinceSeen = datetime_diff('day', now(), LastSeen)\n| project DeviceName, Hostname, User, Os, ClientVersion, LastSeen, DaysSinceSeen, Tags, AdvertisedRoutes\n| order by DaysSinceSeen desc\n", + "version": 2, + "tags": [ + { + "name": "description", + "value": "Identifies tailnet devices that have not connected to the control plane for at least 30 days. Dormant devices accumulate risk - they may still have valid keys, advertised routes, or tags." + }, + { + "name": "tactics", + "value": "Discovery" + }, + { + "name": "techniques", + "value": "T1078" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject5')._huntingQuerycontentId5),'/'))))]", + "properties": { + "description": "Tailscale (CCF) Hunting Query 5", + "parentId": "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject5')._huntingQuerycontentId5)]", + "contentId": "[variables('huntingQueryObject5')._huntingQuerycontentId5]", + "kind": "HuntingQuery", + "version": "[variables('huntingQueryObject5').huntingQueryVersion5]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('huntingQueryObject5')._huntingQuerycontentId5]", + "contentKind": "HuntingQuery", + "displayName": "Tailscale: Devices not seen in 30+ days", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject5')._huntingQuerycontentId5,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject5')._huntingQuerycontentId5,'-', '1.0.0')))]", + "version": "1.0.0" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('huntingQueryObject6').huntingQueryTemplateSpecName6]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleAuthKeysNoExpiry_HuntingQueries Hunting Query with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('huntingQueryObject6').huntingQueryVersion6]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.OperationalInsights/savedSearches", + "apiVersion": "2025-07-01", + "name": "Tailscale_(CCF)_Hunting_Query_6", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Tailscale: Auth keys with no expiry", + "category": "Hunting Queries", + "query": "Tailscale_Keys_CL\n| summarize arg_max(TimeGenerated, *) by KeyId\n| where isnull(Revoked) or Revoked == datetime(null)\n| where isnull(Expires) or Expires == datetime(null)\n| project KeyId, Description, UserId, Created, Capabilities\n| order by Created asc\n", + "version": 2, + "tags": [ + { + "name": "description", + "value": "Identifies tailnet auth keys that have no expiry timestamp set. Non-expiring keys grant unattended enrollment in perpetuity and should be rotated or replaced with ephemeral, time-bounded keys." + }, + { + "name": "tactics", + "value": "Persistence,CredentialAccess" + }, + { + "name": "techniques", + "value": "T1098,T1136" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject6')._huntingQuerycontentId6),'/'))))]", + "properties": { + "description": "Tailscale (CCF) Hunting Query 6", + "parentId": "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject6')._huntingQuerycontentId6)]", + "contentId": "[variables('huntingQueryObject6')._huntingQuerycontentId6]", + "kind": "HuntingQuery", + "version": "[variables('huntingQueryObject6').huntingQueryVersion6]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('huntingQueryObject6')._huntingQuerycontentId6]", + "contentKind": "HuntingQuery", + "displayName": "Tailscale: Auth keys with no expiry", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject6')._huntingQuerycontentId6,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject6')._huntingQuerycontentId6,'-', '1.0.0')))]", + "version": "1.0.0" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('huntingQueryObject7').huntingQueryTemplateSpecName7]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleOrphanedUsers_HuntingQueries Hunting Query with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('huntingQueryObject7').huntingQueryVersion7]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.OperationalInsights/savedSearches", + "apiVersion": "2025-07-01", + "name": "Tailscale_(CCF)_Hunting_Query_7", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Tailscale: Users with zero devices", + "category": "Hunting Queries", + "query": "Tailscale_Users_CL\n| summarize arg_max(TimeGenerated, *) by UserId\n| where DeviceCount == 0\n| where Status != \"suspended\"\n| project LoginName, DisplayName, Role, Status, DeviceCount, Created, LastSeen\n| order by LastSeen asc\n", + "version": 2, + "tags": [ + { + "name": "description", + "value": "Identifies tailnet users who have no devices currently registered. Orphaned identities are candidates for off-boarding - they retain whatever role/permissions they were granted." + }, + { + "name": "tactics", + "value": "InitialAccess" + }, + { + "name": "techniques", + "value": "T1078" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject7')._huntingQuerycontentId7),'/'))))]", + "properties": { + "description": "Tailscale (CCF) Hunting Query 7", + "parentId": "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject7')._huntingQuerycontentId7)]", + "contentId": "[variables('huntingQueryObject7')._huntingQuerycontentId7]", + "kind": "HuntingQuery", + "version": "[variables('huntingQueryObject7').huntingQueryVersion7]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('huntingQueryObject7')._huntingQuerycontentId7]", + "contentKind": "HuntingQuery", + "displayName": "Tailscale: Users with zero devices", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject7')._huntingQuerycontentId7,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject7')._huntingQuerycontentId7,'-', '1.0.0')))]", + "version": "1.0.0" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('huntingQueryObject8').huntingQueryTemplateSpecName8]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleSplitDnsPerDomainChanges_HuntingQueries Hunting Query with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('huntingQueryObject8').huntingQueryVersion8]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.OperationalInsights/savedSearches", + "apiVersion": "2025-07-01", + "name": "Tailscale_(CCF)_Hunting_Query_8", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Tailscale: Split-DNS per-domain change history", + "category": "Hunting Queries", + "query": "Tailscale_Audit_CL\n| where Action == \"UPDATE\"\n| where tostring(Target.type) == \"TAILNET\"\n| where tostring(Target.property) == \"DNS_SPLIT_DNS\"\n| extend ActorLogin = tostring(Actor.loginName)\n| extend OldKeys = bag_keys(Old), NewKeys = bag_keys(New)\n| extend AllDomains = set_union(OldKeys, NewKeys)\n| mv-expand Domain = AllDomains to typeof(string)\n| extend OldResolvers = Old[Domain], NewResolvers = New[Domain]\n| extend Change = case(\n isnull(OldResolvers) and isnotnull(NewResolvers), \"added\",\n isnotnull(OldResolvers) and isnull(NewResolvers), \"removed\",\n tostring(OldResolvers) != tostring(NewResolvers), \"resolvers-changed\",\n \"unchanged\")\n| where Change != \"unchanged\"\n| project TimeGenerated, ActorLogin, Domain, Change, OldResolvers, NewResolvers\n| order by TimeGenerated desc\n", + "version": 2, + "tags": [ + { + "name": "description", + "value": "Identifies the per-domain Split-DNS change history from the audit log. For each modification event, expands Old and New documents and surfaces which domains were added, removed, or changed." + }, + { + "name": "tactics", + "value": "DefenseEvasion,CommandAndControl" + }, + { + "name": "techniques", + "value": "T1556,T1568" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject8')._huntingQuerycontentId8),'/'))))]", + "properties": { + "description": "Tailscale (CCF) Hunting Query 8", + "parentId": "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject8')._huntingQuerycontentId8)]", + "contentId": "[variables('huntingQueryObject8')._huntingQuerycontentId8]", + "kind": "HuntingQuery", + "version": "[variables('huntingQueryObject8').huntingQueryVersion8]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('huntingQueryObject8')._huntingQuerycontentId8]", + "contentKind": "HuntingQuery", + "displayName": "Tailscale: Split-DNS per-domain change history", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject8')._huntingQuerycontentId8,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject8')._huntingQuerycontentId8,'-', '1.0.0')))]", + "version": "1.0.0" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('huntingQueryObject9').huntingQueryTemplateSpecName9]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleDevicesWithSshEnabled_HuntingQueries Hunting Query with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('huntingQueryObject9').huntingQueryVersion9]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.OperationalInsights/savedSearches", + "apiVersion": "2025-07-01", + "name": "Tailscale_(CCF)_Hunting_Query_9", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Tailscale: Devices with Tailscale SSH enabled", + "category": "Hunting Queries", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where SshEnabled == true\n| project DeviceName, Hostname, User, Os, Distro, ClientVersion, LastSeen, Tags\n| order by DeviceName asc\n", + "version": 2, + "tags": [ + { + "name": "description", + "value": "Identifies devices that currently have Tailscale SSH enabled. Tailscale SSH delivers SSH access over the tailnet using Tailscale identity and is governed by the SSH ACL block in the policy file." + }, + { + "name": "tactics", + "value": "LateralMovement,Persistence" + }, + { + "name": "techniques", + "value": "T1021" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject9')._huntingQuerycontentId9),'/'))))]", + "properties": { + "description": "Tailscale (CCF) Hunting Query 9", + "parentId": "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject9')._huntingQuerycontentId9)]", + "contentId": "[variables('huntingQueryObject9')._huntingQuerycontentId9]", + "kind": "HuntingQuery", + "version": "[variables('huntingQueryObject9').huntingQueryVersion9]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('huntingQueryObject9')._huntingQuerycontentId9]", + "contentKind": "HuntingQuery", + "displayName": "Tailscale: Devices with Tailscale SSH enabled", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject9')._huntingQuerycontentId9,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject9')._huntingQuerycontentId9,'-', '1.0.0')))]", + "version": "1.0.0" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('huntingQueryObject10').huntingQueryTemplateSpecName10]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleExternalDeviceInventory_HuntingQueries Hunting Query with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('huntingQueryObject10').huntingQueryVersion10]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.OperationalInsights/savedSearches", + "apiVersion": "2025-07-01", + "name": "Tailscale_(CCF)_Hunting_Query_10", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Tailscale: External (shared-in) device inventory", + "category": "Hunting Queries", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where IsExternal == true\n| project DeviceName, Hostname, User, Os, ClientVersion, Created, LastSeen, Tags\n| order by Created desc\n", + "version": 2, + "tags": [ + { + "name": "description", + "value": "Identifies external (shared-in) devices currently active in the tailnet. These devices belong to another tailnet and have been admitted via a Tailscale sharing arrangement." + }, + { + "name": "tactics", + "value": "InitialAccess" + }, + { + "name": "techniques", + "value": "T1078" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject10')._huntingQuerycontentId10),'/'))))]", + "properties": { + "description": "Tailscale (CCF) Hunting Query 10", + "parentId": "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject10')._huntingQuerycontentId10)]", + "contentId": "[variables('huntingQueryObject10')._huntingQuerycontentId10]", + "kind": "HuntingQuery", + "version": "[variables('huntingQueryObject10').huntingQueryVersion10]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('huntingQueryObject10')._huntingQuerycontentId10]", + "contentKind": "HuntingQuery", + "displayName": "Tailscale: External (shared-in) device inventory", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject10')._huntingQuerycontentId10,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject10')._huntingQuerycontentId10,'-', '1.0.0')))]", + "version": "1.0.0" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('huntingQueryObject11').huntingQueryTemplateSpecName11]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleOutdatedClients_HuntingQueries Hunting Query with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('huntingQueryObject11').huntingQueryVersion11]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.OperationalInsights/savedSearches", + "apiVersion": "2025-07-01", + "name": "Tailscale_(CCF)_Hunting_Query_11", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Tailscale: Devices with outdated client version", + "category": "Hunting Queries", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where UpdateAvailable == true\n| project DeviceName, Hostname, User, Os, Distro, ClientVersion, LastSeen, Tags\n| order by ClientVersion asc, LastSeen desc\n", + "version": 2, + "tags": [ + { + "name": "description", + "value": "Identifies tailnet devices that report UpdateAvailable=true on the latest snapshot. Tailscale releases security updates regularly; outdated clients lack the most recent improvements." + }, + { + "name": "tactics", + "value": "DefenseEvasion" + }, + { + "name": "techniques", + "value": "T1562" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject11')._huntingQuerycontentId11),'/'))))]", + "properties": { + "description": "Tailscale (CCF) Hunting Query 11", + "parentId": "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject11')._huntingQuerycontentId11)]", + "contentId": "[variables('huntingQueryObject11')._huntingQuerycontentId11]", + "kind": "HuntingQuery", + "version": "[variables('huntingQueryObject11').huntingQueryVersion11]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('huntingQueryObject11')._huntingQuerycontentId11]", + "contentKind": "HuntingQuery", + "displayName": "Tailscale: Devices with outdated client version", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject11')._huntingQuerycontentId11,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject11')._huntingQuerycontentId11,'-', '1.0.0')))]", + "version": "1.0.0" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('huntingQueryObject12').huntingQueryTemplateSpecName12]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleSubnetRouteExposure_HuntingQueries Hunting Query with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('huntingQueryObject12').huntingQueryVersion12]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.OperationalInsights/savedSearches", + "apiVersion": "2025-07-01", + "name": "Tailscale_(CCF)_Hunting_Query_12", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Tailscale: Subnet router CIDR exposure inventory", + "category": "Hunting Queries", + "query": "let exitRoutes = dynamic([\"0.0.0.0/0\", \"::/0\"]);\nTailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where array_length(AdvertisedRoutes) > 0 or array_length(EnabledRoutes) > 0\n| extend SubnetAdvertised = set_difference(AdvertisedRoutes, exitRoutes)\n| extend SubnetEnabled = set_difference(EnabledRoutes, exitRoutes)\n| where array_length(SubnetAdvertised) > 0 or array_length(SubnetEnabled) > 0\n| project DeviceName, Hostname, User, Os, SubnetAdvertised, SubnetEnabled, Tags, LastSeen\n| order by DeviceName asc\n", + "version": 2, + "tags": [ + { + "name": "description", + "value": "Identifies every device currently advertising or enabling subnet routes (bridging non-tailnet networks into the tailnet). Excludes pure exit-node advertisements so only true subnet exposure is surfaced." + }, + { + "name": "tactics", + "value": "LateralMovement" + }, + { + "name": "techniques", + "value": "T1021,T1018" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject12')._huntingQuerycontentId12),'/'))))]", + "properties": { + "description": "Tailscale (CCF) Hunting Query 12", + "parentId": "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject12')._huntingQuerycontentId12)]", + "contentId": "[variables('huntingQueryObject12')._huntingQuerycontentId12]", + "kind": "HuntingQuery", + "version": "[variables('huntingQueryObject12').huntingQueryVersion12]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('huntingQueryObject12')._huntingQuerycontentId12]", + "contentKind": "HuntingQuery", + "displayName": "Tailscale: Subnet router CIDR exposure inventory", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject12')._huntingQuerycontentId12,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject12')._huntingQuerycontentId12,'-', '1.0.0')))]", + "version": "1.0.0" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('huntingQueryObject13').huntingQueryTemplateSpecName13]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscalePremiumNewNodePairs_HuntingQueries Hunting Query with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('huntingQueryObject13').huntingQueryVersion13]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.OperationalInsights/savedSearches", + "apiVersion": "2025-07-01", + "name": "Tailscale_(CCF)_Hunting_Query_13", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Tailscale Premium: New src->dst node pairs (lateral movement candidates)", + "category": "Hunting Queries", + "query": "let recent = 1d;\nlet baseline = 7d;\nlet recentPairs =\n Tailscale_Network_CL\n | where TimeGenerated > ago(recent)\n | mv-expand t = VirtualTraffic\n | extend Src = tostring(t.src), Dst = tostring(t.dst), Proto = toint(t.proto)\n | summarize FirstSeen = min(TimeGenerated), TxBytes = sum(tolong(t.txBytes)), RxBytes = sum(tolong(t.rxBytes)), FlowCount = count() by Src, Dst, Proto;\nlet baselinePairs =\n Tailscale_Network_CL\n | where TimeGenerated between (ago(baseline + recent) .. ago(recent))\n | mv-expand t = VirtualTraffic\n | extend Src = tostring(t.src), Dst = tostring(t.dst), Proto = toint(t.proto)\n | distinct Src, Dst, Proto;\nrecentPairs\n| join kind=leftanti baselinePairs on Src, Dst, Proto\n| order by FirstSeen asc\n", + "version": 2, + "tags": [ + { + "name": "description", + "value": "Identifies tailnet src->dst pairs observed in the last 24h that were NOT observed in the prior 7-day baseline. Useful for spotting lateral movement to nodes that don't usually talk." + }, + { + "name": "tactics", + "value": "LateralMovement,Discovery" + }, + { + "name": "techniques", + "value": "T1021,T1018" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject13')._huntingQuerycontentId13),'/'))))]", + "properties": { + "description": "Tailscale (CCF) Hunting Query 13", + "parentId": "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject13')._huntingQuerycontentId13)]", + "contentId": "[variables('huntingQueryObject13')._huntingQuerycontentId13]", + "kind": "HuntingQuery", + "version": "[variables('huntingQueryObject13').huntingQueryVersion13]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('huntingQueryObject13')._huntingQuerycontentId13]", + "contentKind": "HuntingQuery", + "displayName": "Tailscale Premium: New src->dst node pairs (lateral movement candidates)", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject13')._huntingQuerycontentId13,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject13')._huntingQuerycontentId13,'-', '1.0.0')))]", + "version": "1.0.0" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('huntingQueryObject14').huntingQueryTemplateSpecName14]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscalePremiumTopTalkers_HuntingQueries Hunting Query with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('huntingQueryObject14').huntingQueryVersion14]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.OperationalInsights/savedSearches", + "apiVersion": "2025-07-01", + "name": "Tailscale_(CCF)_Hunting_Query_14", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Tailscale Premium: Top talkers by bytes (virtual traffic)", + "category": "Hunting Queries", + "query": "Tailscale_Network_CL\n| where TimeGenerated > ago(1d)\n| mv-expand t = VirtualTraffic\n| extend Src = tostring(t.src), Dst = tostring(t.dst), Proto = toint(t.proto), TxBytes = tolong(t.txBytes), RxBytes = tolong(t.rxBytes)\n| summarize TotalBytes = sum(TxBytes + RxBytes), TotalPackets = sum(tolong(t.txPkts) + tolong(t.rxPkts)), FlowCount = count() by Src, Dst, Proto\n| extend TotalMB = round(TotalBytes / 1024.0 / 1024.0, 2)\n| top 50 by TotalBytes\n", + "version": 2, + "tags": [ + { + "name": "description", + "value": "Identifies tailnet src->dst pairs ranked by total bytes transferred over the last 24h. Useful for capacity planning, identifying data-heavy flows, and spotting unexpected volume that could indicate data staging. Requires Tailscale Premium or Enterprise." + }, + { + "name": "tactics", + "value": "Exfiltration,Collection" + }, + { + "name": "techniques", + "value": "T1041,T1567" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject14')._huntingQuerycontentId14),'/'))))]", + "properties": { + "description": "Tailscale (CCF) Hunting Query 14", + "parentId": "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject14')._huntingQuerycontentId14)]", + "contentId": "[variables('huntingQueryObject14')._huntingQuerycontentId14]", + "kind": "HuntingQuery", + "version": "[variables('huntingQueryObject14').huntingQueryVersion14]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('huntingQueryObject14')._huntingQuerycontentId14]", + "contentKind": "HuntingQuery", + "displayName": "Tailscale Premium: Top talkers by bytes (virtual traffic)", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject14')._huntingQuerycontentId14,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject14')._huntingQuerycontentId14,'-', '1.0.0')))]", + "version": "1.0.0" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('huntingQueryObject15').huntingQueryTemplateSpecName15]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscalePremiumExitNodeUsage_HuntingQueries Hunting Query with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('huntingQueryObject15').huntingQueryVersion15]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.OperationalInsights/savedSearches", + "apiVersion": "2025-07-01", + "name": "Tailscale_(CCF)_Hunting_Query_15", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Tailscale Premium: Exit-node usage patterns", + "category": "Hunting Queries", + "query": "Tailscale_Network_CL\n| where TimeGenerated > ago(1d)\n| where array_length(ExitTraffic) > 0\n| mv-expand t = ExitTraffic\n| extend Src = tostring(t.src), ExitDst = tostring(t.dst), Proto = toint(t.proto), TxBytes = tolong(t.txBytes), RxBytes = tolong(t.rxBytes)\n| summarize TotalBytes = sum(TxBytes + RxBytes), FlowCount = count(), ExitDestinations = make_set(ExitDst, 25)\n by NodeId, SrcNodeName = tostring(SrcNode.name), Src\n| extend TotalMB = round(TotalBytes / 1024.0 / 1024.0, 2)\n| order by TotalBytes desc\n| take 100\n", + "version": 2, + "tags": [ + { + "name": "description", + "value": "Identifies traffic leaving the tailnet via exit nodes. Exit-node use is typically intentional (regional egress, privacy routing) but unexpected egress from a node warrants investigation." + }, + { + "name": "tactics", + "value": "CommandAndControl,Exfiltration" + }, + { + "name": "techniques", + "value": "T1090,T1041" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject15')._huntingQuerycontentId15),'/'))))]", + "properties": { + "description": "Tailscale (CCF) Hunting Query 15", + "parentId": "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject15')._huntingQuerycontentId15)]", + "contentId": "[variables('huntingQueryObject15')._huntingQuerycontentId15]", + "kind": "HuntingQuery", + "version": "[variables('huntingQueryObject15').huntingQueryVersion15]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('huntingQueryObject15')._huntingQuerycontentId15]", + "contentKind": "HuntingQuery", + "displayName": "Tailscale Premium: Exit-node usage patterns", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject15')._huntingQuerycontentId15,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject15')._huntingQuerycontentId15,'-', '1.0.0')))]", + "version": "1.0.0" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('huntingQueryObject16').huntingQueryTemplateSpecName16]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscalePremiumBeaconingCandidates_HuntingQueries Hunting Query with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('huntingQueryObject16').huntingQueryVersion16]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.OperationalInsights/savedSearches", + "apiVersion": "2025-07-01", + "name": "Tailscale_(CCF)_Hunting_Query_16", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Tailscale Premium: Beaconing candidates (regular periodic flows)", + "category": "Hunting Queries", + "query": "let lookback = 2d;\nlet minFlows = 10;\nlet beaconPercentThreshold = 80.0;\nTailscale_Network_CL\n| where TimeGenerated > ago(lookback)\n| mv-expand t = VirtualTraffic\n| extend Src = tostring(t.src), Dst = tostring(t.dst), Proto = toint(t.proto)\n| project TimeGenerated, Src, Dst, Proto\n| sort by Src asc, Dst asc, Proto asc, TimeGenerated asc\n| serialize\n| extend NextTime = next(TimeGenerated), NextSrc = next(Src), NextDst = next(Dst), NextProto = next(Proto)\n| where Src == NextSrc and Dst == NextDst and Proto == NextProto\n| extend DeltaSec = datetime_diff('second', NextTime, TimeGenerated)\n| where DeltaSec > 5\n| summarize DeltaCount = count() by Src, Dst, Proto, DeltaSec\n| summarize (MostFrequentDeltaCount, MostFrequentDeltaSec) = arg_max(DeltaCount, DeltaSec), TotalFlows = sum(DeltaCount) by Src, Dst, Proto\n| where TotalFlows >= minFlows\n| extend BeaconPercent = round(MostFrequentDeltaCount * 100.0 / TotalFlows, 1)\n| where BeaconPercent >= beaconPercentThreshold\n| order by BeaconPercent desc\n", + "version": 2, + "tags": [ + { + "name": "description", + "value": "Identifies flows that recur at a highly regular interval, which is the signature of C2 beaconing or scheduled exfiltration jobs. Looser threshold than the analytic rule - investigation aid." + }, + { + "name": "tactics", + "value": "CommandAndControl,Exfiltration" + }, + { + "name": "techniques", + "value": "T1071,T1095,T1029" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject16')._huntingQuerycontentId16),'/'))))]", + "properties": { + "description": "Tailscale (CCF) Hunting Query 16", + "parentId": "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject16')._huntingQuerycontentId16)]", + "contentId": "[variables('huntingQueryObject16')._huntingQuerycontentId16]", + "kind": "HuntingQuery", + "version": "[variables('huntingQueryObject16').huntingQueryVersion16]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('huntingQueryObject16')._huntingQuerycontentId16]", + "contentKind": "HuntingQuery", + "displayName": "Tailscale Premium: Beaconing candidates (regular periodic flows)", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject16')._huntingQuerycontentId16,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject16')._huntingQuerycontentId16,'-', '1.0.0')))]", + "version": "1.0.0" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('huntingQueryObject17').huntingQueryTemplateSpecName17]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscalePremiumPostureInventory_HuntingQueries Hunting Query with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('huntingQueryObject17').huntingQueryVersion17]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.OperationalInsights/savedSearches", + "apiVersion": "2025-07-01", + "name": "Tailscale_(CCF)_Hunting_Query_17", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Tailscale Premium: Current posture integration inventory", + "category": "Hunting Queries", + "query": "Tailscale_PostureIntegrations_CL\n| summarize arg_max(TimeGenerated, *) by IntegrationId\n| project IntegrationId, Provider, ClientId, TenantId_Provider, Status, ConfigOverwrites\n| order by Provider asc\n", + "version": 2, + "tags": [ + { + "name": "description", + "value": "Identifies the current set of device-posture integrations configured on the tailnet (latest snapshot per integration). Useful for compliance attestation and detecting drift from the expected baseline." + }, + { + "name": "tactics", + "value": "DefenseEvasion" + }, + { + "name": "techniques", + "value": "T1562" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject17')._huntingQuerycontentId17),'/'))))]", + "properties": { + "description": "Tailscale (CCF) Hunting Query 17", + "parentId": "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject17')._huntingQuerycontentId17)]", + "contentId": "[variables('huntingQueryObject17')._huntingQuerycontentId17]", + "kind": "HuntingQuery", + "version": "[variables('huntingQueryObject17').huntingQueryVersion17]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('huntingQueryObject17')._huntingQuerycontentId17]", + "contentKind": "HuntingQuery", + "displayName": "Tailscale Premium: Current posture integration inventory", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject17')._huntingQuerycontentId17,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject17')._huntingQuerycontentId17,'-', '1.0.0')))]", + "version": "1.0.0" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('huntingQueryObject18').huntingQueryTemplateSpecName18]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscalePremiumDerpRelayPersistence_HuntingQueries Hunting Query with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('huntingQueryObject18').huntingQueryVersion18]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.OperationalInsights/savedSearches", + "apiVersion": "2025-07-01", + "name": "Tailscale_(CCF)_Hunting_Query_18", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Tailscale Premium: Devices with persistent DERP relay usage", + "category": "Hunting Queries", + "query": "Tailscale_Network_CL\n| where TimeGenerated > ago(24h)\n| summarize\n TotalFlows = count(),\n RelayedFlows = countif(IsRelayed),\n DistinctDsts = dcount(DstNodeName),\n FirstFlow = min(TimeGenerated),\n LastFlow = max(TimeGenerated)\n by SrcNodeName, SrcUser, SrcOs, SrcTags=tostring(SrcTags)\n| where TotalFlows >= 50\n| extend RelayedPct = round(100.0 * RelayedFlows / TotalFlows, 1)\n| where RelayedPct >= 30.0\n| project SrcNodeName, SrcUser, SrcOs, SrcTags, TotalFlows, RelayedFlows, RelayedPct, DistinctDsts, FirstFlow, LastFlow\n| order by RelayedPct desc, TotalFlows desc\n", + "version": 2, + "tags": [ + { + "name": "description", + "value": "Identifies devices that have consistently fallen back to DERP relay (IsRelayed=true) over the past 24 hours. Sustained relay usage points to NAT/firewall misconfiguration or deliberate evasion." + }, + { + "name": "tactics", + "value": "CommandAndControl" + }, + { + "name": "techniques", + "value": "T1572" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject18')._huntingQuerycontentId18),'/'))))]", + "properties": { + "description": "Tailscale (CCF) Hunting Query 18", + "parentId": "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject18')._huntingQuerycontentId18)]", + "contentId": "[variables('huntingQueryObject18')._huntingQuerycontentId18]", + "kind": "HuntingQuery", + "version": "[variables('huntingQueryObject18').huntingQueryVersion18]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('huntingQueryObject18')._huntingQuerycontentId18]", + "contentKind": "HuntingQuery", + "displayName": "Tailscale Premium: Devices with persistent DERP relay usage", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject18')._huntingQuerycontentId18,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject18')._huntingQuerycontentId18,'-', '1.0.0')))]", + "version": "1.0.0" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('huntingQueryObject19').huntingQueryTemplateSpecName19]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscalePremiumTaggedServiceFanIn_HuntingQueries Hunting Query with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('huntingQueryObject19').huntingQueryVersion19]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.OperationalInsights/savedSearches", + "apiVersion": "2025-07-01", + "name": "Tailscale_(CCF)_Hunting_Query_19", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Tailscale Premium: Tagged services with broad inbound exposure", + "category": "Hunting Queries", + "query": "Tailscale_Network_CL\n| where TimeGenerated > ago(7d)\n| where isnotempty(DstTags)\n| where HasVirtualTraffic or HasSubnetTraffic\n| summarize\n DistinctSrcUsers = dcount(SrcUser),\n DistinctSrcDevices = dcount(SrcNodeName),\n DistinctSrcOs = dcount(SrcOs),\n Flows = count(),\n FirstFlow = min(TimeGenerated),\n LastFlow = max(TimeGenerated)\n by DstNodeName, DstTags=tostring(DstTags)\n| extend ShortDstName = tostring(split(DstNodeName, \".\")[0])\n| project ShortDstName, DstTags, DistinctSrcDevices, DistinctSrcUsers, DistinctSrcOs, Flows, FirstFlow, LastFlow\n| order by DistinctSrcDevices desc, Flows desc\n", + "version": 2, + "tags": [ + { + "name": "description", + "value": "Identifies tagged services (devices with non-empty DstTags) ranked by inbound diversity over 7 days. Surfaces services with the broadest blast-radius; ACL drift candidates." + }, + { + "name": "tactics", + "value": "LateralMovement,InitialAccess" + }, + { + "name": "techniques", + "value": "T1021,T1133" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject19')._huntingQuerycontentId19),'/'))))]", + "properties": { + "description": "Tailscale (CCF) Hunting Query 19", + "parentId": "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject19')._huntingQuerycontentId19)]", + "contentId": "[variables('huntingQueryObject19')._huntingQuerycontentId19]", + "kind": "HuntingQuery", + "version": "[variables('huntingQueryObject19').huntingQueryVersion19]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('huntingQueryObject19')._huntingQuerycontentId19]", + "contentKind": "HuntingQuery", + "displayName": "Tailscale Premium: Tagged services with broad inbound exposure", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject19')._huntingQuerycontentId19,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject19')._huntingQuerycontentId19,'-', '1.0.0')))]", + "version": "1.0.0" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('huntingQueryObject20').huntingQueryTemplateSpecName20]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscalePremiumCrossTagFlowMatrix_HuntingQueries Hunting Query with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('huntingQueryObject20').huntingQueryVersion20]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.OperationalInsights/savedSearches", + "apiVersion": "2025-07-01", + "name": "Tailscale_(CCF)_Hunting_Query_20", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Tailscale Premium: Cross-tag flow matrix", + "category": "Hunting Queries", + "query": "Tailscale_Network_CL\n| where TimeGenerated > ago(7d)\n| extend SrcCategory = case(\n isnotempty(SrcTags), tostring(SrcTags),\n isnotempty(SrcUser), \"\",\n \"\")\n| extend DstCategory = case(\n isnotempty(DstTags), tostring(DstTags),\n isnotempty(DstUser), \"\",\n \"\")\n| summarize\n Flows = count(),\n DistinctSrcDevices = dcount(SrcNodeName),\n DistinctDstDevices = dcount(DstNodeName),\n FirstSeen = min(TimeGenerated),\n LastSeen = max(TimeGenerated)\n by SrcCategory, DstCategory\n| extend SameTagLoop = SrcCategory == DstCategory and SrcCategory != \"\" and SrcCategory != \"\"\n| project SrcCategory, DstCategory, Flows, DistinctSrcDevices, DistinctDstDevices, SameTagLoop, FirstSeen, LastSeen\n| order by Flows desc\n", + "version": 2, + "tags": [ + { + "name": "description", + "value": "Identifies network flows pivoted by source-tag x destination-tag over 7 days. Highlights tag-to-tag traffic, useful for ACL validation. Same-tag loops can signal worm-style propagation." + }, + { + "name": "tactics", + "value": "LateralMovement,Discovery" + }, + { + "name": "techniques", + "value": "T1021,T1046" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject20')._huntingQuerycontentId20),'/'))))]", + "properties": { + "description": "Tailscale (CCF) Hunting Query 20", + "parentId": "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject20')._huntingQuerycontentId20)]", + "contentId": "[variables('huntingQueryObject20')._huntingQuerycontentId20]", + "kind": "HuntingQuery", + "version": "[variables('huntingQueryObject20').huntingQueryVersion20]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('huntingQueryObject20')._huntingQuerycontentId20]", + "contentKind": "HuntingQuery", + "displayName": "Tailscale Premium: Cross-tag flow matrix", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject20')._huntingQuerycontentId20,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject20')._huntingQuerycontentId20,'-', '1.0.0')))]", + "version": "1.0.0" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('huntingQueryObject21').huntingQueryTemplateSpecName21]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscalePremiumOffHoursFlows_HuntingQueries Hunting Query with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('huntingQueryObject21').huntingQueryVersion21]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.OperationalInsights/savedSearches", + "apiVersion": "2025-07-01", + "name": "Tailscale_(CCF)_Hunting_Query_21", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Tailscale Premium: Network flows outside business hours", + "category": "Hunting Queries", + "query": "Tailscale_Network_CL\n| where TimeGenerated > ago(7d)\n| where HasVirtualTraffic or HasSubnetTraffic or HasExitTraffic\n| extend HourUtc = hourofday(TimeGenerated), Dow = dayofweek(TimeGenerated)\n| where HourUtc < 7 or HourUtc >= 19 or Dow in (0d, 6d)\n| extend TaggedSource = isnotempty(SrcTags)\n| summarize\n Flows = count(),\n FirstFlow = min(TimeGenerated),\n LastFlow = max(TimeGenerated),\n DistinctHours = dcount(bin(TimeGenerated, 1h))\n by SrcNodeName, SrcUser, SrcTags=tostring(SrcTags), DstNodeName, DstTags=tostring(DstTags), TaggedSource\n| where Flows >= 5\n| project SrcNodeName, SrcUser, SrcTags, TaggedSource, DstNodeName, DstTags, Flows, DistinctHours, FirstFlow, LastFlow\n| order by Flows desc\n", + "version": 2, + "tags": [ + { + "name": "description", + "value": "Identifies network flows occurring outside 07:00-19:00 UTC on weekdays plus all weekend, over 7 days. Filters to virtual/subnet/exit traffic. Useful for spotting unattended automation gone wrong." + }, + { + "name": "tactics", + "value": "Exfiltration,CommandAndControl" + }, + { + "name": "techniques", + "value": "T1029,T1071" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject21')._huntingQuerycontentId21),'/'))))]", + "properties": { + "description": "Tailscale (CCF) Hunting Query 21", + "parentId": "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject21')._huntingQuerycontentId21)]", + "contentId": "[variables('huntingQueryObject21')._huntingQuerycontentId21]", + "kind": "HuntingQuery", + "version": "[variables('huntingQueryObject21').huntingQueryVersion21]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('huntingQueryObject21')._huntingQuerycontentId21]", + "contentKind": "HuntingQuery", + "displayName": "Tailscale Premium: Network flows outside business hours", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject21')._huntingQuerycontentId21,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject21')._huntingQuerycontentId21,'-', '1.0.0')))]", + "version": "1.0.0" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('huntingQueryObject22').huntingQueryTemplateSpecName22]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscalePremiumUserMultiDevice_HuntingQueries Hunting Query with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('huntingQueryObject22').huntingQueryVersion22]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.OperationalInsights/savedSearches", + "apiVersion": "2025-07-01", + "name": "Tailscale_(CCF)_Hunting_Query_22", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "Tailscale Premium: Users generating traffic from multiple devices", + "category": "Hunting Queries", + "query": "let recent = Tailscale_Network_CL\n | where TimeGenerated > ago(24h)\n | where isnotempty(SrcUser)\n | summarize\n Devices = make_set(SrcNodeName, 20),\n DeviceCount = dcount(SrcNodeName),\n OsTypes = make_set(SrcOs, 10),\n FirstFlow = min(TimeGenerated),\n LastFlow = max(TimeGenerated),\n Flows = count()\n by SrcUser\n | where DeviceCount >= 2;\nlet devicesCreatedToday = Tailscale_Devices_CL\n | where TimeGenerated > ago(24h)\n | summarize arg_max(TimeGenerated, *) by DeviceId\n | where Created > ago(24h)\n | distinct DeviceName;\nrecent\n| extend NewDevicesToday = set_intersect(Devices, toscalar(devicesCreatedToday | summarize make_set(DeviceName)))\n| extend HasNewDevice = array_length(NewDevicesToday) > 0\n| project SrcUser, DeviceCount, Devices, OsTypes, HasNewDevice, NewDevicesToday, Flows, FirstFlow, LastFlow\n| order by HasNewDevice desc, DeviceCount desc\n", + "version": 2, + "tags": [ + { + "name": "description", + "value": "Identifies users (SrcUser) generating tailnet flows from more than one distinct device in the past 24 hours. Useful for spotting account compromise (sudden new device) or unauthorised device enrollment." + }, + { + "name": "tactics", + "value": "InitialAccess,Persistence" + }, + { + "name": "techniques", + "value": "T1078" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject22')._huntingQuerycontentId22),'/'))))]", + "properties": { + "description": "Tailscale (CCF) Hunting Query 22", + "parentId": "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject22')._huntingQuerycontentId22)]", + "contentId": "[variables('huntingQueryObject22')._huntingQuerycontentId22]", + "kind": "HuntingQuery", + "version": "[variables('huntingQueryObject22').huntingQueryVersion22]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('huntingQueryObject22')._huntingQuerycontentId22]", + "contentKind": "HuntingQuery", + "displayName": "Tailscale Premium: Users generating traffic from multiple devices", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject22')._huntingQuerycontentId22,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject22')._huntingQuerycontentId22,'-', '1.0.0')))]", + "version": "1.0.0" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('workbookTemplateSpecName1')]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscaleStandardOperations Workbook with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('workbookVersion1')]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.Insights/workbooks", + "name": "[variables('workbookContentId1')]", + "location": "[parameters('workspace-location')]", + "kind": "shared", + "apiVersion": "2021-08-01", + "metadata": { + "description": "Tailscale Operations workbook for Standard tier. Nine tabs covering an at-a-glance KPI hero row, audit activity overview, an actor + device drilldown (Investigate), embedded hunting queries (first-seen actors, off-hours changes, key-expiry-disabled devices, never-expire auth keys, outdated clients, dormant devices, subnet route exposure, SSH-enabled devices), identity (user roles, status, last login recency, role escalation history, orphaned users), devices (OS / version / tag distribution, devices needing attention, full inventory, subnet routers), credentials (expiry timeline, never-expire flag, CRUD events), admin audit (action heatmap, actor x action heatmap, recent 100), network and DNS (current snapshot, tailnet policy gates, ACL change history), and pipeline health (per-table freshness, ingest rate, operational events). Driven by data polled from the Tailscale REST API." + }, + "properties": { + "displayName": "[parameters('workbook1-name')]", + "serializedData": "{\"version\":\"Notebook/1.0\",\"items\":[{\"type\":9,\"content\":{\"version\":\"KqlParameterItem/1.0\",\"parameters\":[{\"id\":\"7b05a598-5120-43f4-bf5d-576c2a7ff28d\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"TimeRange\",\"type\":4,\"isRequired\":true,\"value\":{\"durationMs\":86400000},\"typeSettings\":{\"selectableValues\":[{\"durationMs\":3600000},{\"durationMs\":14400000},{\"durationMs\":43200000},{\"durationMs\":86400000},{\"durationMs\":172800000},{\"durationMs\":604800000},{\"durationMs\":2592000000}]}}],\"style\":\"pills\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\"},\"name\":\"parameters\"},{\"type\":1,\"content\":{\"json\":\"
Tailscale Operations (Standard)
Single-pane visibility into your Tailscale tailnet on Personal (Free), Starter and Standard tiers: who, what, when, where, and what changed. Scope every panel with the time range below; the Investigate tab adds Actor and Device pickers for drilldown. Premium-tier panels (network flow logs, posture integrations) live in the separate Tailscale Operations (Premium) workbook.
\"},\"name\":\"header\"},{\"type\":11,\"content\":{\"version\":\"LinkItem/1.0\",\"style\":\"tabs\",\"links\":[{\"id\":\"9794e9fd-916b-494d-8e72-af63d2f4c6c7\",\"cellValue\":\"selectedTab\",\"linkTarget\":\"parameter\",\"linkLabel\":\"Overview\",\"subTarget\":\"overview\",\"style\":\"link\"},{\"id\":\"71cf49db-33c5-4d4b-920a-2ec0c6a258dc\",\"cellValue\":\"selectedTab\",\"linkTarget\":\"parameter\",\"linkLabel\":\"Investigate\",\"subTarget\":\"investigate\",\"style\":\"link\"},{\"id\":\"5c56bb37-2053-47a0-b6fd-9539768c144d\",\"cellValue\":\"selectedTab\",\"linkTarget\":\"parameter\",\"linkLabel\":\"Hunts\",\"subTarget\":\"hunts\",\"style\":\"link\"},{\"id\":\"d2004ded-07f8-446a-a720-f0a63d1d9dda\",\"cellValue\":\"selectedTab\",\"linkTarget\":\"parameter\",\"linkLabel\":\"Identity\",\"subTarget\":\"identity\",\"style\":\"link\"},{\"id\":\"f23b3e14-1511-4a29-bf5e-bd65e55dbb40\",\"cellValue\":\"selectedTab\",\"linkTarget\":\"parameter\",\"linkLabel\":\"Devices\",\"subTarget\":\"devices\",\"style\":\"link\"},{\"id\":\"724f8352-e21b-45a6-9029-39dc92693c05\",\"cellValue\":\"selectedTab\",\"linkTarget\":\"parameter\",\"linkLabel\":\"Credentials\",\"subTarget\":\"credentials\",\"style\":\"link\"},{\"id\":\"8400ec64-e118-44e0-ae29-84afe94b8e0e\",\"cellValue\":\"selectedTab\",\"linkTarget\":\"parameter\",\"linkLabel\":\"Admin Audit\",\"subTarget\":\"audit\",\"style\":\"link\"},{\"id\":\"b7e04861-edfa-4426-b6be-f1481ca569b6\",\"cellValue\":\"selectedTab\",\"linkTarget\":\"parameter\",\"linkLabel\":\"Network & DNS\",\"subTarget\":\"network\",\"style\":\"link\"},{\"id\":\"cfbc96e4-8585-4d89-8f65-8344c8cc6eb2\",\"cellValue\":\"selectedTab\",\"linkTarget\":\"parameter\",\"linkLabel\":\"Pipeline Health\",\"subTarget\":\"pipeline\",\"style\":\"link\"}]},\"name\":\"tabs\"},{\"type\":12,\"content\":{\"version\":\"NotebookGroup/1.0\",\"groupType\":\"editable\",\"items\":[{\"type\":1,\"content\":{\"json\":\"
Tailnet at a glance
\"},\"name\":\"div-tailnet-at-a-glance-04e62b\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"let DEV = Tailscale_Devices_CL | summarize arg_max(TimeGenerated, *) by DeviceId;\\nlet USR = Tailscale_Users_CL | summarize arg_max(TimeGenerated, *) by UserId;\\nlet KEY = Tailscale_Keys_CL | summarize arg_max(TimeGenerated, *) by KeyId;\\nunion\\n (DEV | summarize V=toreal(count()) | extend Metric=\\\"Devices\\\", Order=1),\\n (DEV | where Authorized == true | summarize V=toreal(count()) | extend Metric=\\\"Authorized\\\", Order=2),\\n (DEV | where UpdateAvailable == true | summarize V=toreal(count()) | extend Metric=\\\"Updates Available\\\", Order=3),\\n (DEV | where SshEnabled == true | summarize V=toreal(count()) | extend Metric=\\\"SSH-Enabled\\\", Order=4),\\n (USR | summarize V=toreal(count()) | extend Metric=\\\"Users\\\", Order=5),\\n (USR | where Role =~ \\\"admin\\\" or Role =~ \\\"owner\\\" or Role =~ \\\"network-admin\\\" | summarize V=toreal(count()) | extend Metric=\\\"Admins\\\", Order=6),\\n (KEY | where isnull(Revoked) and (isnull(Expires) or Expires > now()) | summarize V=toreal(count()) | extend Metric=\\\"Active Keys\\\", Order=7),\\n (Tailscale_Audit_CL | where TimeGenerated {TimeRange} | summarize V=toreal(count()) | extend Metric=\\\"Audit Events ({TimeRange:label})\\\", Order=8)\\n| order by Order asc | project Metric, Value=V\",\"size\":3,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"tiles\",\"tileSettings\":{\"titleContent\":{\"columnMatch\":\"Metric\",\"formatter\":1},\"leftContent\":{\"columnMatch\":\"Value\",\"formatter\":12,\"formatOptions\":{\"palette\":\"auto\"}},\"showBorder\":false}},\"name\":\"q-4e9d7cff\"},{\"type\":1,\"content\":{\"json\":\"
Audit activity over time
\"},\"name\":\"div-audit-activity-over--546688\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| summarize EventCount = count() by bin(TimeGenerated, 1h), Action\\n| order by TimeGenerated asc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"timechart\",\"title\":\"Audit events by action\",\"noDataMessage\":\"No audit events in the selected window. Widen the time range; remember the Tailscale audit poll runs every ~30 min.\",\"noDataMessageStyle\":5},\"name\":\"q-effcd498\"},{\"type\":1,\"content\":{\"json\":\"
Who's doing what
\"},\"name\":\"div-who's-doing-what-52144e\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend Actor=tostring(coalesce(Actor.loginName, Actor.displayName, Actor.type))\\n| where isnotempty(Actor)\\n| summarize Events=count(), DistinctActions=dcount(Action), LastSeen=max(TimeGenerated) by Actor\\n| order by Events desc | take 15\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Top actors (by event count)\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Events\",\"formatter\":8,\"formatOptions\":{\"palette\":\"blue\"}},{\"columnMatch\":\"LastSeen\",\"formatter\":6}]}},\"name\":\"q-34c77fa9\",\"customWidth\":\"50\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend TargetType=tostring(Target.type)\\n| where isnotempty(TargetType)\\n| summarize Events=count() by TargetType\\n| order by Events desc | take 15\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"piechart\",\"title\":\"Activity by target type\"},\"name\":\"q-4b3b709d\",\"customWidth\":\"50\"},{\"type\":1,\"content\":{\"json\":\"
Recent admin events
\"},\"name\":\"div-recent-admin-events-87c9e0\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend Actor=tostring(coalesce(Actor.loginName, Actor.displayName, Actor.type))\\n| extend TargetType=tostring(Target.type), TargetName=tostring(coalesce(Target.name, Target.id))\\n| project TimeGenerated, Action, Actor, TargetType, TargetName, Origin\\n| order by TimeGenerated desc | take 30\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Most recent 30 audit events\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"TimeGenerated\",\"formatter\":6}]}},\"name\":\"q-5e7d6306\"}]},\"conditionalVisibility\":{\"parameterName\":\"selectedTab\",\"comparison\":\"isEqualTo\",\"value\":\"overview\"},\"name\":\"group-overview\"},{\"type\":12,\"content\":{\"version\":\"NotebookGroup/1.0\",\"groupType\":\"editable\",\"items\":[{\"type\":9,\"content\":{\"version\":\"KqlParameterItem/1.0\",\"parameters\":[{\"id\":\"b83a25c1-da18-49a2-a444-6517f13d891c\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"SelectedActor\",\"label\":\"Actor\",\"type\":2,\"isRequired\":false,\"query\":\"let opts = Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\\n| where isnotempty(ActorLogin)\\n| summarize Events=count() by ActorLogin\\n| project value=ActorLogin, label=strcat(ActorLogin, \\\" (\\\", tostring(Events), \\\" events)\\\");\\n(print value=\\\"__ALL__\\\", label=\\\"(All actors)\\\")\\n| union opts\",\"typeSettings\":{\"showDefault\":false},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":\"__ALL__\"}],\"style\":\"pills\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\"},\"name\":\"investigate-picker-actor\"},{\"type\":1,\"content\":{\"json\":\"
Actor activity timeline
\"},\"name\":\"div-actor-activity-timel-5ec305\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\\n| where \\\"{SelectedActor}\\\" == \\\"__ALL__\\\" or ActorLogin == \\\"{SelectedActor}\\\"\\n| summarize Events=count() by bin(TimeGenerated, 1h), Action\\n| order by TimeGenerated asc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"timechart\",\"title\":\"Actions over time -- actor: {SelectedActor:label}\",\"noDataMessage\":\"Select an actor from the Actor dropdown above, or leave on 'All' to see total activity.\",\"noDataMessageStyle\":5},\"name\":\"q-32d40848\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\\n| where \\\"{SelectedActor}\\\" == \\\"__ALL__\\\" or ActorLogin == \\\"{SelectedActor}\\\"\\n| extend TargetType=tostring(Target.type), TargetName=tostring(coalesce(Target.name, Target.id))\\n| project TimeGenerated, ActorLogin, Action, TargetType, TargetName, Origin\\n| order by TimeGenerated desc | take 100\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Recent events for actor: {SelectedActor:label}\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"TimeGenerated\",\"formatter\":6}]},\"noDataMessage\":\"No events for this actor in the selected window.\",\"noDataMessageStyle\":5},\"name\":\"q-a742f6fd\"},{\"type\":9,\"content\":{\"version\":\"KqlParameterItem/1.0\",\"parameters\":[{\"id\":\"a9cf7907-f201-4725-a072-a8bd34bef74e\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"SelectedDevice\",\"label\":\"Device\",\"type\":2,\"isRequired\":false,\"query\":\"let opts = Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| order by LastSeen desc | take 100\\n| project value=DeviceName, label=strcat(coalesce(DeviceName, Hostname), \\\" (\\\", User, \\\")\\\");\\n(print value=\\\"__ALL__\\\", label=\\\"(All devices)\\\")\\n| union opts\",\"typeSettings\":{\"showDefault\":false},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":\"__ALL__\"}],\"style\":\"pills\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\"},\"name\":\"investigate-picker-device\"},{\"type\":1,\"content\":{\"json\":\"
Selected device timeline
\"},\"name\":\"div-selected-device-time-00bead\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| where \\\"{SelectedDevice}\\\" == \\\"__ALL__\\\" or DeviceName == \\\"{SelectedDevice}\\\"\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| extend OnlineNow = ClientConnectivity.endpoints != \\\"\\\" or ConnectedToControl == true\\n| project DeviceName, Hostname, User, Os, ClientVersion, UpdateAvailable, Authorized, IsExternal, SshEnabled, LastSeen, Expires, KeyExpiryDisabled, OnlineNow, Addresses, Tags, AdvertisedRoutes\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Summary for device: {SelectedDevice:label}\",\"noDataMessage\":\"Select a device from the Device dropdown above. Defaults to 'All'.\",\"noDataMessageStyle\":5},\"name\":\"q-11094dc8\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend TargetType=tostring(Target.type), TargetName=tostring(Target.name), TargetId=tostring(Target.id)\\n| where (\\\"{SelectedDevice}\\\" == \\\"__ALL__\\\" and TargetType == \\\"NODE\\\") or TargetName == \\\"{SelectedDevice}\\\"\\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\\n| project TimeGenerated, Action, ActorLogin, TargetName, TargetId, Origin\\n| order by TimeGenerated desc | take 100\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Audit events touching device: {SelectedDevice:label}\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"TimeGenerated\",\"formatter\":6}]},\"noDataMessage\":\"No audit events recorded against the selected device in this window. Tailscale tags device events with Target.type=NODE; the audit feed only emits NODE events on create/update/delete, so quiet devices stay quiet here.\",\"noDataMessageStyle\":5},\"name\":\"q-ead106ce\"}]},\"conditionalVisibility\":{\"parameterName\":\"selectedTab\",\"comparison\":\"isEqualTo\",\"value\":\"investigate\"},\"name\":\"group-investigate\"},{\"type\":12,\"content\":{\"version\":\"NotebookGroup/1.0\",\"groupType\":\"editable\",\"items\":[{\"type\":1,\"content\":{\"json\":\"
First-seen actors in the last 24h
\"},\"name\":\"div-first-seen-actors-in-ce38d9\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"let recent = Tailscale_Audit_CL | where TimeGenerated > ago(24h) | extend A=tostring(coalesce(Actor.loginName, Actor.displayName)) | summarize FirstSeen24h=min(TimeGenerated), Events=count() by A;\\nlet historical = Tailscale_Audit_CL | where TimeGenerated between(ago(30d) .. ago(24h)) | extend A=tostring(coalesce(Actor.loginName, Actor.displayName)) | distinct A;\\nrecent | join kind=leftanti historical on A | where isnotempty(A) | project FirstSeen24h, Actor=A, Events | order by Events desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Actors who have NEVER appeared before (30d baseline)\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"FirstSeen24h\",\"formatter\":6},{\"columnMatch\":\"Events\",\"formatter\":8,\"formatOptions\":{\"palette\":\"orange\"}}]},\"noDataMessage\":\"Every actor seen in the last 24h has appeared at least once in the prior 30d. Healthy state.\",\"noDataMessageStyle\":1},\"name\":\"q-9ba7b85e\"},{\"type\":1,\"content\":{\"json\":\"
Off-hours configuration changes
\"},\"name\":\"div-off-hours-configurat-3c3787\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend Hour=hourofday(TimeGenerated), DayOfWeek=dayofweek(TimeGenerated)/1d\\n| where Hour < 7 or Hour > 19 or DayOfWeek in (0, 6)\\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\\n| extend TargetType=tostring(Target.type)\\n| where Action !in (\\\"LOGIN\\\", \\\"LOGOUT\\\")\\n| project TimeGenerated, ActorLogin, Action, TargetType, Origin\\n| order by TimeGenerated desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Admin actions outside 07:00-19:00 weekdays\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"TimeGenerated\",\"formatter\":6}]},\"noDataMessage\":\"No off-hours admin changes recorded - healthy state for an organisation working business hours.\",\"noDataMessageStyle\":1},\"name\":\"q-721ee490\"},{\"type\":1,\"content\":{\"json\":\"
Devices with key expiry disabled
\"},\"name\":\"div-devices-with-key-exp-dfe9c1\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| where KeyExpiryDisabled == true\\n| project DeviceName, Hostname, User, Os, ClientVersion, LastSeen, Authorized, Tags\\n| order by LastSeen desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Devices that will never re-authenticate (high-risk drift)\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"LastSeen\",\"formatter\":6}]},\"noDataMessage\":\"No devices have key expiry disabled - good. Disabling key expiry creates devices that never re-auth, drifting from policy.\",\"noDataMessageStyle\":1},\"name\":\"q-8a8e2fb5\"},{\"type\":1,\"content\":{\"json\":\"
Auth keys with no expiry
\"},\"name\":\"div-auth-keys-with-no-ex-b11207\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Keys_CL\\n| summarize arg_max(TimeGenerated, *) by KeyId\\n| where isnull(Revoked) and (isnull(Expires) or ExpirySeconds == 0)\\n| project KeyId, Description, UserId, KeyType, Created, Capabilities\\n| order by Created desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Active keys that never expire\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Created\",\"formatter\":6}]},\"noDataMessage\":\"No never-expiring auth keys - rotation hygiene is good.\",\"noDataMessageStyle\":1},\"name\":\"q-1372740c\"},{\"type\":1,\"content\":{\"json\":\"
Devices running outdated clients
\"},\"name\":\"div-devices-running-outd-bcd077\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| where UpdateAvailable == true\\n| project DeviceName, Hostname, User, Os, ClientVersion, LastSeen, Tags\\n| order by LastSeen desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Devices flagged update-available by Tailscale\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"LastSeen\",\"formatter\":6}]},\"noDataMessage\":\"All devices on current client - nothing to patch.\",\"noDataMessageStyle\":1},\"name\":\"q-ecebf1f7\"},{\"type\":1,\"content\":{\"json\":\"
Dormant devices (LastSeen > 30 days)
\"},\"name\":\"div-dormant-devices-(las-761156\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| where LastSeen < ago(30d)\\n| extend DaysIdle = toint((now() - LastSeen) / 1d)\\n| project DeviceName, Hostname, User, Os, ClientVersion, LastSeen, DaysIdle, Authorized, Tags\\n| order by DaysIdle desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Devices idle 30+ days - candidates for retirement\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"LastSeen\",\"formatter\":6},{\"columnMatch\":\"DaysIdle\",\"formatter\":8,\"formatOptions\":{\"palette\":\"redBright\"}}]},\"noDataMessage\":\"No devices idle 30+ days - inventory is fresh.\",\"noDataMessageStyle\":1},\"name\":\"q-fb6c1fcc\"},{\"type\":1,\"content\":{\"json\":\"
Subnet route exposure
\"},\"name\":\"div-subnet-route-exposur-289c42\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| where array_length(AdvertisedRoutes) > 0 or array_length(EnabledRoutes) > 0\\n| extend Routes = tostring(EnabledRoutes), Advertised = tostring(AdvertisedRoutes)\\n| project DeviceName, Hostname, User, Os, Advertised, Routes, LastSeen, SshEnabled, Authorized\\n| order by LastSeen desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Devices advertising or running subnet routes / exit-node duty\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"LastSeen\",\"formatter\":6}]},\"noDataMessage\":\"No devices advertising subnet routes. Pure mesh topology.\",\"noDataMessageStyle\":1},\"name\":\"q-9533b081\"},{\"type\":1,\"content\":{\"json\":\"
Devices with SSH enabled
\"},\"name\":\"div-devices-with-ssh-ena-285238\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| where SshEnabled == true\\n| project DeviceName, Hostname, User, Os, ClientVersion, LastSeen, Authorized, Tags\\n| order by LastSeen desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Devices with Tailscale SSH enabled (Tailscale-managed remote-shell access)\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"LastSeen\",\"formatter\":6}]},\"noDataMessage\":\"No devices have Tailscale SSH enabled - no SSH-via-Tailscale risk surface.\",\"noDataMessageStyle\":1},\"name\":\"q-735368fb\"}]},\"conditionalVisibility\":{\"parameterName\":\"selectedTab\",\"comparison\":\"isEqualTo\",\"value\":\"hunts\"},\"name\":\"group-hunts\"},{\"type\":12,\"content\":{\"version\":\"NotebookGroup/1.0\",\"groupType\":\"editable\",\"items\":[{\"type\":1,\"content\":{\"json\":\"
User inventory snapshot
\"},\"name\":\"div-user-inventory-snaps-9dc96c\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"let U = Tailscale_Users_CL | summarize arg_max(TimeGenerated, *) by UserId;\\nunion\\n (U | summarize V=toreal(count()) | extend Metric=\\\"Total users\\\", Order=1),\\n (U | where Role in~ (\\\"admin\\\",\\\"owner\\\",\\\"network-admin\\\",\\\"it-admin\\\",\\\"billing-admin\\\") | summarize V=toreal(count()) | extend Metric=\\\"Admin-tier users\\\", Order=2),\\n (U | where Status =~ \\\"active\\\" | summarize V=toreal(count()) | extend Metric=\\\"Active\\\", Order=3),\\n (U | where CurrentlyConnected == true | summarize V=toreal(count()) | extend Metric=\\\"Connected now\\\", Order=4),\\n (U | where Status =~ \\\"idle\\\" or LastSeen < ago(30d) | summarize V=toreal(count()) | extend Metric=\\\"Idle / dormant\\\", Order=5),\\n (U | where UserType =~ \\\"shared\\\" | summarize V=toreal(count()) | extend Metric=\\\"Shared (external)\\\", Order=6)\\n| order by Order asc | project Metric, Value=V\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"tiles\",\"tileSettings\":{\"titleContent\":{\"columnMatch\":\"Metric\",\"formatter\":1},\"leftContent\":{\"columnMatch\":\"Value\",\"formatter\":12,\"formatOptions\":{\"palette\":\"auto\"}},\"showBorder\":false}},\"name\":\"q-5689d9e8\"},{\"type\":1,\"content\":{\"json\":\"
Distribution
\"},\"name\":\"div-distribution-de67ec\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Users_CL\\n| summarize arg_max(TimeGenerated, *) by UserId\\n| summarize Count=count() by Role\\n| order by Count desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"piechart\",\"title\":\"Users by role\"},\"name\":\"q-0c47912a\",\"customWidth\":\"33\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Users_CL\\n| summarize arg_max(TimeGenerated, *) by UserId\\n| summarize Count=count() by Status\\n| order by Count desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"piechart\",\"title\":\"Users by status\"},\"name\":\"q-e8c20a69\",\"customWidth\":\"33\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Users_CL\\n| summarize arg_max(TimeGenerated, *) by UserId\\n| summarize Count=count() by UserType\\n| order by Count desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"piechart\",\"title\":\"Users by type (member / shared)\"},\"name\":\"q-2c51776d\",\"customWidth\":\"33\"},{\"type\":1,\"content\":{\"json\":\"
Activity heatmap
\"},\"name\":\"div-activity-heatmap-ca6a21\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Users_CL\\n| summarize arg_max(TimeGenerated, *) by UserId\\n| extend DaysSinceLogin = toint((now() - LastSeen) / 1d)\\n| extend Bucket = case(\\n DaysSinceLogin < 1, \\\"Today\\\",\\n DaysSinceLogin < 7, \\\"This week\\\",\\n DaysSinceLogin < 30, \\\"This month\\\",\\n DaysSinceLogin < 90, \\\"Past quarter\\\",\\n \\\"90+ days\\\")\\n| summarize Users=count() by Bucket\\n| order by case(Bucket==\\\"Today\\\",1, Bucket==\\\"This week\\\",2, Bucket==\\\"This month\\\",3, Bucket==\\\"Past quarter\\\",4, 5) asc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"barchart\",\"title\":\"Users by recency of last login\"},\"name\":\"q-350e118d\"},{\"type\":1,\"content\":{\"json\":\"
Full user list
\"},\"name\":\"div-full-user-list-136aac\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Users_CL\\n| summarize arg_max(TimeGenerated, *) by UserId\\n| project DisplayName, LoginName, Role, Status, UserType, DeviceCount, CurrentlyConnected, Created, LastSeen\\n| order by LastSeen desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"All users (latest snapshot per user ID)\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Created\",\"formatter\":6},{\"columnMatch\":\"LastSeen\",\"formatter\":6},{\"columnMatch\":\"Role\",\"formatter\":1},{\"columnMatch\":\"Status\",\"formatter\":1},{\"columnMatch\":\"DeviceCount\",\"formatter\":8,\"formatOptions\":{\"palette\":\"blue\"}}]}},\"name\":\"q-cee23d3c\"},{\"type\":1,\"content\":{\"json\":\"
Orphaned users (active but no devices)
\"},\"name\":\"div-orphaned-users-(acti-56d6f8\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Users_CL\\n| summarize arg_max(TimeGenerated, *) by UserId\\n| where Status =~ \\\"active\\\" and DeviceCount == 0\\n| project DisplayName, LoginName, Role, UserType, Created, LastSeen\\n| order by Created desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Active accounts with zero devices - candidates for offboarding review\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Created\",\"formatter\":6},{\"columnMatch\":\"LastSeen\",\"formatter\":6}]},\"noDataMessage\":\"Every active account has at least one device - good hygiene.\",\"noDataMessageStyle\":1},\"name\":\"q-83fcd942\"},{\"type\":1,\"content\":{\"json\":\"
Role escalation history
\"},\"name\":\"div-role-escalation-hist-bd8df4\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| where Action == \\\"USER_ROLE_UPDATE\\\" or Action == \\\"USER_ROLES_ASSIGNED\\\" or Action contains \\\"ROLE\\\"\\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\\n| extend TargetName=tostring(coalesce(Target.name, Target.id))\\n| extend FromRole=tostring(Old.role), ToRole=tostring(New.role)\\n| project TimeGenerated, ActorLogin, Action, TargetName, FromRole, ToRole, Origin\\n| order by TimeGenerated desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Recent role changes\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"TimeGenerated\",\"formatter\":6},{\"columnMatch\":\"ToRole\",\"formatter\":1}]},\"noDataMessage\":\"No role changes in this window.\",\"noDataMessageStyle\":1},\"name\":\"q-f6c8358a\"}]},\"conditionalVisibility\":{\"parameterName\":\"selectedTab\",\"comparison\":\"isEqualTo\",\"value\":\"identity\"},\"name\":\"group-identity\"},{\"type\":12,\"content\":{\"version\":\"NotebookGroup/1.0\",\"groupType\":\"editable\",\"items\":[{\"type\":1,\"content\":{\"json\":\"
Device fleet snapshot
\"},\"name\":\"div-device-fleet-snapsho-939675\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"let D = Tailscale_Devices_CL | summarize arg_max(TimeGenerated, *) by DeviceId;\\nunion\\n (D | summarize V=toreal(count()) | extend Metric=\\\"Total devices\\\", Order=1),\\n (D | where Authorized == true | summarize V=toreal(count()) | extend Metric=\\\"Authorized\\\", Order=2),\\n (D | where IsExternal == true | summarize V=toreal(count()) | extend Metric=\\\"External (shared)\\\", Order=3),\\n (D | where UpdateAvailable == true | summarize V=toreal(count()) | extend Metric=\\\"Updates available\\\", Order=4),\\n (D | where SshEnabled == true | summarize V=toreal(count()) | extend Metric=\\\"SSH-enabled\\\", Order=5),\\n (D | where KeyExpiryDisabled == true | summarize V=toreal(count()) | extend Metric=\\\"No key expiry\\\", Order=6),\\n (D | where array_length(AdvertisedRoutes) > 0 | summarize V=toreal(count()) | extend Metric=\\\"Subnet/exit-node\\\", Order=7),\\n (D | where LastSeen < ago(30d) | summarize V=toreal(count()) | extend Metric=\\\"Stale (30+ days)\\\", Order=8)\\n| order by Order asc | project Metric, Value=V\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"tiles\",\"tileSettings\":{\"titleContent\":{\"columnMatch\":\"Metric\",\"formatter\":1},\"leftContent\":{\"columnMatch\":\"Value\",\"formatter\":12,\"formatOptions\":{\"palette\":\"auto\"}},\"showBorder\":false}},\"name\":\"q-366964fc\"},{\"type\":1,\"content\":{\"json\":\"
Distribution
\"},\"name\":\"div-distribution-396d03\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| summarize Count=count() by Os\\n| order by Count desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"piechart\",\"title\":\"Devices by OS\"},\"name\":\"q-0c8f5988\",\"customWidth\":\"33\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| summarize Count=count() by ClientVersion\\n| order by Count desc | take 10\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"barchart\",\"title\":\"Top 10 client versions\"},\"name\":\"q-af1c45cd\",\"customWidth\":\"33\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| mv-expand Tag = Tags to typeof(string)\\n| summarize Devices=dcount(DeviceId) by Tag=iff(isempty(Tag), \\\"(untagged)\\\", Tag)\\n| order by Devices desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"piechart\",\"title\":\"Devices by tag\"},\"name\":\"q-c3a0ef5b\",\"customWidth\":\"33\"},{\"type\":1,\"content\":{\"json\":\"
Devices needing attention
\"},\"name\":\"div-devices-needing-atte-f04a47\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| where UpdateAvailable == true or KeyExpiryDisabled == true or LastSeen < ago(30d) or Authorized == false\\n| extend Issues = strcat_array(pack_array(\\n iff(UpdateAvailable == true, \\\"needs-update\\\", \\\"\\\"),\\n iff(KeyExpiryDisabled == true, \\\"key-never-expires\\\", \\\"\\\"),\\n iff(LastSeen < ago(30d), \\\"stale\\\", \\\"\\\"),\\n iff(Authorized == false, \\\"unauthorized\\\", \\\"\\\")), \\\",\\\")\\n| extend Issues = trim(\\\",\\\", trim_start(\\\",\\\", trim_end(\\\",\\\", replace_string(Issues, \\\",,\\\", \\\",\\\"))))\\n| project DeviceName, Hostname, User, Os, ClientVersion, LastSeen, Issues\\n| order by LastSeen desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Devices flagged with one or more issues\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"LastSeen\",\"formatter\":6},{\"columnMatch\":\"Issues\",\"formatter\":1}]},\"noDataMessage\":\"No devices need attention - all updated, fresh, authorized, and key-rotating.\",\"noDataMessageStyle\":1},\"name\":\"q-dc5db84c\"},{\"type\":1,\"content\":{\"json\":\"
Full device inventory
\"},\"name\":\"div-full-device-inventor-b70642\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| project DeviceName, Hostname, User, Os, ClientVersion, UpdateAvailable, Authorized, IsExternal, SshEnabled, LastSeen, KeyExpiryDisabled, Tags, AdvertisedRoutes\\n| order by LastSeen desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"All devices (latest snapshot per device ID)\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"LastSeen\",\"formatter\":6},{\"columnMatch\":\"Os\",\"formatter\":1},{\"columnMatch\":\"ClientVersion\",\"formatter\":1}]}},\"name\":\"q-7f7e7a9a\"},{\"type\":1,\"content\":{\"json\":\"
Subnet routers / exit nodes
\"},\"name\":\"div-subnet-routers-/-exi-b082d6\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| where array_length(AdvertisedRoutes) > 0\\n| extend AdvertisedSummary = tostring(AdvertisedRoutes), EnabledSummary = tostring(EnabledRoutes)\\n| project DeviceName, Hostname, User, Os, AdvertisedSummary, EnabledSummary, LastSeen, Authorized\\n| order by LastSeen desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Devices advertising subnet routes or exit-node capability\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"LastSeen\",\"formatter\":6}]},\"noDataMessage\":\"No subnet routers in this tailnet - pure mesh topology.\",\"noDataMessageStyle\":1},\"name\":\"q-5004df25\"}]},\"conditionalVisibility\":{\"parameterName\":\"selectedTab\",\"comparison\":\"isEqualTo\",\"value\":\"devices\"},\"name\":\"group-devices\"},{\"type\":12,\"content\":{\"version\":\"NotebookGroup/1.0\",\"groupType\":\"editable\",\"items\":[{\"type\":1,\"content\":{\"json\":\"
Credentials snapshot
\"},\"name\":\"div-credentials-snapshot-fd464a\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"let K = Tailscale_Keys_CL | summarize arg_max(TimeGenerated, *) by KeyId;\\nunion\\n (K | summarize V=toreal(count()) | extend Metric=\\\"Total keys\\\", Order=1),\\n (K | where isnull(Revoked) and (isnull(Expires) or Expires > now()) | summarize V=toreal(count()) | extend Metric=\\\"Active\\\", Order=2),\\n (K | where isnotnull(Revoked) | summarize V=toreal(count()) | extend Metric=\\\"Revoked\\\", Order=3),\\n (K | where Expires < now() and isnull(Revoked) | summarize V=toreal(count()) | extend Metric=\\\"Expired\\\", Order=4),\\n (K | where isnull(Revoked) and Expires between(now() .. ago(-7d)) | summarize V=toreal(count()) | extend Metric=\\\"Expiring in 7d\\\", Order=5),\\n (K | where isnull(Revoked) and (isnull(Expires) or ExpirySeconds==0) | summarize V=toreal(count()) | extend Metric=\\\"Never expire\\\", Order=6),\\n (K | where KeyType =~ \\\"auth\\\" | summarize V=toreal(count()) | extend Metric=\\\"Auth keys\\\", Order=7),\\n (K | where KeyType =~ \\\"api\\\" or KeyType contains \\\"oauth\\\" | summarize V=toreal(count()) | extend Metric=\\\"API / OAuth\\\", Order=8)\\n| order by Order asc | project Metric, Value=V\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"tiles\",\"tileSettings\":{\"titleContent\":{\"columnMatch\":\"Metric\",\"formatter\":1},\"leftContent\":{\"columnMatch\":\"Value\",\"formatter\":12,\"formatOptions\":{\"palette\":\"auto\"}},\"showBorder\":false}},\"name\":\"q-79398bc0\"},{\"type\":1,\"content\":{\"json\":\"
Distribution
\"},\"name\":\"div-distribution-15f665\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Keys_CL\\n| summarize arg_max(TimeGenerated, *) by KeyId\\n| summarize Count=count() by KeyType\\n| order by Count desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"piechart\",\"title\":\"Keys by type\"},\"name\":\"q-23e6618b\",\"customWidth\":\"50\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Keys_CL\\n| summarize arg_max(TimeGenerated, *) by KeyId\\n| where isnull(Revoked)\\n| extend Bucket = case(\\n isnull(Expires) or ExpirySeconds == 0, \\\"Never\\\",\\n Expires < now(), \\\"Already expired\\\",\\n Expires < ago(-1d), \\\"<24h\\\",\\n Expires < ago(-7d), \\\"1-7d\\\",\\n Expires < ago(-30d), \\\"8-30d\\\",\\n Expires < ago(-90d), \\\"31-90d\\\",\\n \\\"90+d\\\")\\n| summarize Keys=count() by Bucket\\n| order by case(Bucket==\\\"Already expired\\\",1, Bucket==\\\"<24h\\\",2, Bucket==\\\"1-7d\\\",3, Bucket==\\\"8-30d\\\",4, Bucket==\\\"31-90d\\\",5, Bucket==\\\"90+d\\\",6, Bucket==\\\"Never\\\",7, 8) asc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"barchart\",\"title\":\"Active key expiry distribution\"},\"name\":\"q-7484d1a0\",\"customWidth\":\"50\"},{\"type\":1,\"content\":{\"json\":\"
Active credential register
\"},\"name\":\"div-active-credential-re-c27539\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Keys_CL\\n| summarize arg_max(TimeGenerated, *) by KeyId\\n| where isnull(Revoked)\\n| extend ExpiryStatus = case(\\n isnull(Expires) or ExpirySeconds == 0, \\\"Never expires\\\",\\n Expires < now(), \\\"Expired\\\",\\n Expires < ago(-7d), \\\"Expires in 7d\\\",\\n Expires < ago(-30d), \\\"Expires in 30d\\\",\\n \\\"OK\\\")\\n| project KeyId, KeyType, Description, UserId, Created, Expires, ExpiryStatus, Capabilities\\n| order by Created desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"All active credentials with computed expiry status\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Created\",\"formatter\":6},{\"columnMatch\":\"Expires\",\"formatter\":6},{\"columnMatch\":\"ExpiryStatus\",\"formatter\":1},{\"columnMatch\":\"KeyType\",\"formatter\":1}]}},\"name\":\"q-4b27a750\"},{\"type\":1,\"content\":{\"json\":\"
Credential CRUD events
\"},\"name\":\"div-credential-crud-even-e455b0\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| where Action contains \\\"API_KEY\\\" or Action contains \\\"AUTH_KEY\\\" or Action contains \\\"OAUTH\\\" or Action contains \\\"KEY_CREATE\\\" or Action contains \\\"KEY_REVOKE\\\"\\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\\n| extend TargetId=tostring(Target.id), TargetType=tostring(Target.type)\\n| project TimeGenerated, Action, ActorLogin, TargetType, TargetId, Origin\\n| order by TimeGenerated desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Recent credential create / revoke / rotate events\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"TimeGenerated\",\"formatter\":6}]},\"noDataMessage\":\"No credential CRUD activity in this window.\",\"noDataMessageStyle\":1},\"name\":\"q-777693bd\"}]},\"conditionalVisibility\":{\"parameterName\":\"selectedTab\",\"comparison\":\"isEqualTo\",\"value\":\"credentials\"},\"name\":\"group-credentials\"},{\"type\":12,\"content\":{\"version\":\"NotebookGroup/1.0\",\"groupType\":\"editable\",\"items\":[{\"type\":1,\"content\":{\"json\":\"
Audit volume
\"},\"name\":\"div-audit-volume-de29a2\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| summarize Events=count() by bin(TimeGenerated, 1h)\\n| order by TimeGenerated asc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"timechart\",\"title\":\"Audit events per hour\",\"noDataMessage\":\"No audit events in this window.\",\"noDataMessageStyle\":5},\"name\":\"q-f5eee265\"},{\"type\":1,\"content\":{\"json\":\"
Action heatmap by hour of day
\"},\"name\":\"div-action-heatmap-by-ho-c8bd5e\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend Hour=hourofday(TimeGenerated)\\n| summarize Events=count() by Hour, Action\\n| order by Hour asc, Events desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"categoricalbar\",\"title\":\"When are admin actions happening?\"},\"name\":\"q-c6f45d95\"},{\"type\":1,\"content\":{\"json\":\"
Actor / Action heatmap
\"},\"name\":\"div-actor-/-action-heatm-d820ba\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\\n| where isnotempty(ActorLogin)\\n| summarize Events=count() by ActorLogin, Action\\n| order by Events desc | take 100\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Who is firing which action\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Events\",\"formatter\":4,\"formatOptions\":{\"palette\":\"blue\"}}]},\"noDataMessage\":\"No audit events in this window.\",\"noDataMessageStyle\":5},\"name\":\"q-06c13b3b\"},{\"type\":1,\"content\":{\"json\":\"
Recent activity
\"},\"name\":\"div-recent-activity-63b210\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\\n| extend TargetType=tostring(Target.type), TargetName=tostring(coalesce(Target.name, Target.id))\\n| project TimeGenerated, Action, ActorLogin, TargetType, TargetName, Origin, EventGroupID\\n| order by TimeGenerated desc | take 100\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Last 100 audit events\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"TimeGenerated\",\"formatter\":6},{\"columnMatch\":\"Action\",\"formatter\":1}]},\"noDataMessage\":\"No audit events in this window.\",\"noDataMessageStyle\":5},\"name\":\"q-07a25a40\"}]},\"conditionalVisibility\":{\"parameterName\":\"selectedTab\",\"comparison\":\"isEqualTo\",\"value\":\"audit\"},\"name\":\"group-audit\"},{\"type\":12,\"content\":{\"version\":\"NotebookGroup/1.0\",\"groupType\":\"editable\",\"items\":[{\"type\":1,\"content\":{\"json\":\"
DNS configuration (current state)
\"},\"name\":\"div-dns-configuration-(c-5f2e1e\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Dns_CL\\n| summarize arg_max(TimeGenerated, *) by ConfigType\\n| project ConfigType, Nameservers, MagicDNS, SearchPaths, LastSnapshot=TimeGenerated\\n| order by ConfigType asc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"MagicDNS, nameservers, search paths\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"LastSnapshot\",\"formatter\":6},{\"columnMatch\":\"ConfigType\",\"formatter\":1}]},\"noDataMessage\":\"No DNS snapshots in the workspace yet. DNS polls runs at ~30 min cadence.\",\"noDataMessageStyle\":5},\"name\":\"q-d6a6a358\"},{\"type\":1,\"content\":{\"json\":\"
Tailnet settings (current)
\"},\"name\":\"div-tailnet-settings-(cu-e03b16\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Settings_CL\\n| summarize arg_max(TimeGenerated, *) by TenantId\\n| project DevicesApprovalOn, DevicesAutoUpdatesOn, DevicesKeyDurationDays, UsersApprovalOn, NetworkFlowLoggingOn, RegionalRoutingOn, PostureIdentityCollectionOn, UsersRoleAllowedToJoinExternalTailnets, LastSnapshot=TimeGenerated\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Tailnet policy gates\",\"noDataMessage\":\"No settings snapshot yet.\",\"noDataMessageStyle\":5},\"name\":\"q-0622cfc3\"},{\"type\":1,\"content\":{\"json\":\"
DNS change history
\"},\"name\":\"div-dns-change-history-9dd376\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend TargetProperty=tostring(Target.property)\\n| where Action contains \\\"DNS\\\" or TargetProperty has_any (\\\"DNS_NAMESERVERS\\\", \\\"DNS_SPLIT_DNS\\\", \\\"MAGICDNS\\\", \\\"DNS_SEARCH_PATHS\\\")\\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\\n| project TimeGenerated, ActorLogin, Action, TargetProperty, Origin\\n| order by TimeGenerated desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Recent DNS-related admin changes\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"TimeGenerated\",\"formatter\":6}]},\"noDataMessage\":\"No DNS changes in this window.\",\"noDataMessageStyle\":1},\"name\":\"q-a132ff6a\"},{\"type\":1,\"content\":{\"json\":\"
ACL policy changes
\"},\"name\":\"div-acl-policy-changes-ff0e68\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| where Action == \\\"ACL_UPDATE\\\" or Action contains \\\"ACL\\\"\\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\\n| project TimeGenerated, ActorLogin, Action, Origin, EventGroupID\\n| order by TimeGenerated desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Recent ACL / policy file modifications\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"TimeGenerated\",\"formatter\":6}]},\"noDataMessage\":\"No ACL changes in this window.\",\"noDataMessageStyle\":1},\"name\":\"q-94818c53\"},{\"type\":1,\"content\":{\"json\":\"
Subnet routes & exit nodes
\"},\"name\":\"div-subnet-routes-and-ex-eef47f\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| where array_length(EnabledRoutes) > 0\\n| project DeviceName, User, Os, EnabledRoutes=tostring(EnabledRoutes), AdvertisedRoutes=tostring(AdvertisedRoutes), LastSeen\\n| order by LastSeen desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Routes currently being served from devices\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"LastSeen\",\"formatter\":6}]},\"noDataMessage\":\"No subnet routers active in this tailnet.\",\"noDataMessageStyle\":1},\"name\":\"q-61a792cd\"}]},\"conditionalVisibility\":{\"parameterName\":\"selectedTab\",\"comparison\":\"isEqualTo\",\"value\":\"network\"},\"name\":\"group-network\"},{\"type\":12,\"content\":{\"version\":\"NotebookGroup/1.0\",\"groupType\":\"editable\",\"items\":[{\"type\":1,\"content\":{\"json\":\"
Ingest rate per table
\"},\"name\":\"div-ingest-rate-per-tabl-24e5f6\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"union withsource=Table Tailscale_Audit_CL, Tailscale_Devices_CL, Tailscale_Users_CL, Tailscale_Keys_CL, Tailscale_Dns_CL, Tailscale_Settings_CL\\n| where TimeGenerated > ago(24h)\\n| summarize Rows=count() by Table, bin(TimeGenerated, 1h)\\n| order by TimeGenerated asc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"timechart\",\"title\":\"Rows ingested per Tailscale table per hour (last 24h)\",\"noDataMessage\":\"No Tailscale data ingested in the last 24h - check the connector card under Sentinel Data Connectors.\",\"noDataMessageStyle\":5},\"name\":\"q-c6fd0143\"},{\"type\":1,\"content\":{\"json\":\"
Last poll time per table
\"},\"name\":\"div-last-poll-time-per-t-49b051\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"union withsource=Table Tailscale_Audit_CL, Tailscale_Devices_CL, Tailscale_Users_CL, Tailscale_Keys_CL, Tailscale_Dns_CL, Tailscale_Settings_CL\\n| summarize LastRow=max(TimeGenerated), TotalRows=count() by Table\\n| extend MinutesAgo=toint((now() - LastRow) / 1m)\\n| extend Status=case(MinutesAgo < 60, \\\"Fresh\\\", MinutesAgo < 360, \\\"Recent\\\", MinutesAgo < 1440, \\\"Stale\\\", \\\"Very Stale\\\")\\n| project Table, LastRow, MinutesAgo, TotalRows, Status\\n| order by MinutesAgo asc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Per-table freshness\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"LastRow\",\"formatter\":6},{\"columnMatch\":\"MinutesAgo\",\"formatter\":8,\"formatOptions\":{\"palette\":\"redBright\"}},{\"columnMatch\":\"TotalRows\",\"formatter\":8,\"formatOptions\":{\"palette\":\"blue\"}},{\"columnMatch\":\"Status\",\"formatter\":1}]}},\"name\":\"q-a02e37a8\"},{\"type\":1,\"content\":{\"json\":\"
Log Analytics operational events
\"},\"name\":\"div-log-analytics-operat-630749\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"_LogOperation\\n| where TimeGenerated > ago(24h)\\n| where _ResourceId contains \\\"tailscale\\\" or Detail contains \\\"Tailscale_\\\"\\n| project TimeGenerated, Operation, Level, Detail\\n| order by TimeGenerated desc | take 100\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Log Analytics operational events touching Tailscale tables\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"TimeGenerated\",\"formatter\":6},{\"columnMatch\":\"Level\",\"formatter\":1}]},\"noDataMessage\":\"No operational issues recorded in the last 24h.\",\"noDataMessageStyle\":1},\"name\":\"q-b492f150\"}]},\"conditionalVisibility\":{\"parameterName\":\"selectedTab\",\"comparison\":\"isEqualTo\",\"value\":\"pipeline\"},\"name\":\"group-pipeline\"},{\"type\":1,\"content\":{\"json\":\"
Tailscale Operations (Standard) (CCF) - Microsoft Sentinel content from the Tailscale (CCF) solution, Standard-tier surface. Tables polled from the Tailscale REST API: audit, devices, users, keys, dns, settings. Filter every panel via the time range above; the Investigate tab adds Actor and Device pickers for drilldown. For network flow logs and posture integrations, install the companion Tailscale Operations (Premium) workbook on a Premium / Enterprise tailnet.
\"},\"name\":\"footer\"}],\"fallbackResourceIds\":[\"Azure Monitor\"],\"fromTemplateId\":\"sentinel-Tailscale-CCF\",\"$schema\":\"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"}\n", + "version": "1.0", + "sourceId": "[variables('workspaceResourceId')]", + "category": "sentinel" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('Workbook-', last(split(variables('workbookId1'),'/'))))]", + "properties": { + "description": "@{workbookKey=TailscaleStandardOperationsWorkbook; logoFileName=Tailscale.svg; description=Tailscale Operations workbook for Standard tier. Nine tabs covering an at-a-glance KPI hero row, audit activity overview, an actor + device drilldown (Investigate), embedded hunting queries (first-seen actors, off-hours changes, key-expiry-disabled devices, never-expire auth keys, outdated clients, dormant devices, subnet route exposure, SSH-enabled devices), identity (user roles, status, last login recency, role escalation history, orphaned users), devices (OS / version / tag distribution, devices needing attention, full inventory, subnet routers), credentials (expiry timeline, never-expire flag, CRUD events), admin audit (action heatmap, actor x action heatmap, recent 100), network and DNS (current snapshot, tailnet policy gates, ACL change history), and pipeline health (per-table freshness, ingest rate, operational events). Driven by data polled from the Tailscale REST API.; dataTypesDependencies=System.Object[]; dataConnectorsDependencies=System.Object[]; previewImagesFileNames=System.Object[]; version=1.0.0; title=Tailscale Operations (Standard); templateRelativePath=TailscaleStandardOperations.json; subtitle=; provider=Community; support=; source=; categories=; author=}.description", + "parentId": "[variables('workbookId1')]", + "contentId": "[variables('_workbookContentId1')]", + "kind": "Workbook", + "version": "[variables('workbookVersion1')]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + }, + "dependencies": { + "operator": "AND", + "criteria": [ + { + "contentId": "Tailscale_Audit_CL", + "kind": "DataType" + }, + { + "contentId": "Tailscale_Devices_CL", + "kind": "DataType" + }, + { + "contentId": "Tailscale_Users_CL", + "kind": "DataType" + }, + { + "contentId": "Tailscale_Keys_CL", + "kind": "DataType" + }, + { + "contentId": "Tailscale_Webhooks_CL", + "kind": "DataType" + }, + { + "contentId": "Tailscale_Settings_CL", + "kind": "DataType" + }, + { + "contentId": "Tailscale_Dns_CL", + "kind": "DataType" + }, + { + "contentId": "TailscaleCCF", + "kind": "DataConnector" + } + ] + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('_workbookContentId1')]", + "contentKind": "Workbook", + "displayName": "[parameters('workbook1-name')]", + "contentProductId": "[variables('_workbookcontentProductId1')]", + "id": "[variables('_workbookcontentProductId1')]", + "version": "[variables('workbookVersion1')]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('workbookTemplateSpecName2')]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "TailscalePremiumOperations Workbook with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('workbookVersion2')]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.Insights/workbooks", + "name": "[variables('workbookContentId2')]", + "location": "[parameters('workspace-location')]", + "kind": "shared", + "apiVersion": "2021-08-01", + "metadata": { + "description": "Tailscale Operations workbook for Premium / Enterprise tier - everything in the Standard workbook plus network flow analysis (top talkers, src-dst pairs, exit-node egress, beaconing candidates) and posture integration inventory." + }, + "properties": { + "displayName": "[parameters('workbook2-name')]", + "serializedData": "{\"version\":\"Notebook/1.0\",\"items\":[{\"type\":9,\"content\":{\"version\":\"KqlParameterItem/1.0\",\"parameters\":[{\"id\":\"7b05a598-5120-43f4-bf5d-576c2a7ff28d\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"TimeRange\",\"type\":4,\"isRequired\":true,\"value\":{\"durationMs\":86400000},\"typeSettings\":{\"selectableValues\":[{\"durationMs\":3600000},{\"durationMs\":14400000},{\"durationMs\":43200000},{\"durationMs\":86400000},{\"durationMs\":172800000},{\"durationMs\":604800000},{\"durationMs\":2592000000}]}}],\"style\":\"pills\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\"},\"name\":\"parameters\"},{\"type\":1,\"content\":{\"json\":\"
Tailscale Operations (Premium)
Single-pane visibility into your Tailscale tailnet on Personal (Free), Starter and Premium tiers: who, what, when, where, and what changed. Scope every panel with the time range below; the Investigate tab adds Actor and Device pickers for drilldown. Premium-tier panels (network flow logs, posture integrations) live in the separate Tailscale Operations (Premium) workbook.
\"},\"name\":\"header\"},{\"type\":11,\"content\":{\"version\":\"LinkItem/1.0\",\"style\":\"tabs\",\"links\":[{\"id\":\"9794e9fd-916b-494d-8e72-af63d2f4c6c7\",\"cellValue\":\"selectedTab\",\"linkTarget\":\"parameter\",\"linkLabel\":\"Overview\",\"subTarget\":\"overview\",\"style\":\"link\"},{\"id\":\"71cf49db-33c5-4d4b-920a-2ec0c6a258dc\",\"cellValue\":\"selectedTab\",\"linkTarget\":\"parameter\",\"linkLabel\":\"Investigate\",\"subTarget\":\"investigate\",\"style\":\"link\"},{\"id\":\"5c56bb37-2053-47a0-b6fd-9539768c144d\",\"cellValue\":\"selectedTab\",\"linkTarget\":\"parameter\",\"linkLabel\":\"Hunts\",\"subTarget\":\"hunts\",\"style\":\"link\"},{\"id\":\"d2004ded-07f8-446a-a720-f0a63d1d9dda\",\"cellValue\":\"selectedTab\",\"linkTarget\":\"parameter\",\"linkLabel\":\"Identity\",\"subTarget\":\"identity\",\"style\":\"link\"},{\"id\":\"f23b3e14-1511-4a29-bf5e-bd65e55dbb40\",\"cellValue\":\"selectedTab\",\"linkTarget\":\"parameter\",\"linkLabel\":\"Devices\",\"subTarget\":\"devices\",\"style\":\"link\"},{\"id\":\"724f8352-e21b-45a6-9029-39dc92693c05\",\"cellValue\":\"selectedTab\",\"linkTarget\":\"parameter\",\"linkLabel\":\"Credentials\",\"subTarget\":\"credentials\",\"style\":\"link\"},{\"id\":\"8400ec64-e118-44e0-ae29-84afe94b8e0e\",\"cellValue\":\"selectedTab\",\"linkTarget\":\"parameter\",\"linkLabel\":\"Admin Audit\",\"subTarget\":\"audit\",\"style\":\"link\"},{\"id\":\"b7e04861-edfa-4426-b6be-f1481ca569b6\",\"cellValue\":\"selectedTab\",\"linkTarget\":\"parameter\",\"linkLabel\":\"Network & DNS\",\"subTarget\":\"network\",\"style\":\"link\"},{\"id\":\"b8506d3d-e615-4775-8c6f-14d9cc7db943\",\"cellValue\":\"selectedTab\",\"linkTarget\":\"parameter\",\"linkLabel\":\"Network Flows\",\"subTarget\":\"network-flows\",\"style\":\"link\"},{\"id\":\"c98574ec-7a60-4c1a-b4c6-4d24c5bd02ce\",\"cellValue\":\"selectedTab\",\"linkTarget\":\"parameter\",\"linkLabel\":\"Posture\",\"subTarget\":\"posture\",\"style\":\"link\"},{\"id\":\"cfbc96e4-8585-4d89-8f65-8344c8cc6eb2\",\"cellValue\":\"selectedTab\",\"linkTarget\":\"parameter\",\"linkLabel\":\"Pipeline Health\",\"subTarget\":\"pipeline\",\"style\":\"link\"}]},\"name\":\"tabs\"},{\"type\":12,\"content\":{\"version\":\"NotebookGroup/1.0\",\"groupType\":\"editable\",\"items\":[{\"type\":1,\"content\":{\"json\":\"
Tailnet at a glance
\"},\"name\":\"div-tailnet-at-a-glance-04e62b\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"let DEV = Tailscale_Devices_CL | summarize arg_max(TimeGenerated, *) by DeviceId;\\nlet USR = Tailscale_Users_CL | summarize arg_max(TimeGenerated, *) by UserId;\\nlet KEY = Tailscale_Keys_CL | summarize arg_max(TimeGenerated, *) by KeyId;\\nunion\\n (DEV | summarize V=toreal(count()) | extend Metric=\\\"Devices\\\", Order=1),\\n (DEV | where Authorized == true | summarize V=toreal(count()) | extend Metric=\\\"Authorized\\\", Order=2),\\n (DEV | where UpdateAvailable == true | summarize V=toreal(count()) | extend Metric=\\\"Updates Available\\\", Order=3),\\n (DEV | where SshEnabled == true | summarize V=toreal(count()) | extend Metric=\\\"SSH-Enabled\\\", Order=4),\\n (USR | summarize V=toreal(count()) | extend Metric=\\\"Users\\\", Order=5),\\n (USR | where Role =~ \\\"admin\\\" or Role =~ \\\"owner\\\" or Role =~ \\\"network-admin\\\" | summarize V=toreal(count()) | extend Metric=\\\"Admins\\\", Order=6),\\n (KEY | where isnull(Revoked) and (isnull(Expires) or Expires > now()) | summarize V=toreal(count()) | extend Metric=\\\"Active Keys\\\", Order=7),\\n (Tailscale_Audit_CL | where TimeGenerated {TimeRange} | summarize V=toreal(count()) | extend Metric=\\\"Audit Events ({TimeRange:label})\\\", Order=8)\\n,\\n (Tailscale_Network_CL | where TimeGenerated {TimeRange} | summarize V=toreal(count()) | extend Metric=\\\"Flows ({TimeRange:label})\\\", Order=9),\\n (Tailscale_Network_CL | where TimeGenerated {TimeRange} | summarize V=toreal(dcount(SrcNodeName)) | extend Metric=\\\"Active Talkers\\\", Order=10),\\n (Tailscale_Network_CL | where TimeGenerated {TimeRange} | summarize V=toreal(iff(count()==0, 0.0, 100.0 * countif(IsRelayed) / count())) | extend Metric=\\\"DERP Relayed %\\\", Order=11),\\n (Tailscale_PostureIntegrations_CL | where TimeGenerated {TimeRange} | summarize arg_max(TimeGenerated, *) by IntegrationId | summarize V=toreal(count()) | extend Metric=\\\"Posture Integrations\\\", Order=12)\\n| order by Order asc | project Metric, Value=V\",\"size\":3,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"tiles\",\"tileSettings\":{\"titleContent\":{\"columnMatch\":\"Metric\",\"formatter\":1},\"leftContent\":{\"columnMatch\":\"Value\",\"formatter\":12,\"formatOptions\":{\"palette\":\"auto\"}},\"showBorder\":false}},\"name\":\"q-4e9d7cff\"},{\"type\":1,\"content\":{\"json\":\"
Audit activity over time
\"},\"name\":\"div-audit-activity-over--546688\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| summarize EventCount = count() by bin(TimeGenerated, 1h), Action\\n| order by TimeGenerated asc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"timechart\",\"title\":\"Audit events by action\",\"noDataMessage\":\"No audit events in the selected window. Widen the time range; remember the Tailscale audit poll runs every ~30 min.\",\"noDataMessageStyle\":5},\"name\":\"q-effcd498\"},{\"type\":1,\"content\":{\"json\":\"
Who's doing what
\"},\"name\":\"div-who's-doing-what-52144e\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend Actor=tostring(coalesce(Actor.loginName, Actor.displayName, Actor.type))\\n| where isnotempty(Actor)\\n| summarize Events=count(), DistinctActions=dcount(Action), LastSeen=max(TimeGenerated) by Actor\\n| order by Events desc | take 15\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Top actors (by event count)\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Events\",\"formatter\":8,\"formatOptions\":{\"palette\":\"blue\"}},{\"columnMatch\":\"LastSeen\",\"formatter\":6}]}},\"name\":\"q-34c77fa9\",\"customWidth\":\"50\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend TargetType=tostring(Target.type)\\n| where isnotempty(TargetType)\\n| summarize Events=count() by TargetType\\n| order by Events desc | take 15\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"piechart\",\"title\":\"Activity by target type\"},\"name\":\"q-4b3b709d\",\"customWidth\":\"50\"},{\"type\":1,\"content\":{\"json\":\"
Recent admin events
\"},\"name\":\"div-recent-admin-events-87c9e0\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend Actor=tostring(coalesce(Actor.loginName, Actor.displayName, Actor.type))\\n| extend TargetType=tostring(Target.type), TargetName=tostring(coalesce(Target.name, Target.id))\\n| project TimeGenerated, Action, Actor, TargetType, TargetName, Origin\\n| order by TimeGenerated desc | take 30\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Most recent 30 audit events\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"TimeGenerated\",\"formatter\":6}]}},\"name\":\"q-5e7d6306\"}]},\"conditionalVisibility\":{\"parameterName\":\"selectedTab\",\"comparison\":\"isEqualTo\",\"value\":\"overview\"},\"name\":\"group-overview\"},{\"type\":12,\"content\":{\"version\":\"NotebookGroup/1.0\",\"groupType\":\"editable\",\"items\":[{\"type\":9,\"content\":{\"version\":\"KqlParameterItem/1.0\",\"parameters\":[{\"id\":\"b83a25c1-da18-49a2-a444-6517f13d891c\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"SelectedActor\",\"label\":\"Actor\",\"type\":2,\"isRequired\":false,\"query\":\"let opts = Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\\n| where isnotempty(ActorLogin)\\n| summarize Events=count() by ActorLogin\\n| project value=ActorLogin, label=strcat(ActorLogin, \\\" (\\\", tostring(Events), \\\" events)\\\");\\n(print value=\\\"__ALL__\\\", label=\\\"(All actors)\\\")\\n| union opts\",\"typeSettings\":{\"showDefault\":false},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":\"__ALL__\"}],\"style\":\"pills\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\"},\"name\":\"investigate-picker-actor\"},{\"type\":1,\"content\":{\"json\":\"
Actor activity timeline
\"},\"name\":\"div-actor-activity-timel-5ec305\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\\n| where \\\"{SelectedActor}\\\" == \\\"__ALL__\\\" or ActorLogin == \\\"{SelectedActor}\\\"\\n| summarize Events=count() by bin(TimeGenerated, 1h), Action\\n| order by TimeGenerated asc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"timechart\",\"title\":\"Actions over time -- actor: {SelectedActor:label}\",\"noDataMessage\":\"Select an actor from the Actor dropdown above, or leave on 'All' to see total activity.\",\"noDataMessageStyle\":5},\"name\":\"q-32d40848\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\\n| where \\\"{SelectedActor}\\\" == \\\"__ALL__\\\" or ActorLogin == \\\"{SelectedActor}\\\"\\n| extend TargetType=tostring(Target.type), TargetName=tostring(coalesce(Target.name, Target.id))\\n| project TimeGenerated, ActorLogin, Action, TargetType, TargetName, Origin\\n| order by TimeGenerated desc | take 100\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Recent events for actor: {SelectedActor:label}\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"TimeGenerated\",\"formatter\":6}]},\"noDataMessage\":\"No events for this actor in the selected window.\",\"noDataMessageStyle\":5},\"name\":\"q-a742f6fd\"},{\"type\":9,\"content\":{\"version\":\"KqlParameterItem/1.0\",\"parameters\":[{\"id\":\"a9cf7907-f201-4725-a072-a8bd34bef74e\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"SelectedDevice\",\"label\":\"Device\",\"type\":2,\"isRequired\":false,\"query\":\"let opts = Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| order by LastSeen desc | take 100\\n| project value=DeviceName, label=strcat(coalesce(DeviceName, Hostname), \\\" (\\\", User, \\\")\\\");\\n(print value=\\\"__ALL__\\\", label=\\\"(All devices)\\\")\\n| union opts\",\"typeSettings\":{\"showDefault\":false},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":\"__ALL__\"}],\"style\":\"pills\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\"},\"name\":\"investigate-picker-device\"},{\"type\":1,\"content\":{\"json\":\"
Selected device timeline
\"},\"name\":\"div-selected-device-time-00bead\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| where \\\"{SelectedDevice}\\\" == \\\"__ALL__\\\" or DeviceName == \\\"{SelectedDevice}\\\"\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| extend OnlineNow = ClientConnectivity.endpoints != \\\"\\\" or ConnectedToControl == true\\n| project DeviceName, Hostname, User, Os, ClientVersion, UpdateAvailable, Authorized, IsExternal, SshEnabled, LastSeen, Expires, KeyExpiryDisabled, OnlineNow, Addresses, Tags, AdvertisedRoutes\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Summary for device: {SelectedDevice:label}\",\"noDataMessage\":\"Select a device from the Device dropdown above. Defaults to 'All'.\",\"noDataMessageStyle\":5},\"name\":\"q-11094dc8\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend TargetType=tostring(Target.type), TargetName=tostring(Target.name), TargetId=tostring(Target.id)\\n| where (\\\"{SelectedDevice}\\\" == \\\"__ALL__\\\" and TargetType == \\\"NODE\\\") or TargetName == \\\"{SelectedDevice}\\\"\\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\\n| project TimeGenerated, Action, ActorLogin, TargetName, TargetId, Origin\\n| order by TimeGenerated desc | take 100\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Audit events touching device: {SelectedDevice:label}\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"TimeGenerated\",\"formatter\":6}]},\"noDataMessage\":\"No audit events recorded against the selected device in this window. Tailscale tags device events with Target.type=NODE; the audit feed only emits NODE events on create/update/delete, so quiet devices stay quiet here.\",\"noDataMessageStyle\":5},\"name\":\"q-ead106ce\"}]},\"conditionalVisibility\":{\"parameterName\":\"selectedTab\",\"comparison\":\"isEqualTo\",\"value\":\"investigate\"},\"name\":\"group-investigate\"},{\"type\":12,\"content\":{\"version\":\"NotebookGroup/1.0\",\"groupType\":\"editable\",\"items\":[{\"type\":1,\"content\":{\"json\":\"
First-seen actors in the last 24h
\"},\"name\":\"div-first-seen-actors-in-ce38d9\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"let recent = Tailscale_Audit_CL | where TimeGenerated > ago(24h) | extend A=tostring(coalesce(Actor.loginName, Actor.displayName)) | summarize FirstSeen24h=min(TimeGenerated), Events=count() by A;\\nlet historical = Tailscale_Audit_CL | where TimeGenerated between(ago(30d) .. ago(24h)) | extend A=tostring(coalesce(Actor.loginName, Actor.displayName)) | distinct A;\\nrecent | join kind=leftanti historical on A | where isnotempty(A) | project FirstSeen24h, Actor=A, Events | order by Events desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Actors who have NEVER appeared before (30d baseline)\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"FirstSeen24h\",\"formatter\":6},{\"columnMatch\":\"Events\",\"formatter\":8,\"formatOptions\":{\"palette\":\"orange\"}}]},\"noDataMessage\":\"Every actor seen in the last 24h has appeared at least once in the prior 30d. Healthy state.\",\"noDataMessageStyle\":1},\"name\":\"q-9ba7b85e\"},{\"type\":1,\"content\":{\"json\":\"
Off-hours configuration changes
\"},\"name\":\"div-off-hours-configurat-3c3787\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend Hour=hourofday(TimeGenerated), DayOfWeek=dayofweek(TimeGenerated)/1d\\n| where Hour < 7 or Hour > 19 or DayOfWeek in (0, 6)\\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\\n| extend TargetType=tostring(Target.type)\\n| where Action !in (\\\"LOGIN\\\", \\\"LOGOUT\\\")\\n| project TimeGenerated, ActorLogin, Action, TargetType, Origin\\n| order by TimeGenerated desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Admin actions outside 07:00-19:00 weekdays\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"TimeGenerated\",\"formatter\":6}]},\"noDataMessage\":\"No off-hours admin changes recorded - healthy state for an organisation working business hours.\",\"noDataMessageStyle\":1},\"name\":\"q-721ee490\"},{\"type\":1,\"content\":{\"json\":\"
Devices with key expiry disabled
\"},\"name\":\"div-devices-with-key-exp-dfe9c1\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| where KeyExpiryDisabled == true\\n| project DeviceName, Hostname, User, Os, ClientVersion, LastSeen, Authorized, Tags\\n| order by LastSeen desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Devices that will never re-authenticate (high-risk drift)\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"LastSeen\",\"formatter\":6}]},\"noDataMessage\":\"No devices have key expiry disabled - good. Disabling key expiry creates devices that never re-auth, drifting from policy.\",\"noDataMessageStyle\":1},\"name\":\"q-8a8e2fb5\"},{\"type\":1,\"content\":{\"json\":\"
Auth keys with no expiry
\"},\"name\":\"div-auth-keys-with-no-ex-b11207\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Keys_CL\\n| summarize arg_max(TimeGenerated, *) by KeyId\\n| where isnull(Revoked) and (isnull(Expires) or ExpirySeconds == 0)\\n| project KeyId, Description, UserId, KeyType, Created, Capabilities\\n| order by Created desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Active keys that never expire\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Created\",\"formatter\":6}]},\"noDataMessage\":\"No never-expiring auth keys - rotation hygiene is good.\",\"noDataMessageStyle\":1},\"name\":\"q-1372740c\"},{\"type\":1,\"content\":{\"json\":\"
Devices running outdated clients
\"},\"name\":\"div-devices-running-outd-bcd077\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| where UpdateAvailable == true\\n| project DeviceName, Hostname, User, Os, ClientVersion, LastSeen, Tags\\n| order by LastSeen desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Devices flagged update-available by Tailscale\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"LastSeen\",\"formatter\":6}]},\"noDataMessage\":\"All devices on current client - nothing to patch.\",\"noDataMessageStyle\":1},\"name\":\"q-ecebf1f7\"},{\"type\":1,\"content\":{\"json\":\"
Dormant devices (LastSeen > 30 days)
\"},\"name\":\"div-dormant-devices-(las-761156\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| where LastSeen < ago(30d)\\n| extend DaysIdle = toint((now() - LastSeen) / 1d)\\n| project DeviceName, Hostname, User, Os, ClientVersion, LastSeen, DaysIdle, Authorized, Tags\\n| order by DaysIdle desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Devices idle 30+ days - candidates for retirement\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"LastSeen\",\"formatter\":6},{\"columnMatch\":\"DaysIdle\",\"formatter\":8,\"formatOptions\":{\"palette\":\"redBright\"}}]},\"noDataMessage\":\"No devices idle 30+ days - inventory is fresh.\",\"noDataMessageStyle\":1},\"name\":\"q-fb6c1fcc\"},{\"type\":1,\"content\":{\"json\":\"
Subnet route exposure
\"},\"name\":\"div-subnet-route-exposur-289c42\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| where array_length(AdvertisedRoutes) > 0 or array_length(EnabledRoutes) > 0\\n| extend Routes = tostring(EnabledRoutes), Advertised = tostring(AdvertisedRoutes)\\n| project DeviceName, Hostname, User, Os, Advertised, Routes, LastSeen, SshEnabled, Authorized\\n| order by LastSeen desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Devices advertising or running subnet routes / exit-node duty\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"LastSeen\",\"formatter\":6}]},\"noDataMessage\":\"No devices advertising subnet routes. Pure mesh topology.\",\"noDataMessageStyle\":1},\"name\":\"q-9533b081\"},{\"type\":1,\"content\":{\"json\":\"
Devices with SSH enabled
\"},\"name\":\"div-devices-with-ssh-ena-285238\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| where SshEnabled == true\\n| project DeviceName, Hostname, User, Os, ClientVersion, LastSeen, Authorized, Tags\\n| order by LastSeen desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Devices with Tailscale SSH enabled (Tailscale-managed remote-shell access)\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"LastSeen\",\"formatter\":6}]},\"noDataMessage\":\"No devices have Tailscale SSH enabled - no SSH-via-Tailscale risk surface.\",\"noDataMessageStyle\":1},\"name\":\"q-735368fb\"}]},\"conditionalVisibility\":{\"parameterName\":\"selectedTab\",\"comparison\":\"isEqualTo\",\"value\":\"hunts\"},\"name\":\"group-hunts\"},{\"type\":12,\"content\":{\"version\":\"NotebookGroup/1.0\",\"groupType\":\"editable\",\"items\":[{\"type\":1,\"content\":{\"json\":\"
User inventory snapshot
\"},\"name\":\"div-user-inventory-snaps-9dc96c\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"let U = Tailscale_Users_CL | summarize arg_max(TimeGenerated, *) by UserId;\\nunion\\n (U | summarize V=toreal(count()) | extend Metric=\\\"Total users\\\", Order=1),\\n (U | where Role in~ (\\\"admin\\\",\\\"owner\\\",\\\"network-admin\\\",\\\"it-admin\\\",\\\"billing-admin\\\") | summarize V=toreal(count()) | extend Metric=\\\"Admin-tier users\\\", Order=2),\\n (U | where Status =~ \\\"active\\\" | summarize V=toreal(count()) | extend Metric=\\\"Active\\\", Order=3),\\n (U | where CurrentlyConnected == true | summarize V=toreal(count()) | extend Metric=\\\"Connected now\\\", Order=4),\\n (U | where Status =~ \\\"idle\\\" or LastSeen < ago(30d) | summarize V=toreal(count()) | extend Metric=\\\"Idle / dormant\\\", Order=5),\\n (U | where UserType =~ \\\"shared\\\" | summarize V=toreal(count()) | extend Metric=\\\"Shared (external)\\\", Order=6)\\n| order by Order asc | project Metric, Value=V\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"tiles\",\"tileSettings\":{\"titleContent\":{\"columnMatch\":\"Metric\",\"formatter\":1},\"leftContent\":{\"columnMatch\":\"Value\",\"formatter\":12,\"formatOptions\":{\"palette\":\"auto\"}},\"showBorder\":false}},\"name\":\"q-5689d9e8\"},{\"type\":1,\"content\":{\"json\":\"
Distribution
\"},\"name\":\"div-distribution-de67ec\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Users_CL\\n| summarize arg_max(TimeGenerated, *) by UserId\\n| summarize Count=count() by Role\\n| order by Count desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"piechart\",\"title\":\"Users by role\"},\"name\":\"q-0c47912a\",\"customWidth\":\"33\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Users_CL\\n| summarize arg_max(TimeGenerated, *) by UserId\\n| summarize Count=count() by Status\\n| order by Count desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"piechart\",\"title\":\"Users by status\"},\"name\":\"q-e8c20a69\",\"customWidth\":\"33\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Users_CL\\n| summarize arg_max(TimeGenerated, *) by UserId\\n| summarize Count=count() by UserType\\n| order by Count desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"piechart\",\"title\":\"Users by type (member / shared)\"},\"name\":\"q-2c51776d\",\"customWidth\":\"33\"},{\"type\":1,\"content\":{\"json\":\"
Activity heatmap
\"},\"name\":\"div-activity-heatmap-ca6a21\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Users_CL\\n| summarize arg_max(TimeGenerated, *) by UserId\\n| extend DaysSinceLogin = toint((now() - LastSeen) / 1d)\\n| extend Bucket = case(\\n DaysSinceLogin < 1, \\\"Today\\\",\\n DaysSinceLogin < 7, \\\"This week\\\",\\n DaysSinceLogin < 30, \\\"This month\\\",\\n DaysSinceLogin < 90, \\\"Past quarter\\\",\\n \\\"90+ days\\\")\\n| summarize Users=count() by Bucket\\n| order by case(Bucket==\\\"Today\\\",1, Bucket==\\\"This week\\\",2, Bucket==\\\"This month\\\",3, Bucket==\\\"Past quarter\\\",4, 5) asc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"barchart\",\"title\":\"Users by recency of last login\"},\"name\":\"q-350e118d\"},{\"type\":1,\"content\":{\"json\":\"
Full user list
\"},\"name\":\"div-full-user-list-136aac\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Users_CL\\n| summarize arg_max(TimeGenerated, *) by UserId\\n| project DisplayName, LoginName, Role, Status, UserType, DeviceCount, CurrentlyConnected, Created, LastSeen\\n| order by LastSeen desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"All users (latest snapshot per user ID)\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Created\",\"formatter\":6},{\"columnMatch\":\"LastSeen\",\"formatter\":6},{\"columnMatch\":\"Role\",\"formatter\":1},{\"columnMatch\":\"Status\",\"formatter\":1},{\"columnMatch\":\"DeviceCount\",\"formatter\":8,\"formatOptions\":{\"palette\":\"blue\"}}]}},\"name\":\"q-cee23d3c\"},{\"type\":1,\"content\":{\"json\":\"
Orphaned users (active but no devices)
\"},\"name\":\"div-orphaned-users-(acti-56d6f8\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Users_CL\\n| summarize arg_max(TimeGenerated, *) by UserId\\n| where Status =~ \\\"active\\\" and DeviceCount == 0\\n| project DisplayName, LoginName, Role, UserType, Created, LastSeen\\n| order by Created desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Active accounts with zero devices - candidates for offboarding review\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Created\",\"formatter\":6},{\"columnMatch\":\"LastSeen\",\"formatter\":6}]},\"noDataMessage\":\"Every active account has at least one device - good hygiene.\",\"noDataMessageStyle\":1},\"name\":\"q-83fcd942\"},{\"type\":1,\"content\":{\"json\":\"
Role escalation history
\"},\"name\":\"div-role-escalation-hist-bd8df4\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| where Action == \\\"USER_ROLE_UPDATE\\\" or Action == \\\"USER_ROLES_ASSIGNED\\\" or Action contains \\\"ROLE\\\"\\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\\n| extend TargetName=tostring(coalesce(Target.name, Target.id))\\n| extend FromRole=tostring(Old.role), ToRole=tostring(New.role)\\n| project TimeGenerated, ActorLogin, Action, TargetName, FromRole, ToRole, Origin\\n| order by TimeGenerated desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Recent role changes\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"TimeGenerated\",\"formatter\":6},{\"columnMatch\":\"ToRole\",\"formatter\":1}]},\"noDataMessage\":\"No role changes in this window.\",\"noDataMessageStyle\":1},\"name\":\"q-f6c8358a\"}]},\"conditionalVisibility\":{\"parameterName\":\"selectedTab\",\"comparison\":\"isEqualTo\",\"value\":\"identity\"},\"name\":\"group-identity\"},{\"type\":12,\"content\":{\"version\":\"NotebookGroup/1.0\",\"groupType\":\"editable\",\"items\":[{\"type\":1,\"content\":{\"json\":\"
Device fleet snapshot
\"},\"name\":\"div-device-fleet-snapsho-939675\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"let D = Tailscale_Devices_CL | summarize arg_max(TimeGenerated, *) by DeviceId;\\nunion\\n (D | summarize V=toreal(count()) | extend Metric=\\\"Total devices\\\", Order=1),\\n (D | where Authorized == true | summarize V=toreal(count()) | extend Metric=\\\"Authorized\\\", Order=2),\\n (D | where IsExternal == true | summarize V=toreal(count()) | extend Metric=\\\"External (shared)\\\", Order=3),\\n (D | where UpdateAvailable == true | summarize V=toreal(count()) | extend Metric=\\\"Updates available\\\", Order=4),\\n (D | where SshEnabled == true | summarize V=toreal(count()) | extend Metric=\\\"SSH-enabled\\\", Order=5),\\n (D | where KeyExpiryDisabled == true | summarize V=toreal(count()) | extend Metric=\\\"No key expiry\\\", Order=6),\\n (D | where array_length(AdvertisedRoutes) > 0 | summarize V=toreal(count()) | extend Metric=\\\"Subnet/exit-node\\\", Order=7),\\n (D | where LastSeen < ago(30d) | summarize V=toreal(count()) | extend Metric=\\\"Stale (30+ days)\\\", Order=8)\\n| order by Order asc | project Metric, Value=V\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"tiles\",\"tileSettings\":{\"titleContent\":{\"columnMatch\":\"Metric\",\"formatter\":1},\"leftContent\":{\"columnMatch\":\"Value\",\"formatter\":12,\"formatOptions\":{\"palette\":\"auto\"}},\"showBorder\":false}},\"name\":\"q-366964fc\"},{\"type\":1,\"content\":{\"json\":\"
Distribution
\"},\"name\":\"div-distribution-396d03\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| summarize Count=count() by Os\\n| order by Count desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"piechart\",\"title\":\"Devices by OS\"},\"name\":\"q-0c8f5988\",\"customWidth\":\"33\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| summarize Count=count() by ClientVersion\\n| order by Count desc | take 10\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"barchart\",\"title\":\"Top 10 client versions\"},\"name\":\"q-af1c45cd\",\"customWidth\":\"33\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| mv-expand Tag = Tags to typeof(string)\\n| summarize Devices=dcount(DeviceId) by Tag=iff(isempty(Tag), \\\"(untagged)\\\", Tag)\\n| order by Devices desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"piechart\",\"title\":\"Devices by tag\"},\"name\":\"q-c3a0ef5b\",\"customWidth\":\"33\"},{\"type\":1,\"content\":{\"json\":\"
Devices needing attention
\"},\"name\":\"div-devices-needing-atte-f04a47\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| where UpdateAvailable == true or KeyExpiryDisabled == true or LastSeen < ago(30d) or Authorized == false\\n| extend Issues = strcat_array(pack_array(\\n iff(UpdateAvailable == true, \\\"needs-update\\\", \\\"\\\"),\\n iff(KeyExpiryDisabled == true, \\\"key-never-expires\\\", \\\"\\\"),\\n iff(LastSeen < ago(30d), \\\"stale\\\", \\\"\\\"),\\n iff(Authorized == false, \\\"unauthorized\\\", \\\"\\\")), \\\",\\\")\\n| extend Issues = trim(\\\",\\\", trim_start(\\\",\\\", trim_end(\\\",\\\", replace_string(Issues, \\\",,\\\", \\\",\\\"))))\\n| project DeviceName, Hostname, User, Os, ClientVersion, LastSeen, Issues\\n| order by LastSeen desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Devices flagged with one or more issues\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"LastSeen\",\"formatter\":6},{\"columnMatch\":\"Issues\",\"formatter\":1}]},\"noDataMessage\":\"No devices need attention - all updated, fresh, authorized, and key-rotating.\",\"noDataMessageStyle\":1},\"name\":\"q-dc5db84c\"},{\"type\":1,\"content\":{\"json\":\"
Full device inventory
\"},\"name\":\"div-full-device-inventor-b70642\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| project DeviceName, Hostname, User, Os, ClientVersion, UpdateAvailable, Authorized, IsExternal, SshEnabled, LastSeen, KeyExpiryDisabled, Tags, AdvertisedRoutes\\n| order by LastSeen desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"All devices (latest snapshot per device ID)\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"LastSeen\",\"formatter\":6},{\"columnMatch\":\"Os\",\"formatter\":1},{\"columnMatch\":\"ClientVersion\",\"formatter\":1}]}},\"name\":\"q-7f7e7a9a\"},{\"type\":1,\"content\":{\"json\":\"
Subnet routers / exit nodes
\"},\"name\":\"div-subnet-routers-/-exi-b082d6\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| where array_length(AdvertisedRoutes) > 0\\n| extend AdvertisedSummary = tostring(AdvertisedRoutes), EnabledSummary = tostring(EnabledRoutes)\\n| project DeviceName, Hostname, User, Os, AdvertisedSummary, EnabledSummary, LastSeen, Authorized\\n| order by LastSeen desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Devices advertising subnet routes or exit-node capability\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"LastSeen\",\"formatter\":6}]},\"noDataMessage\":\"No subnet routers in this tailnet - pure mesh topology.\",\"noDataMessageStyle\":1},\"name\":\"q-5004df25\"}]},\"conditionalVisibility\":{\"parameterName\":\"selectedTab\",\"comparison\":\"isEqualTo\",\"value\":\"devices\"},\"name\":\"group-devices\"},{\"type\":12,\"content\":{\"version\":\"NotebookGroup/1.0\",\"groupType\":\"editable\",\"items\":[{\"type\":1,\"content\":{\"json\":\"
Credentials snapshot
\"},\"name\":\"div-credentials-snapshot-fd464a\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"let K = Tailscale_Keys_CL | summarize arg_max(TimeGenerated, *) by KeyId;\\nunion\\n (K | summarize V=toreal(count()) | extend Metric=\\\"Total keys\\\", Order=1),\\n (K | where isnull(Revoked) and (isnull(Expires) or Expires > now()) | summarize V=toreal(count()) | extend Metric=\\\"Active\\\", Order=2),\\n (K | where isnotnull(Revoked) | summarize V=toreal(count()) | extend Metric=\\\"Revoked\\\", Order=3),\\n (K | where Expires < now() and isnull(Revoked) | summarize V=toreal(count()) | extend Metric=\\\"Expired\\\", Order=4),\\n (K | where isnull(Revoked) and Expires between(now() .. ago(-7d)) | summarize V=toreal(count()) | extend Metric=\\\"Expiring in 7d\\\", Order=5),\\n (K | where isnull(Revoked) and (isnull(Expires) or ExpirySeconds==0) | summarize V=toreal(count()) | extend Metric=\\\"Never expire\\\", Order=6),\\n (K | where KeyType =~ \\\"auth\\\" | summarize V=toreal(count()) | extend Metric=\\\"Auth keys\\\", Order=7),\\n (K | where KeyType =~ \\\"api\\\" or KeyType contains \\\"oauth\\\" | summarize V=toreal(count()) | extend Metric=\\\"API / OAuth\\\", Order=8)\\n| order by Order asc | project Metric, Value=V\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"tiles\",\"tileSettings\":{\"titleContent\":{\"columnMatch\":\"Metric\",\"formatter\":1},\"leftContent\":{\"columnMatch\":\"Value\",\"formatter\":12,\"formatOptions\":{\"palette\":\"auto\"}},\"showBorder\":false}},\"name\":\"q-79398bc0\"},{\"type\":1,\"content\":{\"json\":\"
Distribution
\"},\"name\":\"div-distribution-15f665\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Keys_CL\\n| summarize arg_max(TimeGenerated, *) by KeyId\\n| summarize Count=count() by KeyType\\n| order by Count desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"piechart\",\"title\":\"Keys by type\"},\"name\":\"q-23e6618b\",\"customWidth\":\"50\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Keys_CL\\n| summarize arg_max(TimeGenerated, *) by KeyId\\n| where isnull(Revoked)\\n| extend Bucket = case(\\n isnull(Expires) or ExpirySeconds == 0, \\\"Never\\\",\\n Expires < now(), \\\"Already expired\\\",\\n Expires < ago(-1d), \\\"<24h\\\",\\n Expires < ago(-7d), \\\"1-7d\\\",\\n Expires < ago(-30d), \\\"8-30d\\\",\\n Expires < ago(-90d), \\\"31-90d\\\",\\n \\\"90+d\\\")\\n| summarize Keys=count() by Bucket\\n| order by case(Bucket==\\\"Already expired\\\",1, Bucket==\\\"<24h\\\",2, Bucket==\\\"1-7d\\\",3, Bucket==\\\"8-30d\\\",4, Bucket==\\\"31-90d\\\",5, Bucket==\\\"90+d\\\",6, Bucket==\\\"Never\\\",7, 8) asc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"barchart\",\"title\":\"Active key expiry distribution\"},\"name\":\"q-7484d1a0\",\"customWidth\":\"50\"},{\"type\":1,\"content\":{\"json\":\"
Active credential register
\"},\"name\":\"div-active-credential-re-c27539\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Keys_CL\\n| summarize arg_max(TimeGenerated, *) by KeyId\\n| where isnull(Revoked)\\n| extend ExpiryStatus = case(\\n isnull(Expires) or ExpirySeconds == 0, \\\"Never expires\\\",\\n Expires < now(), \\\"Expired\\\",\\n Expires < ago(-7d), \\\"Expires in 7d\\\",\\n Expires < ago(-30d), \\\"Expires in 30d\\\",\\n \\\"OK\\\")\\n| project KeyId, KeyType, Description, UserId, Created, Expires, ExpiryStatus, Capabilities\\n| order by Created desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"All active credentials with computed expiry status\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Created\",\"formatter\":6},{\"columnMatch\":\"Expires\",\"formatter\":6},{\"columnMatch\":\"ExpiryStatus\",\"formatter\":1},{\"columnMatch\":\"KeyType\",\"formatter\":1}]}},\"name\":\"q-4b27a750\"},{\"type\":1,\"content\":{\"json\":\"
Credential CRUD events
\"},\"name\":\"div-credential-crud-even-e455b0\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| where Action contains \\\"API_KEY\\\" or Action contains \\\"AUTH_KEY\\\" or Action contains \\\"OAUTH\\\" or Action contains \\\"KEY_CREATE\\\" or Action contains \\\"KEY_REVOKE\\\"\\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\\n| extend TargetId=tostring(Target.id), TargetType=tostring(Target.type)\\n| project TimeGenerated, Action, ActorLogin, TargetType, TargetId, Origin\\n| order by TimeGenerated desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Recent credential create / revoke / rotate events\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"TimeGenerated\",\"formatter\":6}]},\"noDataMessage\":\"No credential CRUD activity in this window.\",\"noDataMessageStyle\":1},\"name\":\"q-777693bd\"}]},\"conditionalVisibility\":{\"parameterName\":\"selectedTab\",\"comparison\":\"isEqualTo\",\"value\":\"credentials\"},\"name\":\"group-credentials\"},{\"type\":12,\"content\":{\"version\":\"NotebookGroup/1.0\",\"groupType\":\"editable\",\"items\":[{\"type\":1,\"content\":{\"json\":\"
Audit volume
\"},\"name\":\"div-audit-volume-de29a2\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| summarize Events=count() by bin(TimeGenerated, 1h)\\n| order by TimeGenerated asc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"timechart\",\"title\":\"Audit events per hour\",\"noDataMessage\":\"No audit events in this window.\",\"noDataMessageStyle\":5},\"name\":\"q-f5eee265\"},{\"type\":1,\"content\":{\"json\":\"
Action heatmap by hour of day
\"},\"name\":\"div-action-heatmap-by-ho-c8bd5e\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend Hour=hourofday(TimeGenerated)\\n| summarize Events=count() by Hour, Action\\n| order by Hour asc, Events desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"categoricalbar\",\"title\":\"When are admin actions happening?\"},\"name\":\"q-c6f45d95\"},{\"type\":1,\"content\":{\"json\":\"
Actor / Action heatmap
\"},\"name\":\"div-actor-/-action-heatm-d820ba\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\\n| where isnotempty(ActorLogin)\\n| summarize Events=count() by ActorLogin, Action\\n| order by Events desc | take 100\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Who is firing which action\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Events\",\"formatter\":4,\"formatOptions\":{\"palette\":\"blue\"}}]},\"noDataMessage\":\"No audit events in this window.\",\"noDataMessageStyle\":5},\"name\":\"q-06c13b3b\"},{\"type\":1,\"content\":{\"json\":\"
Recent activity
\"},\"name\":\"div-recent-activity-63b210\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\\n| extend TargetType=tostring(Target.type), TargetName=tostring(coalesce(Target.name, Target.id))\\n| project TimeGenerated, Action, ActorLogin, TargetType, TargetName, Origin, EventGroupID\\n| order by TimeGenerated desc | take 100\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Last 100 audit events\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"TimeGenerated\",\"formatter\":6},{\"columnMatch\":\"Action\",\"formatter\":1}]},\"noDataMessage\":\"No audit events in this window.\",\"noDataMessageStyle\":5},\"name\":\"q-07a25a40\"}]},\"conditionalVisibility\":{\"parameterName\":\"selectedTab\",\"comparison\":\"isEqualTo\",\"value\":\"audit\"},\"name\":\"group-audit\"},{\"type\":12,\"content\":{\"version\":\"NotebookGroup/1.0\",\"groupType\":\"editable\",\"items\":[{\"type\":1,\"content\":{\"json\":\"
DNS configuration (current state)
\"},\"name\":\"div-dns-configuration-(c-5f2e1e\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Dns_CL\\n| summarize arg_max(TimeGenerated, *) by ConfigType\\n| project ConfigType, Nameservers, MagicDNS, SearchPaths, LastSnapshot=TimeGenerated\\n| order by ConfigType asc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"MagicDNS, nameservers, search paths\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"LastSnapshot\",\"formatter\":6},{\"columnMatch\":\"ConfigType\",\"formatter\":1}]},\"noDataMessage\":\"No DNS snapshots in the workspace yet. DNS polls runs at ~30 min cadence.\",\"noDataMessageStyle\":5},\"name\":\"q-d6a6a358\"},{\"type\":1,\"content\":{\"json\":\"
Tailnet settings (current)
\"},\"name\":\"div-tailnet-settings-(cu-e03b16\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Settings_CL\\n| summarize arg_max(TimeGenerated, *) by TenantId\\n| project DevicesApprovalOn, DevicesAutoUpdatesOn, DevicesKeyDurationDays, UsersApprovalOn, NetworkFlowLoggingOn, RegionalRoutingOn, PostureIdentityCollectionOn, UsersRoleAllowedToJoinExternalTailnets, LastSnapshot=TimeGenerated\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Tailnet policy gates\",\"noDataMessage\":\"No settings snapshot yet.\",\"noDataMessageStyle\":5},\"name\":\"q-0622cfc3\"},{\"type\":1,\"content\":{\"json\":\"
DNS change history
\"},\"name\":\"div-dns-change-history-9dd376\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| extend TargetProperty=tostring(Target.property)\\n| where Action contains \\\"DNS\\\" or TargetProperty has_any (\\\"DNS_NAMESERVERS\\\", \\\"DNS_SPLIT_DNS\\\", \\\"MAGICDNS\\\", \\\"DNS_SEARCH_PATHS\\\")\\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\\n| project TimeGenerated, ActorLogin, Action, TargetProperty, Origin\\n| order by TimeGenerated desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Recent DNS-related admin changes\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"TimeGenerated\",\"formatter\":6}]},\"noDataMessage\":\"No DNS changes in this window.\",\"noDataMessageStyle\":1},\"name\":\"q-a132ff6a\"},{\"type\":1,\"content\":{\"json\":\"
ACL policy changes
\"},\"name\":\"div-acl-policy-changes-ff0e68\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| where Action == \\\"ACL_UPDATE\\\" or Action contains \\\"ACL\\\"\\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\\n| project TimeGenerated, ActorLogin, Action, Origin, EventGroupID\\n| order by TimeGenerated desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Recent ACL / policy file modifications\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"TimeGenerated\",\"formatter\":6}]},\"noDataMessage\":\"No ACL changes in this window.\",\"noDataMessageStyle\":1},\"name\":\"q-94818c53\"},{\"type\":1,\"content\":{\"json\":\"
Subnet routes & exit nodes
\"},\"name\":\"div-subnet-routes-and-ex-eef47f\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Devices_CL\\n| summarize arg_max(TimeGenerated, *) by DeviceId\\n| where array_length(EnabledRoutes) > 0\\n| project DeviceName, User, Os, EnabledRoutes=tostring(EnabledRoutes), AdvertisedRoutes=tostring(AdvertisedRoutes), LastSeen\\n| order by LastSeen desc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Routes currently being served from devices\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"LastSeen\",\"formatter\":6}]},\"noDataMessage\":\"No subnet routers active in this tailnet.\",\"noDataMessageStyle\":1},\"name\":\"q-61a792cd\"}]},\"conditionalVisibility\":{\"parameterName\":\"selectedTab\",\"comparison\":\"isEqualTo\",\"value\":\"network\"},\"name\":\"group-network\"},{\"type\":12,\"conditionalVisibility\":{\"parameterName\":\"selectedTab\",\"comparison\":\"isEqualTo\",\"value\":\"network-flows\"},\"name\":\"group-network-flows\",\"content\":{\"version\":\"NotebookGroup/1.0\",\"groupType\":\"editable\",\"items\":[{\"type\":1,\"content\":{\"json\":\"
Activity snapshot
\"},\"name\":\"div-flow-snapshot\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"let NET = Tailscale_Network_CL | where TimeGenerated {TimeRange};\\nunion\\n (NET | summarize V=toreal(count()) | extend Metric=\\\"Total flows\\\", Order=1),\\n (NET | summarize V=toreal(dcount(SrcNodeName)) | extend Metric=\\\"Src nodes\\\", Order=2),\\n (NET | summarize V=toreal(dcount(DstNodeName)) | extend Metric=\\\"Dst nodes\\\", Order=3),\\n (NET | where HasVirtualTraffic | summarize V=toreal(count())| extend Metric=\\\"Virtual\\\", Order=4),\\n (NET | where HasSubnetTraffic | summarize V=toreal(count())| extend Metric=\\\"Subnet\\\", Order=5),\\n (NET | where HasExitTraffic | summarize V=toreal(count())| extend Metric=\\\"Exit\\\", Order=6),\\n (NET | where IsRelayed | summarize V=toreal(count())| extend Metric=\\\"Relayed (DERP)\\\", Order=7)\\n| order by Order asc | project Metric, Value=V\",\"size\":3,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"tiles\",\"title\":\"Activity (selected time range)\",\"noDataMessage\":\"No data in this window.\",\"noDataMessageStyle\":5,\"tileSettings\":{\"titleContent\":{\"columnMatch\":\"Metric\",\"formatter\":1},\"leftContent\":{\"columnMatch\":\"Value\",\"formatter\":12,\"formatOptions\":{\"palette\":\"auto\"},\"numberFormat\":{\"unit\":17,\"options\":{\"style\":\"decimal\",\"maximumFractionDigits\":0}}},\"showBorder\":false}},\"name\":\"q-flow-tiles\"},{\"type\":1,\"content\":{\"json\":\"
Top talkers
\"},\"name\":\"div-flow-top-talkers\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Network_CL\\n| where TimeGenerated {TimeRange}\\n| extend SrcLabel = case(\\n isnotempty(SrcUser), strcat(SrcNodeName, \\\" - \\\", SrcUser),\\n isnotempty(SrcTags), strcat(SrcNodeName, \\\" \\\", tostring(SrcTags)),\\n SrcNodeName)\\n| summarize Flows=count() by SrcLabel\\n| top 10 by Flows\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"barchart\",\"title\":\"Top source nodes by flow count\",\"noDataMessage\":\"No flow records in this window.\",\"noDataMessageStyle\":5},\"name\":\"q-flow-top-src\",\"customWidth\":\"50\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Network_CL\\n| where TimeGenerated {TimeRange}\\n| extend DstLabel = case(\\n isnotempty(DstUser), strcat(DstNodeName, \\\" - \\\", DstUser),\\n isnotempty(DstTags), strcat(DstNodeName, \\\" \\\", tostring(DstTags)),\\n DstNodeName)\\n| extend DstKind = case(\\n isnotempty(DstUser), \\\"User device\\\",\\n isnotempty(DstTags), \\\"Tagged service\\\",\\n \\\"Other\\\")\\n| summarize Flows=count() by DstLabel, DstKind\\n| top 10 by Flows\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"categoricalbar\",\"title\":\"Top destination nodes (split by user vs tagged service)\",\"noDataMessage\":\"No flow records in this window.\",\"noDataMessageStyle\":5},\"name\":\"q-flow-top-dst\",\"customWidth\":\"50\"},{\"type\":1,\"content\":{\"json\":\"
Traffic mix over time (stacked area: Virtual / Subnet / Exit / Physical)
\"},\"name\":\"div-flow-time-mix\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"let NET = Tailscale_Network_CL | where TimeGenerated {TimeRange};\\nunion\\n (NET | where HasVirtualTraffic | summarize Flows=count() by bin(TimeGenerated, 10m) | extend Kind=\\\"Virtual\\\"),\\n (NET | where HasSubnetTraffic | summarize Flows=count() by bin(TimeGenerated, 10m) | extend Kind=\\\"Subnet\\\"),\\n (NET | where HasExitTraffic | summarize Flows=count() by bin(TimeGenerated, 10m) | extend Kind=\\\"Exit\\\"),\\n (NET | where HasPhysicalTraffic | summarize Flows=count() by bin(TimeGenerated, 10m) | extend Kind=\\\"Physical\\\")\\n| project TimeGenerated, Kind, Flows\\n| order by TimeGenerated asc\",\"size\":4,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"timechart\",\"title\":\"Flow count by traffic kind over time\",\"noDataMessage\":\"No flow records.\",\"noDataMessageStyle\":5,\"chartSettings\":{\"group\":\"Kind\",\"createOtherGroup\":10,\"showLegend\":true,\"ySettings\":{\"min\":0},\"chartType\":\"Area\"}},\"name\":\"q-flow-traffic-mix\"},{\"type\":1,\"content\":{\"json\":\"
DERP relay health
\"},\"name\":\"div-flow-derp-watch\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Network_CL\\n| where TimeGenerated {TimeRange}\\n| summarize Count=count() by Path = iff(IsRelayed, \\\"Relayed (DERP)\\\", \\\"Direct (P2P)\\\")\\n| project Path, Count\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"piechart\",\"title\":\"Direct vs relayed\",\"noDataMessage\":\"No flow records.\",\"noDataMessageStyle\":5},\"name\":\"q-flow-derp-pie\",\"customWidth\":\"40\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Network_CL\\n| where TimeGenerated {TimeRange}\\n| where IsRelayed\\n| extend SrcLabel = strcat(SrcNodeName, iff(isempty(SrcUser),\\\"\\\",strcat(\\\" (\\\",SrcUser,\\\")\\\")))\\n| summarize RelayedFlows=count(), LastRelay=max(TimeGenerated) by SrcLabel, SrcOs\\n| top 10 by RelayedFlows\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Devices stuck on DERP (potential NAT/firewall issue)\",\"noDataMessage\":\"No data in this window.\",\"noDataMessageStyle\":5,\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"RelayedFlows\",\"formatter\":4,\"formatOptions\":{\"palette\":\"orange\"}},{\"columnMatch\":\"LastRelay\",\"formatter\":6}]}},\"name\":\"q-flow-derp-devices\",\"customWidth\":\"60\"},{\"type\":1,\"content\":{\"json\":\"
Tagged-service flows + anomaly hunt
\"},\"name\":\"div-flow-tag-anomaly\"},{\"type\":12,\"name\":\"row-tag-anomaly-row\",\"content\":{\"version\":\"NotebookGroup/1.0\",\"groupType\":\"editable\",\"items\":[{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Network_CL\\n| where TimeGenerated {TimeRange}\\n| extend SrcKind = case(\\n isnotempty(SrcTags), tostring(SrcTags),\\n isnotempty(SrcUser), \\\"\\\",\\n \\\"\\\")\\n| extend DstKind = case(\\n isnotempty(DstTags), tostring(DstTags),\\n isnotempty(DstUser), \\\"\\\",\\n \\\"\\\")\\n| summarize Flows=count() by SrcKind, DstKind\\n| order by Flows desc\",\"size\":1,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Tagged-service flow matrix\",\"noDataMessage\":\"No data in this window.\",\"noDataMessageStyle\":5,\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Flows\",\"formatter\":4,\"formatOptions\":{\"palette\":\"blue\"}}]}},\"name\":\"q-flow-tag-matrix\",\"customWidth\":\"50\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"let baseline = Tailscale_Network_CL\\n | where TimeGenerated between (ago(7d) .. ago(1h))\\n | distinct SrcNodeName, DstNodeName;\\nTailscale_Network_CL\\n| where TimeGenerated {TimeRange}\\n| where HasVirtualTraffic or HasSubnetTraffic or HasExitTraffic\\n| extend ShortSrc = tostring(split(SrcNodeName, \\\".\\\")[0])\\n| extend ShortDst = tostring(split(DstNodeName, \\\".\\\")[0])\\n| extend ShortSrcUser = tostring(split(SrcUser, \\\"@\\\")[0])\\n| extend ShortDstUser = tostring(split(DstUser, \\\"@\\\")[0])\\n| extend Src = strcat(ShortSrc, iff(isempty(ShortSrcUser),\\\"\\\",strcat(\\\" - \\\",ShortSrcUser)))\\n| extend Dst = strcat(ShortDst, iff(isempty(ShortDstUser),\\\"\\\",strcat(\\\" - \\\",ShortDstUser)))\\n| summarize FirstSeen=min(TimeGenerated), Flows=count() by Src, Dst, SrcNodeName, DstNodeName\\n| join kind=leftanti baseline on SrcNodeName, DstNodeName\\n| project FirstSeen, Src, Dst, Flows\\n| order by FirstSeen desc\",\"size\":1,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Anomaly: pairs NOT seen in past 7 days\",\"noDataMessage\":\"No new src/dst pairs - all flows match the 7-day baseline.\",\"noDataMessageStyle\":5,\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"FirstSeen\",\"formatter\":6},{\"columnMatch\":\"Flows\",\"formatter\":4,\"formatOptions\":{\"palette\":\"red\"}}]}},\"name\":\"q-flow-new-pairs\",\"customWidth\":\"50\"}]}},{\"type\":1,\"content\":{\"json\":\"
Egress destinations - exit nodes + subnet routes
\"},\"name\":\"div-flow-egress\"},{\"type\":12,\"name\":\"row-egress-row\",\"content\":{\"version\":\"NotebookGroup/1.0\",\"groupType\":\"editable\",\"items\":[{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Network_CL\\n| where TimeGenerated {TimeRange}\\n| where HasExitTraffic\\n| extend ExitTag = iff(isempty(DstTags), DstNodeName, tostring(DstTags))\\n| summarize Flows=count() by ExitTag\",\"size\":1,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"piechart\",\"title\":\"Exit-node tag distribution\",\"noDataMessage\":\"No exit-node traffic in this window.\",\"noDataMessageStyle\":5},\"name\":\"q-flow-exit-providers\",\"customWidth\":\"40\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Network_CL\\n| where TimeGenerated {TimeRange}\\n| where HasSubnetTraffic\\n| mv-expand s=SubnetTraffic\\n| extend DstHost = tostring(split(tostring(s.dst), \\\":\\\")[0])\\n| extend Bytes = toint(coalesce(s.txBytes,0)) + toint(coalesce(s.rxBytes,0))\\n| extend Pkts = toint(coalesce(s.txPkts,0)) + toint(coalesce(s.rxPkts,0))\\n| summarize TotalBytes=sum(Bytes), TotalPkts=sum(Pkts), Talkers=dcount(SrcNodeName) by DstHost\\n| top 10 by TotalBytes\\n| project DstHost, TotalBytes, TotalPkts, Talkers\",\"size\":1,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Top subnet route destinations\",\"noDataMessage\":\"No subnet-route traffic in this window.\",\"noDataMessageStyle\":5,\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"TotalBytes\",\"formatter\":4,\"formatOptions\":{\"palette\":\"green\"}},{\"columnMatch\":\"TotalPkts\",\"formatter\":4,\"formatOptions\":{\"palette\":\"blue\"}},{\"columnMatch\":\"Talkers\",\"formatter\":4,\"formatOptions\":{\"palette\":\"purple\"}}]}},\"name\":\"q-flow-subnet-dests\",\"customWidth\":\"60\"}]}},{\"type\":1,\"content\":{\"json\":\"
Recent flow detail (last 100)
\"},\"name\":\"div-flow-recent-detail\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Network_CL\\n| where TimeGenerated {TimeRange}\\n| order by TimeGenerated desc\\n| take 100\\n| project TimeGenerated,\\n Src=SrcNodeName, SrcUser, SrcTags=tostring(SrcTags),\\n Dst=DstNodeName, DstUser, DstTags=tostring(DstTags),\\n Vir=HasVirtualTraffic, Sub=HasSubnetTraffic, Exi=HasExitTraffic, Phy=HasPhysicalTraffic, IsRelayed\",\"size\":4,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Last 100 flow records in time range\",\"noDataMessage\":\"No data in this window.\",\"noDataMessageStyle\":5,\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"TimeGenerated\",\"formatter\":6},{\"columnMatch\":\"IsRelayed\",\"formatter\":11}]}},\"name\":\"q-flow-recent\"}]}},{\"type\":12,\"conditionalVisibility\":{\"parameterName\":\"selectedTab\",\"comparison\":\"isEqualTo\",\"value\":\"posture\"},\"name\":\"group-posture\",\"content\":{\"version\":\"NotebookGroup/1.0\",\"groupType\":\"editable\",\"items\":[{\"type\":1,\"content\":{\"json\":\"
Posture integrations - MDM/EDR providers configured for device posture
\"},\"name\":\"div-posture-overview\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"let CUR = Tailscale_PostureIntegrations_CL\\n | where TimeGenerated {TimeRange}\\n | summarize arg_max(TimeGenerated, *) by IntegrationId;\\nunion\\n (CUR | summarize V=toreal(count()) | extend Metric=\\\"Integrations\\\", Order=1),\\n (CUR | summarize V=toreal(dcount(Provider)) | extend Metric=\\\"Distinct providers\\\", Order=2),\\n (CUR | where tostring(Status) has \\\"healthy\\\" or tostring(Status) has \\\"ok\\\"\\n | summarize V=toreal(count()) | extend Metric=\\\"Healthy\\\", Order=3),\\n (CUR | where not(tostring(Status) has \\\"healthy\\\" or tostring(Status) has \\\"ok\\\")\\n | summarize V=toreal(count()) | extend Metric=\\\"Unhealthy / unknown\\\", Order=4)\\n| order by Order asc | project Metric, Value=V\",\"size\":3,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"tiles\",\"title\":\"Integration inventory (selected time range)\",\"noDataMessage\":\"No posture integrations configured. Set them up at https://login.tailscale.com/admin/settings/posture-integrations.\",\"noDataMessageStyle\":5,\"tileSettings\":{\"titleContent\":{\"columnMatch\":\"Metric\",\"formatter\":1},\"leftContent\":{\"columnMatch\":\"Value\",\"formatter\":12,\"formatOptions\":{\"palette\":\"auto\"},\"numberFormat\":{\"unit\":17,\"options\":{\"style\":\"decimal\",\"maximumFractionDigits\":0}}},\"showBorder\":false}},\"name\":\"q-posture-tile\"},{\"type\":1,\"content\":{\"json\":\"
Distribution
\"},\"name\":\"div-posture-distribution\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_PostureIntegrations_CL\\n| where TimeGenerated {TimeRange}\\n| summarize arg_max(TimeGenerated, *) by IntegrationId\\n| summarize Count=count() by Provider\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"piechart\",\"title\":\"Integrations by provider\",\"noDataMessage\":\"No posture integrations configured.\",\"noDataMessageStyle\":5},\"name\":\"q-posture-by-provider\",\"customWidth\":\"50\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_PostureIntegrations_CL\\n| where TimeGenerated {TimeRange}\\n| summarize arg_max(TimeGenerated, *) by IntegrationId\\n| extend Health = case(\\n tostring(Status) has \\\"healthy\\\" or tostring(Status) has \\\"ok\\\", \\\"Healthy\\\",\\n tostring(Status) has \\\"error\\\" or tostring(Status) has \\\"failed\\\", \\\"Error\\\",\\n isnotempty(tostring(Status)), \\\"Other\\\",\\n \\\"Unknown\\\")\\n| summarize Count=count() by Health\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"piechart\",\"title\":\"Health status across integrations\",\"noDataMessage\":\"No posture integrations configured.\",\"noDataMessageStyle\":5},\"name\":\"q-posture-by-status\",\"customWidth\":\"50\"},{\"type\":1,\"content\":{\"json\":\"
Inventory detail
\"},\"name\":\"div-posture-inventory\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_PostureIntegrations_CL\\n| where TimeGenerated {TimeRange}\\n| summarize arg_max(TimeGenerated, *) by IntegrationId\\n| project IntegrationId, Provider, CloudId, ClientId, TenantId_Provider, Status, LastSnapshot=TimeGenerated\",\"size\":4,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Configured integrations (latest snapshot per IntegrationId)\",\"noDataMessage\":\"No posture integrations configured.\",\"noDataMessageStyle\":5,\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Provider\",\"formatter\":11},{\"columnMatch\":\"LastSnapshot\",\"formatter\":6}]}},\"name\":\"q-posture-inventory\"},{\"type\":1,\"content\":{\"json\":\"
Lifecycle (from audit log)
\"},\"name\":\"div-posture-lifecycle\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"Tailscale_Audit_CL\\n| where TimeGenerated {TimeRange}\\n| where EventType == \\\"CONFIG\\\"\\n| where tostring(Target.type) contains \\\"POSTURE\\\" or tostring(Target.type) contains \\\"INTEGRATION\\\"\\n| project TimeGenerated, Actor=tostring(Actor.loginName), Action,\\n Target=tostring(Target.name), TargetType=tostring(Target.type), ActionDetails\\n| order by TimeGenerated desc\",\"size\":4,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Recent posture integration create/update/delete events\",\"noDataMessage\":\"No posture-integration audit events in this window.\",\"noDataMessageStyle\":5,\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"TimeGenerated\",\"formatter\":6},{\"columnMatch\":\"Action\",\"formatter\":11}]}},\"name\":\"q-posture-audit\"}]}},{\"type\":12,\"content\":{\"version\":\"NotebookGroup/1.0\",\"groupType\":\"editable\",\"items\":[{\"type\":1,\"content\":{\"json\":\"
Ingest rate per table
\"},\"name\":\"div-ingest-rate-per-tabl-24e5f6\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"union withsource=Table Tailscale_Audit_CL, Tailscale_Devices_CL, Tailscale_Users_CL, Tailscale_Keys_CL, Tailscale_Dns_CL, Tailscale_Settings_CL\\n| where TimeGenerated > ago(24h)\\n| summarize Rows=count() by Table, bin(TimeGenerated, 1h)\\n| order by TimeGenerated asc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"timechart\",\"title\":\"Rows ingested per Tailscale table per hour (last 24h)\",\"noDataMessage\":\"No Tailscale data ingested in the last 24h - check the connector card under Sentinel Data Connectors.\",\"noDataMessageStyle\":5},\"name\":\"q-c6fd0143\"},{\"type\":1,\"content\":{\"json\":\"
Last poll time per table
\"},\"name\":\"div-last-poll-time-per-t-49b051\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"union withsource=Table Tailscale_Audit_CL, Tailscale_Devices_CL, Tailscale_Users_CL, Tailscale_Keys_CL, Tailscale_Dns_CL, Tailscale_Settings_CL\\n| summarize LastRow=max(TimeGenerated), TotalRows=count() by Table\\n| extend MinutesAgo=toint((now() - LastRow) / 1m)\\n| extend Status=case(MinutesAgo < 60, \\\"Fresh\\\", MinutesAgo < 360, \\\"Recent\\\", MinutesAgo < 1440, \\\"Stale\\\", \\\"Very Stale\\\")\\n| project Table, LastRow, MinutesAgo, TotalRows, Status\\n| order by MinutesAgo asc\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Per-table freshness\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"LastRow\",\"formatter\":6},{\"columnMatch\":\"MinutesAgo\",\"formatter\":8,\"formatOptions\":{\"palette\":\"redBright\"}},{\"columnMatch\":\"TotalRows\",\"formatter\":8,\"formatOptions\":{\"palette\":\"blue\"}},{\"columnMatch\":\"Status\",\"formatter\":1}]}},\"name\":\"q-a02e37a8\"},{\"type\":1,\"content\":{\"json\":\"
Log Analytics operational events
\"},\"name\":\"div-log-analytics-operat-630749\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"_LogOperation\\n| where TimeGenerated > ago(24h)\\n| where _ResourceId contains \\\"tailscale\\\" or Detail contains \\\"Tailscale_\\\"\\n| project TimeGenerated, Operation, Level, Detail\\n| order by TimeGenerated desc | take 100\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"title\":\"Log Analytics operational events touching Tailscale tables\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"TimeGenerated\",\"formatter\":6},{\"columnMatch\":\"Level\",\"formatter\":1}]},\"noDataMessage\":\"No operational issues recorded in the last 24h.\",\"noDataMessageStyle\":1},\"name\":\"q-b492f150\"}]},\"conditionalVisibility\":{\"parameterName\":\"selectedTab\",\"comparison\":\"isEqualTo\",\"value\":\"pipeline\"},\"name\":\"group-pipeline\"},{\"type\":1,\"content\":{\"json\":\"
Tailscale Operations (Premium) (CCF) - Microsoft Sentinel content from the Tailscale (CCF) solution, Premium-tier surface. Tables polled from the Tailscale REST API: audit, devices, users, keys, dns, settings, network flows (/logging/network), posture integrations. Filter every panel via the time range above; the Investigate tab adds Actor and Device pickers for drilldown. The Network Flows and Posture tabs use the 15 promoted columns added in 3.1.0 (SrcUser, SrcTags, DstUser, DstTags, HasVirtualTraffic, HasSubnetTraffic, HasExitTraffic, HasPhysicalTraffic, IsRelayed, etc.). Companion workbook: Tailscale Operations (Standard) for Personal / Starter tailnets.
\"},\"name\":\"footer\"}],\"fallbackResourceIds\":[\"Azure Monitor\"],\"fromTemplateId\":\"sentinel-TailscalePremiumWorkbook\",\"$schema\":\"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"}\n", + "version": "1.0", + "sourceId": "[variables('workspaceResourceId')]", + "category": "sentinel" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('Workbook-', last(split(variables('workbookId2'),'/'))))]", + "properties": { + "description": "@{workbookKey=TailscalePremiumOperationsWorkbook; logoFileName=Tailscale.svg; description=Tailscale Operations workbook for Premium / Enterprise tier - everything in the Standard workbook plus network flow analysis (top talkers, src-dst pairs, exit-node egress, beaconing candidates) and posture integration inventory.; dataTypesDependencies=System.Object[]; dataConnectorsDependencies=System.Object[]; previewImagesFileNames=System.Object[]; version=1.0.0; title=Tailscale Operations (Premium); templateRelativePath=TailscalePremiumOperations.json; subtitle=; provider=Community; support=; source=; categories=; author=}.description", + "parentId": "[variables('workbookId2')]", + "contentId": "[variables('_workbookContentId2')]", + "kind": "Workbook", + "version": "[variables('workbookVersion2')]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + }, + "dependencies": { + "operator": "AND", + "criteria": [ + { + "contentId": "Tailscale_Audit_CL", + "kind": "DataType" + }, + { + "contentId": "Tailscale_Devices_CL", + "kind": "DataType" + }, + { + "contentId": "Tailscale_Users_CL", + "kind": "DataType" + }, + { + "contentId": "Tailscale_Keys_CL", + "kind": "DataType" + }, + { + "contentId": "Tailscale_Webhooks_CL", + "kind": "DataType" + }, + { + "contentId": "Tailscale_Settings_CL", + "kind": "DataType" + }, + { + "contentId": "Tailscale_Dns_CL", + "kind": "DataType" + }, + { + "contentId": "Tailscale_Network_CL", + "kind": "DataType" + }, + { + "contentId": "Tailscale_PostureIntegrations_CL", + "kind": "DataType" + }, + { + "contentId": "TailscalePremiumCCF", + "kind": "DataConnector" + } + ] + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('_workbookContentId2')]", + "contentKind": "Workbook", + "displayName": "[parameters('workbook2-name')]", + "contentProductId": "[variables('_workbookcontentProductId2')]", + "id": "[variables('_workbookcontentProductId2')]", + "version": "[variables('workbookVersion2')]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('parserObject1').parserTemplateSpecName1]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "vimNetworkSessionTailscale Data Parser with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('parserObject1').parserVersion1]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "name": "[variables('parserObject1')._parserName1]", + "apiVersion": "2025-07-01", + "type": "Microsoft.OperationalInsights/workspaces/savedSearches", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "ASIM Network Session parser for Tailscale", + "category": "Microsoft Sentinel Parser", + "functionAlias": "vimNetworkSessionTailscale", + "query": "let NetworkProtocolLookup = datatable(proto_lookup: int, NetworkProtocol_lookup: string)\n[\n 1, \"ICMP\",\n 6, \"TCP\",\n 17, \"UDP\",\n 58, \"ICMPv6\"\n];\nlet baseEvents = Tailscale_Network_CL;\nlet virtualFlows = baseEvents\n | where HasVirtualTraffic\n | mv-expand t = VirtualTraffic\n | extend NetworkSessionType = \"Virtual\", NetworkDirection = \"Local\";\nlet subnetFlows = baseEvents\n | where HasSubnetTraffic\n | mv-expand t = SubnetTraffic\n | extend NetworkSessionType = \"Subnet\", NetworkDirection = \"Outbound\";\nlet exitFlows = baseEvents\n | where HasExitTraffic\n | mv-expand t = ExitTraffic\n | extend NetworkSessionType = \"Exit\", NetworkDirection = \"Outbound\";\nunion virtualFlows, subnetFlows, exitFlows\n| extend\n SrcIpAddrAndPort = tostring(t.src),\n DstIpAddrAndPort = tostring(t.dst),\n proto_lookup = toint(t.proto),\n SrcBytes = tolong(t.txBytes),\n DstBytes = tolong(t.rxBytes),\n SrcPackets = tolong(t.txPkts),\n DstPackets = tolong(t.rxPkts)\n| extend\n SrcRawHost = extract(@\"^(.+):([0-9]+)$\", 1, SrcIpAddrAndPort),\n SrcPortNumber = toint(extract(@\"^(.+):([0-9]+)$\", 2, SrcIpAddrAndPort)),\n DstRawHost = extract(@\"^(.+):([0-9]+)$\", 1, DstIpAddrAndPort),\n DstPortNumber = toint(extract(@\"^(.+):([0-9]+)$\", 2, DstIpAddrAndPort))\n| extend\n SrcIpAddr = case(\n isempty(SrcRawHost), SrcIpAddrAndPort,\n SrcRawHost startswith \"[\", substring(SrcRawHost, 1, strlen(SrcRawHost) - 2),\n SrcRawHost\n ),\n DstIpAddr = case(\n isempty(DstRawHost), DstIpAddrAndPort,\n DstRawHost startswith \"[\", substring(DstRawHost, 1, strlen(DstRawHost) - 2),\n DstRawHost\n )\n| lookup NetworkProtocolLookup on proto_lookup\n| extend\n NetworkProtocol = coalesce(NetworkProtocol_lookup, tostring(proto_lookup)),\n NetworkBytes = SrcBytes + DstBytes,\n NetworkPackets = SrcPackets + DstPackets\n| extend\n EventStartTime = todatetime(FlowStart),\n EventEndTime = todatetime(FlowEnd),\n EventProduct = \"Tailscale\",\n EventVendor = \"Tailscale\",\n EventSchema = \"NetworkSession\",\n EventSchemaVersion = \"0.2.6\",\n EventType = \"NetworkSession\",\n EventResult = \"Success\",\n EventCount = int(1),\n EventSeverity = \"Informational\",\n DvcAction = \"Allow\"\n| extend\n SrcHostname = SrcNodeName,\n SrcUsername = SrcUser,\n SrcUsernameType = iff(isnotempty(SrcUser), \"Simple\", \"\"),\n SrcDvcOs = SrcOs,\n DstHostname = DstNodeName,\n DstUsername = DstUser,\n DstUsernameType = iff(isnotempty(DstUser), \"Simple\", \"\"),\n DstDvcOs = DstOs,\n Dvc = SrcNodeName,\n Src = SrcIpAddr,\n Dst = DstIpAddr,\n IpAddr = SrcIpAddr,\n User = SrcUser,\n Hostname = SrcNodeName\n| project-away\n t, NetworkProtocol_lookup, proto_lookup,\n VirtualTraffic, SubnetTraffic, ExitTraffic, PhysicalTraffic,\n HasVirtualTraffic, HasSubnetTraffic, HasExitTraffic, HasPhysicalTraffic,\n SrcAddresses, DstAddresses, SrcTags, DstTags,\n SrcOs, DstOs, SrcUser, DstUser, SrcNodeName, DstNodeName,\n DstCount, DstNodeId, NodeId, IsRelayed,\n SrcIpAddrAndPort, DstIpAddrAndPort, SrcRawHost, DstRawHost,\n FlowStart, FlowEnd,\n TenantId, _ResourceId, Type\n", + "functionParameters": "", + "version": 2, + "tags": [ + { + "name": "description", + "value": "" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('Parser-', last(split(variables('parserObject1')._parserId1,'/'))))]", + "dependsOn": [ + "[variables('parserObject1')._parserId1]" + ], + "properties": { + "parentId": "[resourceId('Microsoft.OperationalInsights/workspaces/savedSearches', parameters('workspace'), 'vimNetworkSessionTailscale')]", + "contentId": "[variables('parserObject1').parserContentId1]", + "kind": "Parser", + "version": "[variables('parserObject1').parserVersion1]", + "source": { + "name": "Tailscale (CCF)", + "kind": "Solution", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('parserObject1').parserContentId1]", + "contentKind": "Parser", + "displayName": "ASIM Network Session parser for Tailscale", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','pr','-', uniqueString(concat(variables('_solutionId'),'-','Parser','-',variables('parserObject1').parserContentId1,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','pr','-', uniqueString(concat(variables('_solutionId'),'-','Parser','-',variables('parserObject1').parserContentId1,'-', '1.0.0')))]", + "version": "[variables('parserObject1').parserVersion1]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/savedSearches", + "apiVersion": "2025-07-01", + "name": "[variables('parserObject1')._parserName1]", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "ASIM Network Session parser for Tailscale", + "category": "Microsoft Sentinel Parser", + "functionAlias": "vimNetworkSessionTailscale", + "query": "let NetworkProtocolLookup = datatable(proto_lookup: int, NetworkProtocol_lookup: string)\n[\n 1, \"ICMP\",\n 6, \"TCP\",\n 17, \"UDP\",\n 58, \"ICMPv6\"\n];\nlet baseEvents = Tailscale_Network_CL;\nlet virtualFlows = baseEvents\n | where HasVirtualTraffic\n | mv-expand t = VirtualTraffic\n | extend NetworkSessionType = \"Virtual\", NetworkDirection = \"Local\";\nlet subnetFlows = baseEvents\n | where HasSubnetTraffic\n | mv-expand t = SubnetTraffic\n | extend NetworkSessionType = \"Subnet\", NetworkDirection = \"Outbound\";\nlet exitFlows = baseEvents\n | where HasExitTraffic\n | mv-expand t = ExitTraffic\n | extend NetworkSessionType = \"Exit\", NetworkDirection = \"Outbound\";\nunion virtualFlows, subnetFlows, exitFlows\n| extend\n SrcIpAddrAndPort = tostring(t.src),\n DstIpAddrAndPort = tostring(t.dst),\n proto_lookup = toint(t.proto),\n SrcBytes = tolong(t.txBytes),\n DstBytes = tolong(t.rxBytes),\n SrcPackets = tolong(t.txPkts),\n DstPackets = tolong(t.rxPkts)\n| extend\n SrcRawHost = extract(@\"^(.+):([0-9]+)$\", 1, SrcIpAddrAndPort),\n SrcPortNumber = toint(extract(@\"^(.+):([0-9]+)$\", 2, SrcIpAddrAndPort)),\n DstRawHost = extract(@\"^(.+):([0-9]+)$\", 1, DstIpAddrAndPort),\n DstPortNumber = toint(extract(@\"^(.+):([0-9]+)$\", 2, DstIpAddrAndPort))\n| extend\n SrcIpAddr = case(\n isempty(SrcRawHost), SrcIpAddrAndPort,\n SrcRawHost startswith \"[\", substring(SrcRawHost, 1, strlen(SrcRawHost) - 2),\n SrcRawHost\n ),\n DstIpAddr = case(\n isempty(DstRawHost), DstIpAddrAndPort,\n DstRawHost startswith \"[\", substring(DstRawHost, 1, strlen(DstRawHost) - 2),\n DstRawHost\n )\n| lookup NetworkProtocolLookup on proto_lookup\n| extend\n NetworkProtocol = coalesce(NetworkProtocol_lookup, tostring(proto_lookup)),\n NetworkBytes = SrcBytes + DstBytes,\n NetworkPackets = SrcPackets + DstPackets\n| extend\n EventStartTime = todatetime(FlowStart),\n EventEndTime = todatetime(FlowEnd),\n EventProduct = \"Tailscale\",\n EventVendor = \"Tailscale\",\n EventSchema = \"NetworkSession\",\n EventSchemaVersion = \"0.2.6\",\n EventType = \"NetworkSession\",\n EventResult = \"Success\",\n EventCount = int(1),\n EventSeverity = \"Informational\",\n DvcAction = \"Allow\"\n| extend\n SrcHostname = SrcNodeName,\n SrcUsername = SrcUser,\n SrcUsernameType = iff(isnotempty(SrcUser), \"Simple\", \"\"),\n SrcDvcOs = SrcOs,\n DstHostname = DstNodeName,\n DstUsername = DstUser,\n DstUsernameType = iff(isnotempty(DstUser), \"Simple\", \"\"),\n DstDvcOs = DstOs,\n Dvc = SrcNodeName,\n Src = SrcIpAddr,\n Dst = DstIpAddr,\n IpAddr = SrcIpAddr,\n User = SrcUser,\n Hostname = SrcNodeName\n| project-away\n t, NetworkProtocol_lookup, proto_lookup,\n VirtualTraffic, SubnetTraffic, ExitTraffic, PhysicalTraffic,\n HasVirtualTraffic, HasSubnetTraffic, HasExitTraffic, HasPhysicalTraffic,\n SrcAddresses, DstAddresses, SrcTags, DstTags,\n SrcOs, DstOs, SrcUser, DstUser, SrcNodeName, DstNodeName,\n DstCount, DstNodeId, NodeId, IsRelayed,\n SrcIpAddrAndPort, DstIpAddrAndPort, SrcRawHost, DstRawHost,\n FlowStart, FlowEnd,\n TenantId, _ResourceId, Type\n", + "functionParameters": "", + "version": 2, + "tags": [ + { + "name": "description", + "value": "" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "location": "[parameters('workspace-location')]", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('Parser-', last(split(variables('parserObject1')._parserId1,'/'))))]", + "dependsOn": [ + "[variables('parserObject1')._parserId1]" + ], + "properties": { + "parentId": "[resourceId('Microsoft.OperationalInsights/workspaces/savedSearches', parameters('workspace'), 'vimNetworkSessionTailscale')]", + "contentId": "[variables('parserObject1').parserContentId1]", + "kind": "Parser", + "version": "[variables('parserObject1').parserVersion1]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('parserObject2').parserTemplateSpecName2]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "ASimNetworkSessionTailscale Data Parser with template version 3.0.1", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('parserObject2').parserVersion2]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "name": "[variables('parserObject2')._parserName2]", + "apiVersion": "2025-07-01", + "type": "Microsoft.OperationalInsights/workspaces/savedSearches", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "ASIM Network Session parser for Tailscale (no-prefix wrapper)", + "category": "Microsoft Sentinel Parser", + "functionAlias": "ASimNetworkSessionTailscale", + "query": "let NetworkProtocolLookup = datatable(proto_lookup: int, NetworkProtocol_lookup: string)\n[\n 1, \"ICMP\",\n 6, \"TCP\",\n 17, \"UDP\",\n 58, \"ICMPv6\"\n];\nlet baseEvents = Tailscale_Network_CL;\nlet virtualFlows = baseEvents\n | where HasVirtualTraffic\n | mv-expand t = VirtualTraffic\n | extend NetworkSessionType = \"Virtual\", NetworkDirection = \"Local\";\nlet subnetFlows = baseEvents\n | where HasSubnetTraffic\n | mv-expand t = SubnetTraffic\n | extend NetworkSessionType = \"Subnet\", NetworkDirection = \"Outbound\";\nlet exitFlows = baseEvents\n | where HasExitTraffic\n | mv-expand t = ExitTraffic\n | extend NetworkSessionType = \"Exit\", NetworkDirection = \"Outbound\";\nunion virtualFlows, subnetFlows, exitFlows\n| extend\n SrcIpAddrAndPort = tostring(t.src),\n DstIpAddrAndPort = tostring(t.dst),\n proto_lookup = toint(t.proto),\n SrcBytes = tolong(t.txBytes),\n DstBytes = tolong(t.rxBytes),\n SrcPackets = tolong(t.txPkts),\n DstPackets = tolong(t.rxPkts)\n| extend\n SrcRawHost = extract(@\"^(.+):([0-9]+)$\", 1, SrcIpAddrAndPort),\n SrcPortNumber = toint(extract(@\"^(.+):([0-9]+)$\", 2, SrcIpAddrAndPort)),\n DstRawHost = extract(@\"^(.+):([0-9]+)$\", 1, DstIpAddrAndPort),\n DstPortNumber = toint(extract(@\"^(.+):([0-9]+)$\", 2, DstIpAddrAndPort))\n| extend\n SrcIpAddr = case(\n isempty(SrcRawHost), SrcIpAddrAndPort,\n SrcRawHost startswith \"[\", substring(SrcRawHost, 1, strlen(SrcRawHost) - 2),\n SrcRawHost\n ),\n DstIpAddr = case(\n isempty(DstRawHost), DstIpAddrAndPort,\n DstRawHost startswith \"[\", substring(DstRawHost, 1, strlen(DstRawHost) - 2),\n DstRawHost\n )\n| lookup NetworkProtocolLookup on proto_lookup\n| extend\n NetworkProtocol = coalesce(NetworkProtocol_lookup, tostring(proto_lookup)),\n NetworkBytes = SrcBytes + DstBytes,\n NetworkPackets = SrcPackets + DstPackets\n| extend\n EventStartTime = todatetime(FlowStart),\n EventEndTime = todatetime(FlowEnd),\n EventProduct = \"Tailscale\",\n EventVendor = \"Tailscale\",\n EventSchema = \"NetworkSession\",\n EventSchemaVersion = \"0.2.6\",\n EventType = \"NetworkSession\",\n EventResult = \"Success\",\n EventCount = int(1),\n EventSeverity = \"Informational\",\n DvcAction = \"Allow\"\n| extend\n SrcHostname = SrcNodeName,\n SrcUsername = SrcUser,\n SrcUsernameType = iff(isnotempty(SrcUser), \"Simple\", \"\"),\n SrcDvcOs = SrcOs,\n DstHostname = DstNodeName,\n DstUsername = DstUser,\n DstUsernameType = iff(isnotempty(DstUser), \"Simple\", \"\"),\n DstDvcOs = DstOs,\n Dvc = SrcNodeName,\n Src = SrcIpAddr,\n Dst = DstIpAddr,\n IpAddr = SrcIpAddr,\n User = SrcUser,\n Hostname = SrcNodeName\n| project-away\n t, NetworkProtocol_lookup, proto_lookup,\n VirtualTraffic, SubnetTraffic, ExitTraffic, PhysicalTraffic,\n HasVirtualTraffic, HasSubnetTraffic, HasExitTraffic, HasPhysicalTraffic,\n SrcAddresses, DstAddresses, SrcTags, DstTags,\n SrcOs, DstOs, SrcUser, DstUser, SrcNodeName, DstNodeName,\n DstCount, DstNodeId, NodeId, IsRelayed,\n SrcIpAddrAndPort, DstIpAddrAndPort, SrcRawHost, DstRawHost,\n FlowStart, FlowEnd,\n TenantId, _ResourceId, Type\n", + "functionParameters": "", + "version": 2, + "tags": [ + { + "name": "description", + "value": "" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('Parser-', last(split(variables('parserObject2')._parserId2,'/'))))]", + "dependsOn": [ + "[variables('parserObject2')._parserId2]" + ], + "properties": { + "parentId": "[resourceId('Microsoft.OperationalInsights/workspaces/savedSearches', parameters('workspace'), 'ASimNetworkSessionTailscale')]", + "contentId": "[variables('parserObject2').parserContentId2]", + "kind": "Parser", + "version": "[variables('parserObject2').parserVersion2]", + "source": { + "name": "Tailscale (CCF)", + "kind": "Solution", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('parserObject2').parserContentId2]", + "contentKind": "Parser", + "displayName": "ASIM Network Session parser for Tailscale (no-prefix wrapper)", + "contentProductId": "[concat(take(variables('_solutionId'),50),'-','pr','-', uniqueString(concat(variables('_solutionId'),'-','Parser','-',variables('parserObject2').parserContentId2,'-', '1.0.0')))]", + "id": "[concat(take(variables('_solutionId'),50),'-','pr','-', uniqueString(concat(variables('_solutionId'),'-','Parser','-',variables('parserObject2').parserContentId2,'-', '1.0.0')))]", + "version": "[variables('parserObject2').parserVersion2]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/savedSearches", + "apiVersion": "2025-07-01", + "name": "[variables('parserObject2')._parserName2]", + "location": "[parameters('workspace-location')]", + "properties": { + "eTag": "*", + "displayName": "ASIM Network Session parser for Tailscale (no-prefix wrapper)", + "category": "Microsoft Sentinel Parser", + "functionAlias": "ASimNetworkSessionTailscale", + "query": "let NetworkProtocolLookup = datatable(proto_lookup: int, NetworkProtocol_lookup: string)\n[\n 1, \"ICMP\",\n 6, \"TCP\",\n 17, \"UDP\",\n 58, \"ICMPv6\"\n];\nlet baseEvents = Tailscale_Network_CL;\nlet virtualFlows = baseEvents\n | where HasVirtualTraffic\n | mv-expand t = VirtualTraffic\n | extend NetworkSessionType = \"Virtual\", NetworkDirection = \"Local\";\nlet subnetFlows = baseEvents\n | where HasSubnetTraffic\n | mv-expand t = SubnetTraffic\n | extend NetworkSessionType = \"Subnet\", NetworkDirection = \"Outbound\";\nlet exitFlows = baseEvents\n | where HasExitTraffic\n | mv-expand t = ExitTraffic\n | extend NetworkSessionType = \"Exit\", NetworkDirection = \"Outbound\";\nunion virtualFlows, subnetFlows, exitFlows\n| extend\n SrcIpAddrAndPort = tostring(t.src),\n DstIpAddrAndPort = tostring(t.dst),\n proto_lookup = toint(t.proto),\n SrcBytes = tolong(t.txBytes),\n DstBytes = tolong(t.rxBytes),\n SrcPackets = tolong(t.txPkts),\n DstPackets = tolong(t.rxPkts)\n| extend\n SrcRawHost = extract(@\"^(.+):([0-9]+)$\", 1, SrcIpAddrAndPort),\n SrcPortNumber = toint(extract(@\"^(.+):([0-9]+)$\", 2, SrcIpAddrAndPort)),\n DstRawHost = extract(@\"^(.+):([0-9]+)$\", 1, DstIpAddrAndPort),\n DstPortNumber = toint(extract(@\"^(.+):([0-9]+)$\", 2, DstIpAddrAndPort))\n| extend\n SrcIpAddr = case(\n isempty(SrcRawHost), SrcIpAddrAndPort,\n SrcRawHost startswith \"[\", substring(SrcRawHost, 1, strlen(SrcRawHost) - 2),\n SrcRawHost\n ),\n DstIpAddr = case(\n isempty(DstRawHost), DstIpAddrAndPort,\n DstRawHost startswith \"[\", substring(DstRawHost, 1, strlen(DstRawHost) - 2),\n DstRawHost\n )\n| lookup NetworkProtocolLookup on proto_lookup\n| extend\n NetworkProtocol = coalesce(NetworkProtocol_lookup, tostring(proto_lookup)),\n NetworkBytes = SrcBytes + DstBytes,\n NetworkPackets = SrcPackets + DstPackets\n| extend\n EventStartTime = todatetime(FlowStart),\n EventEndTime = todatetime(FlowEnd),\n EventProduct = \"Tailscale\",\n EventVendor = \"Tailscale\",\n EventSchema = \"NetworkSession\",\n EventSchemaVersion = \"0.2.6\",\n EventType = \"NetworkSession\",\n EventResult = \"Success\",\n EventCount = int(1),\n EventSeverity = \"Informational\",\n DvcAction = \"Allow\"\n| extend\n SrcHostname = SrcNodeName,\n SrcUsername = SrcUser,\n SrcUsernameType = iff(isnotempty(SrcUser), \"Simple\", \"\"),\n SrcDvcOs = SrcOs,\n DstHostname = DstNodeName,\n DstUsername = DstUser,\n DstUsernameType = iff(isnotempty(DstUser), \"Simple\", \"\"),\n DstDvcOs = DstOs,\n Dvc = SrcNodeName,\n Src = SrcIpAddr,\n Dst = DstIpAddr,\n IpAddr = SrcIpAddr,\n User = SrcUser,\n Hostname = SrcNodeName\n| project-away\n t, NetworkProtocol_lookup, proto_lookup,\n VirtualTraffic, SubnetTraffic, ExitTraffic, PhysicalTraffic,\n HasVirtualTraffic, HasSubnetTraffic, HasExitTraffic, HasPhysicalTraffic,\n SrcAddresses, DstAddresses, SrcTags, DstTags,\n SrcOs, DstOs, SrcUser, DstUser, SrcNodeName, DstNodeName,\n DstCount, DstNodeId, NodeId, IsRelayed,\n SrcIpAddrAndPort, DstIpAddrAndPort, SrcRawHost, DstRawHost,\n FlowStart, FlowEnd,\n TenantId, _ResourceId, Type\n", + "functionParameters": "", + "version": 2, + "tags": [ + { + "name": "description", + "value": "" + } + ] + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "location": "[parameters('workspace-location')]", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('Parser-', last(split(variables('parserObject2')._parserId2,'/'))))]", + "dependsOn": [ + "[variables('parserObject2')._parserId2]" + ], + "properties": { + "parentId": "[resourceId('Microsoft.OperationalInsights/workspaces/savedSearches', parameters('workspace'), 'ASimNetworkSessionTailscale')]", + "contentId": "[variables('parserObject2').parserContentId2]", + "kind": "Parser", + "version": "[variables('parserObject2').parserVersion2]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentPackages", + "apiVersion": "2023-04-01-preview", + "location": "[parameters('workspace-location')]", + "properties": { + "version": "3.0.0", + "kind": "Solution", + "contentSchemaVersion": "3.0.0", + "displayName": "Tailscale (CCF)", + "publisherDisplayName": "Tailscale (CCF)", + "descriptionHtml": "

Note: Please refer to the following before installing the solution:

\n

\u2022 Review the solution Release Notes

\n

\u2022 There may be known issues pertaining to this Solution, please refer to them before installing.

\n

The Tailscale solution for Microsoft Sentinel ingests Tailscale identity, device, configuration, audit and (Premium) network-flow telemetry via OAuth2-secured APIs. Built on the Codeless Connector Framework (CCF) - no Function App or container required.

\n

Data connectors in this solution (install the one matching your Tailscale plan):

\n
    \n
  • Tailscale Standard (CCF) - Configuration audit, devices, users, keys, webhooks, DNS, settings. Use on Personal (Free), Starter and Premium tailnets.
  • \n
  • Tailscale Premium (CCF) - Everything in Standard plus network flow logs and posture integrations. Use on Premium and Enterprise tailnets for full coverage.
  • \n
\n

Pre-requisites:

\n
    \n
  1. Sign in to Tailscale OAuth settings
  2. \n
  3. Create an OAuth client with the scopes for your tier (see the README in this solution).
  4. \n
  5. Copy the client ID and client secret (secret shown once).
  6. \n
  7. Note your tailnet name (e.g. tailb094d7.ts.net) from the Keys page.
  8. \n
\n

Data Connectors: 2, Parsers: 2, Workbooks: 2, Analytic Rules: 24, Hunting Queries: 22

\n

Learn more about Microsoft Sentinel | Learn more about Solutions

\n", + "contentKind": "Solution", + "contentProductId": "[variables('_solutioncontentProductId')]", + "id": "[variables('_solutioncontentProductId')]", + "icon": "", + "contentId": "[variables('_solutionId')]", + "parentId": "[variables('_solutionId')]", + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "noodlemctwoodle", + "email": "[variables('_email')]" + }, + "support": { + "name": "Tailscale (CCF)", + "email": "ccfconnectors.county118@passmail.com", + "tier": "Community", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + }, + "dependencies": { + "operator": "AND", + "criteria": [ + { + "kind": "DataConnector", + "contentId": "[variables('_dataConnectorContentIdConnections1')]", + "version": "[variables('dataConnectorCCPVersion')]" + }, + { + "kind": "DataConnector", + "contentId": "[variables('_dataConnectorContentIdConnections2')]", + "version": "[variables('dataConnectorCCPVersion')]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject1')._analyticRulecontentId1]", + "version": "[variables('analyticRuleObject1').analyticRuleVersion1]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject2')._analyticRulecontentId2]", + "version": "[variables('analyticRuleObject2').analyticRuleVersion2]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject3')._analyticRulecontentId3]", + "version": "[variables('analyticRuleObject3').analyticRuleVersion3]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject4')._analyticRulecontentId4]", + "version": "[variables('analyticRuleObject4').analyticRuleVersion4]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject5')._analyticRulecontentId5]", + "version": "[variables('analyticRuleObject5').analyticRuleVersion5]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject6')._analyticRulecontentId6]", + "version": "[variables('analyticRuleObject6').analyticRuleVersion6]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject7')._analyticRulecontentId7]", + "version": "[variables('analyticRuleObject7').analyticRuleVersion7]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject8')._analyticRulecontentId8]", + "version": "[variables('analyticRuleObject8').analyticRuleVersion8]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject9')._analyticRulecontentId9]", + "version": "[variables('analyticRuleObject9').analyticRuleVersion9]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject10')._analyticRulecontentId10]", + "version": "[variables('analyticRuleObject10').analyticRuleVersion10]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject11')._analyticRulecontentId11]", + "version": "[variables('analyticRuleObject11').analyticRuleVersion11]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject12')._analyticRulecontentId12]", + "version": "[variables('analyticRuleObject12').analyticRuleVersion12]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject13')._analyticRulecontentId13]", + "version": "[variables('analyticRuleObject13').analyticRuleVersion13]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject14')._analyticRulecontentId14]", + "version": "[variables('analyticRuleObject14').analyticRuleVersion14]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject15')._analyticRulecontentId15]", + "version": "[variables('analyticRuleObject15').analyticRuleVersion15]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject16')._analyticRulecontentId16]", + "version": "[variables('analyticRuleObject16').analyticRuleVersion16]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject17')._analyticRulecontentId17]", + "version": "[variables('analyticRuleObject17').analyticRuleVersion17]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject18')._analyticRulecontentId18]", + "version": "[variables('analyticRuleObject18').analyticRuleVersion18]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject19')._analyticRulecontentId19]", + "version": "[variables('analyticRuleObject19').analyticRuleVersion19]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject20')._analyticRulecontentId20]", + "version": "[variables('analyticRuleObject20').analyticRuleVersion20]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject21')._analyticRulecontentId21]", + "version": "[variables('analyticRuleObject21').analyticRuleVersion21]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject22')._analyticRulecontentId22]", + "version": "[variables('analyticRuleObject22').analyticRuleVersion22]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject23')._analyticRulecontentId23]", + "version": "[variables('analyticRuleObject23').analyticRuleVersion23]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject24')._analyticRulecontentId24]", + "version": "[variables('analyticRuleObject24').analyticRuleVersion24]" + }, + { + "kind": "HuntingQuery", + "contentId": "[variables('huntingQueryObject1')._huntingQuerycontentId1]", + "version": "[variables('huntingQueryObject1').huntingQueryVersion1]" + }, + { + "kind": "HuntingQuery", + "contentId": "[variables('huntingQueryObject2')._huntingQuerycontentId2]", + "version": "[variables('huntingQueryObject2').huntingQueryVersion2]" + }, + { + "kind": "HuntingQuery", + "contentId": "[variables('huntingQueryObject3')._huntingQuerycontentId3]", + "version": "[variables('huntingQueryObject3').huntingQueryVersion3]" + }, + { + "kind": "HuntingQuery", + "contentId": "[variables('huntingQueryObject4')._huntingQuerycontentId4]", + "version": "[variables('huntingQueryObject4').huntingQueryVersion4]" + }, + { + "kind": "HuntingQuery", + "contentId": "[variables('huntingQueryObject5')._huntingQuerycontentId5]", + "version": "[variables('huntingQueryObject5').huntingQueryVersion5]" + }, + { + "kind": "HuntingQuery", + "contentId": "[variables('huntingQueryObject6')._huntingQuerycontentId6]", + "version": "[variables('huntingQueryObject6').huntingQueryVersion6]" + }, + { + "kind": "HuntingQuery", + "contentId": "[variables('huntingQueryObject7')._huntingQuerycontentId7]", + "version": "[variables('huntingQueryObject7').huntingQueryVersion7]" + }, + { + "kind": "HuntingQuery", + "contentId": "[variables('huntingQueryObject8')._huntingQuerycontentId8]", + "version": "[variables('huntingQueryObject8').huntingQueryVersion8]" + }, + { + "kind": "HuntingQuery", + "contentId": "[variables('huntingQueryObject9')._huntingQuerycontentId9]", + "version": "[variables('huntingQueryObject9').huntingQueryVersion9]" + }, + { + "kind": "HuntingQuery", + "contentId": "[variables('huntingQueryObject10')._huntingQuerycontentId10]", + "version": "[variables('huntingQueryObject10').huntingQueryVersion10]" + }, + { + "kind": "HuntingQuery", + "contentId": "[variables('huntingQueryObject11')._huntingQuerycontentId11]", + "version": "[variables('huntingQueryObject11').huntingQueryVersion11]" + }, + { + "kind": "HuntingQuery", + "contentId": "[variables('huntingQueryObject12')._huntingQuerycontentId12]", + "version": "[variables('huntingQueryObject12').huntingQueryVersion12]" + }, + { + "kind": "HuntingQuery", + "contentId": "[variables('huntingQueryObject13')._huntingQuerycontentId13]", + "version": "[variables('huntingQueryObject13').huntingQueryVersion13]" + }, + { + "kind": "HuntingQuery", + "contentId": "[variables('huntingQueryObject14')._huntingQuerycontentId14]", + "version": "[variables('huntingQueryObject14').huntingQueryVersion14]" + }, + { + "kind": "HuntingQuery", + "contentId": "[variables('huntingQueryObject15')._huntingQuerycontentId15]", + "version": "[variables('huntingQueryObject15').huntingQueryVersion15]" + }, + { + "kind": "HuntingQuery", + "contentId": "[variables('huntingQueryObject16')._huntingQuerycontentId16]", + "version": "[variables('huntingQueryObject16').huntingQueryVersion16]" + }, + { + "kind": "HuntingQuery", + "contentId": "[variables('huntingQueryObject17')._huntingQuerycontentId17]", + "version": "[variables('huntingQueryObject17').huntingQueryVersion17]" + }, + { + "kind": "HuntingQuery", + "contentId": "[variables('huntingQueryObject18')._huntingQuerycontentId18]", + "version": "[variables('huntingQueryObject18').huntingQueryVersion18]" + }, + { + "kind": "HuntingQuery", + "contentId": "[variables('huntingQueryObject19')._huntingQuerycontentId19]", + "version": "[variables('huntingQueryObject19').huntingQueryVersion19]" + }, + { + "kind": "HuntingQuery", + "contentId": "[variables('huntingQueryObject20')._huntingQuerycontentId20]", + "version": "[variables('huntingQueryObject20').huntingQueryVersion20]" + }, + { + "kind": "HuntingQuery", + "contentId": "[variables('huntingQueryObject21')._huntingQuerycontentId21]", + "version": "[variables('huntingQueryObject21').huntingQueryVersion21]" + }, + { + "kind": "HuntingQuery", + "contentId": "[variables('huntingQueryObject22')._huntingQuerycontentId22]", + "version": "[variables('huntingQueryObject22').huntingQueryVersion22]" + }, + { + "kind": "Workbook", + "contentId": "[variables('_workbookContentId1')]", + "version": "[variables('workbookVersion1')]" + }, + { + "kind": "Workbook", + "contentId": "[variables('_workbookContentId2')]", + "version": "[variables('workbookVersion2')]" + }, + { + "kind": "Parser", + "contentId": "[variables('parserObject1').parserContentId1]", + "version": "[variables('parserObject1').parserVersion1]" + }, + { + "kind": "Parser", + "contentId": "[variables('parserObject2').parserContentId2]", + "version": "[variables('parserObject2').parserVersion2]" + } + ] + }, + "firstPublishDate": "2026-05-19", + "lastPublishDate": "2026-05-19", + "providers": [ + "Community" + ], + "categories": { + "domains": [ + "Networking", + "Security - Network", + "Identity" + ] + } + }, + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/', variables('_solutionId'))]" + } + ], + "outputs": {} +} \ No newline at end of file diff --git a/Solutions/Tailscale (CCF)/Package/testParameters.json b/Solutions/Tailscale (CCF)/Package/testParameters.json new file mode 100644 index 00000000000..8e1e27a3c33 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Package/testParameters.json @@ -0,0 +1,54 @@ +{ + "location": { + "type": "string", + "minLength": 1, + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Not used, but needed to pass arm-ttk test `Location-Should-Not-Be-Hardcoded`. We instead use the `workspace-location` which is derived from the LA workspace" + } + }, + "workspace-location": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "[concat('Region to deploy solution resources -- separate from location selection',parameters('location'))]" + } + }, + "workspace": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "Workspace name for Log Analytics where Microsoft Sentinel is setup" + } + }, + "resourceGroupName": { + "type": "string", + "defaultValue": "[resourceGroup().name]", + "metadata": { + "description": "resource group name where Microsoft Sentinel is setup" + } + }, + "subscription": { + "type": "string", + "defaultValue": "[last(split(subscription().id, '/'))]", + "metadata": { + "description": "subscription id where Microsoft Sentinel is setup" + } + }, + "workbook1-name": { + "type": "string", + "defaultValue": "Tailscale Operations (Standard)", + "minLength": 1, + "metadata": { + "description": "Name for the workbook" + } + }, + "workbook2-name": { + "type": "string", + "defaultValue": "Tailscale Operations (Premium)", + "minLength": 1, + "metadata": { + "description": "Name for the workbook" + } + } +} diff --git a/Solutions/Tailscale (CCF)/Parsers/ASimNetworkSessionTailscale.yaml b/Solutions/Tailscale (CCF)/Parsers/ASimNetworkSessionTailscale.yaml new file mode 100644 index 00000000000..4b8af4fb11a --- /dev/null +++ b/Solutions/Tailscale (CCF)/Parsers/ASimNetworkSessionTailscale.yaml @@ -0,0 +1,96 @@ +id: 2b9c1f5a-7d3e-4f8b-a456-c1d2e3f4a5b6 +Function: + Title: ASIM Network Session parser for Tailscale (no-prefix wrapper) + Version: '1.0.0' + LastUpdated: '2026-05-19' +Category: Microsoft Sentinel Parser +FunctionName: ASimNetworkSessionTailscale +FunctionAlias: ASimNetworkSessionTailscale +FunctionQuery: | + let NetworkProtocolLookup = datatable(proto_lookup: int, NetworkProtocol_lookup: string) + [ + 1, "ICMP", + 6, "TCP", + 17, "UDP", + 58, "ICMPv6" + ]; + let baseEvents = Tailscale_Network_CL; + let virtualFlows = baseEvents + | where HasVirtualTraffic + | mv-expand t = VirtualTraffic + | extend NetworkSessionType = "Virtual", NetworkDirection = "Local"; + let subnetFlows = baseEvents + | where HasSubnetTraffic + | mv-expand t = SubnetTraffic + | extend NetworkSessionType = "Subnet", NetworkDirection = "Outbound"; + let exitFlows = baseEvents + | where HasExitTraffic + | mv-expand t = ExitTraffic + | extend NetworkSessionType = "Exit", NetworkDirection = "Outbound"; + union virtualFlows, subnetFlows, exitFlows + | extend + SrcIpAddrAndPort = tostring(t.src), + DstIpAddrAndPort = tostring(t.dst), + proto_lookup = toint(t.proto), + SrcBytes = tolong(t.txBytes), + DstBytes = tolong(t.rxBytes), + SrcPackets = tolong(t.txPkts), + DstPackets = tolong(t.rxPkts) + | extend + SrcRawHost = extract(@"^(.+):([0-9]+)$", 1, SrcIpAddrAndPort), + SrcPortNumber = toint(extract(@"^(.+):([0-9]+)$", 2, SrcIpAddrAndPort)), + DstRawHost = extract(@"^(.+):([0-9]+)$", 1, DstIpAddrAndPort), + DstPortNumber = toint(extract(@"^(.+):([0-9]+)$", 2, DstIpAddrAndPort)) + | extend + SrcIpAddr = case( + isempty(SrcRawHost), SrcIpAddrAndPort, + SrcRawHost startswith "[", substring(SrcRawHost, 1, strlen(SrcRawHost) - 2), + SrcRawHost + ), + DstIpAddr = case( + isempty(DstRawHost), DstIpAddrAndPort, + DstRawHost startswith "[", substring(DstRawHost, 1, strlen(DstRawHost) - 2), + DstRawHost + ) + | lookup NetworkProtocolLookup on proto_lookup + | extend + NetworkProtocol = coalesce(NetworkProtocol_lookup, tostring(proto_lookup)), + NetworkBytes = SrcBytes + DstBytes, + NetworkPackets = SrcPackets + DstPackets + | extend + EventStartTime = todatetime(FlowStart), + EventEndTime = todatetime(FlowEnd), + EventProduct = "Tailscale", + EventVendor = "Tailscale", + EventSchema = "NetworkSession", + EventSchemaVersion = "0.2.6", + EventType = "NetworkSession", + EventResult = "Success", + EventCount = int(1), + EventSeverity = "Informational", + DvcAction = "Allow" + | extend + SrcHostname = SrcNodeName, + SrcUsername = SrcUser, + SrcUsernameType = iff(isnotempty(SrcUser), "Simple", ""), + SrcDvcOs = SrcOs, + DstHostname = DstNodeName, + DstUsername = DstUser, + DstUsernameType = iff(isnotempty(DstUser), "Simple", ""), + DstDvcOs = DstOs, + Dvc = SrcNodeName, + Src = SrcIpAddr, + Dst = DstIpAddr, + IpAddr = SrcIpAddr, + User = SrcUser, + Hostname = SrcNodeName + | project-away + t, NetworkProtocol_lookup, proto_lookup, + VirtualTraffic, SubnetTraffic, ExitTraffic, PhysicalTraffic, + HasVirtualTraffic, HasSubnetTraffic, HasExitTraffic, HasPhysicalTraffic, + SrcAddresses, DstAddresses, SrcTags, DstTags, + SrcOs, DstOs, SrcUser, DstUser, SrcNodeName, DstNodeName, + DstCount, DstNodeId, NodeId, IsRelayed, + SrcIpAddrAndPort, DstIpAddrAndPort, SrcRawHost, DstRawHost, + FlowStart, FlowEnd, + TenantId, _ResourceId, Type diff --git a/Solutions/Tailscale (CCF)/Parsers/vimNetworkSessionTailscale.yaml b/Solutions/Tailscale (CCF)/Parsers/vimNetworkSessionTailscale.yaml new file mode 100644 index 00000000000..9f4faca048a --- /dev/null +++ b/Solutions/Tailscale (CCF)/Parsers/vimNetworkSessionTailscale.yaml @@ -0,0 +1,96 @@ +id: 0d8fe6c1-3a4f-4f5b-9c8d-1a2b3c4d5e6f +Function: + Title: ASIM Network Session parser for Tailscale + Version: '1.0.0' + LastUpdated: '2026-05-19' +Category: Microsoft Sentinel Parser +FunctionName: vimNetworkSessionTailscale +FunctionAlias: vimNetworkSessionTailscale +FunctionQuery: | + let NetworkProtocolLookup = datatable(proto_lookup: int, NetworkProtocol_lookup: string) + [ + 1, "ICMP", + 6, "TCP", + 17, "UDP", + 58, "ICMPv6" + ]; + let baseEvents = Tailscale_Network_CL; + let virtualFlows = baseEvents + | where HasVirtualTraffic + | mv-expand t = VirtualTraffic + | extend NetworkSessionType = "Virtual", NetworkDirection = "Local"; + let subnetFlows = baseEvents + | where HasSubnetTraffic + | mv-expand t = SubnetTraffic + | extend NetworkSessionType = "Subnet", NetworkDirection = "Outbound"; + let exitFlows = baseEvents + | where HasExitTraffic + | mv-expand t = ExitTraffic + | extend NetworkSessionType = "Exit", NetworkDirection = "Outbound"; + union virtualFlows, subnetFlows, exitFlows + | extend + SrcIpAddrAndPort = tostring(t.src), + DstIpAddrAndPort = tostring(t.dst), + proto_lookup = toint(t.proto), + SrcBytes = tolong(t.txBytes), + DstBytes = tolong(t.rxBytes), + SrcPackets = tolong(t.txPkts), + DstPackets = tolong(t.rxPkts) + | extend + SrcRawHost = extract(@"^(.+):([0-9]+)$", 1, SrcIpAddrAndPort), + SrcPortNumber = toint(extract(@"^(.+):([0-9]+)$", 2, SrcIpAddrAndPort)), + DstRawHost = extract(@"^(.+):([0-9]+)$", 1, DstIpAddrAndPort), + DstPortNumber = toint(extract(@"^(.+):([0-9]+)$", 2, DstIpAddrAndPort)) + | extend + SrcIpAddr = case( + isempty(SrcRawHost), SrcIpAddrAndPort, + SrcRawHost startswith "[", substring(SrcRawHost, 1, strlen(SrcRawHost) - 2), + SrcRawHost + ), + DstIpAddr = case( + isempty(DstRawHost), DstIpAddrAndPort, + DstRawHost startswith "[", substring(DstRawHost, 1, strlen(DstRawHost) - 2), + DstRawHost + ) + | lookup NetworkProtocolLookup on proto_lookup + | extend + NetworkProtocol = coalesce(NetworkProtocol_lookup, tostring(proto_lookup)), + NetworkBytes = SrcBytes + DstBytes, + NetworkPackets = SrcPackets + DstPackets + | extend + EventStartTime = todatetime(FlowStart), + EventEndTime = todatetime(FlowEnd), + EventProduct = "Tailscale", + EventVendor = "Tailscale", + EventSchema = "NetworkSession", + EventSchemaVersion = "0.2.6", + EventType = "NetworkSession", + EventResult = "Success", + EventCount = int(1), + EventSeverity = "Informational", + DvcAction = "Allow" + | extend + SrcHostname = SrcNodeName, + SrcUsername = SrcUser, + SrcUsernameType = iff(isnotempty(SrcUser), "Simple", ""), + SrcDvcOs = SrcOs, + DstHostname = DstNodeName, + DstUsername = DstUser, + DstUsernameType = iff(isnotempty(DstUser), "Simple", ""), + DstDvcOs = DstOs, + Dvc = SrcNodeName, + Src = SrcIpAddr, + Dst = DstIpAddr, + IpAddr = SrcIpAddr, + User = SrcUser, + Hostname = SrcNodeName + | project-away + t, NetworkProtocol_lookup, proto_lookup, + VirtualTraffic, SubnetTraffic, ExitTraffic, PhysicalTraffic, + HasVirtualTraffic, HasSubnetTraffic, HasExitTraffic, HasPhysicalTraffic, + SrcAddresses, DstAddresses, SrcTags, DstTags, + SrcOs, DstOs, SrcUser, DstUser, SrcNodeName, DstNodeName, + DstCount, DstNodeId, NodeId, IsRelayed, + SrcIpAddrAndPort, DstIpAddrAndPort, SrcRawHost, DstRawHost, + FlowStart, FlowEnd, + TenantId, _ResourceId, Type diff --git a/Solutions/Tailscale (CCF)/README.md b/Solutions/Tailscale (CCF)/README.md new file mode 100644 index 00000000000..38512f55518 --- /dev/null +++ b/Solutions/Tailscale (CCF)/README.md @@ -0,0 +1,368 @@ +# Tailscale (CCF) + +Microsoft Sentinel solution that ingests Tailscale identity, device, configuration, audit and (Premium) network-flow telemetry via the OAuth2-secured Tailscale API. Built on the Codeless Connector Framework (CCF) - no Function App or container required. + +- **2 data connectors** (Standard, Premium) - install whichever matches your Tailscale plan +- **24 analytic rules** (16 Standard + 8 Premium-only) +- **22 hunting queries** (12 Standard + 10 Premium-only) +- **2 workbooks** (Standard Operations, Premium Operations) +- **2 ASIM NetworkSession parsers** (`vimNetworkSessionTailscale` + `ASimNetworkSessionTailscale` wrapper) - Premium only +- **9 custom tables** ingested via 9-11 polling rules behind a single Connect button + +--- + +## Table of contents + +1. [Pick your tier](#1-pick-your-tier) +2. [Pre-requisites](#2-pre-requisites) +3. [Installation](#3-installation) +4. [Verification](#4-verification) +5. [Custom tables](#5-custom-tables) +6. [Analytic rules](#6-analytic-rules) +7. [Hunting queries](#7-hunting-queries) +8. [Workbooks](#8-workbooks) +9. [Architecture notes](#9-architecture-notes) +10. [Limitations](#10-limitations) +11. [Troubleshooting](#11-troubleshooting) +12. [Support](#12-support) +13. [Acknowledgements](#13-acknowledgements) + +--- + +## 1. Pick your tier + +Install **one** of the two connectors based on your Tailscale plan. The split mirrors what the Tailscale API actually exposes per tier - network flow logs are only available on Premium and Enterprise tailnets. + +| | Tailscale Standard (CCF) | Tailscale Premium (CCF) | +|---|---|---| +| **Tailscale plan** | Personal (Free), Starter, Premium\* | Premium, Enterprise | +| **Pollers behind one Connect** | 9 | 11 | +| **Custom tables created** | 7 | 9 | +| **Analytic rules wired** | 16 | 24 (Standard 16 + Premium 8) | +| **Hunting queries wired** | 12 | 22 (Standard 12 + Premium 10) | +| **Workbook** | Standard Operations | Premium Operations | +| **Network flow logs** | not exposed by API | `Tailscale_Network_CL` | +| **Posture integrations** | not exposed by API | `Tailscale_PostureIntegrations_CL` | +| **Required OAuth scopes** | `logs:configuration:read`, `devices:read`, `users:read`, `keys:read`, `webhooks:read`, `dns:read`, `settings:read` | All of Standard plus `logs:network:read`, `posture-integrations:read` | + +\* Premium tailnets can use the Standard connector if you don't want network-flow data, but the Premium connector is the recommended path. + +--- + +## 2. Pre-requisites + +You need four things before clicking Connect: + +1. **A Microsoft Sentinel-enabled Log Analytics workspace** in any region. +2. **A Data Collection Endpoint (DCE)** in the same region as the workspace. The Sentinel Content Hub installer creates one automatically if you don't already have a shared DCE. +3. **A Tailscale OAuth client** generated at . Personal API tokens (`tskey-api-...`) do **not** work - see [Architecture notes](#9-architecture-notes) for the reason. + - Tick the scopes listed in the table above for your tier. + - Copy the **Client ID** and **Client Secret** when prompted - the secret is shown only once. +4. **Your tailnet name** (e.g. `tailb094d7.ts.net`). Find it on the [Keys page](https://login.tailscale.com/admin/settings/keys) or in your Tailscale admin URL. + +### OAuth scope checklist + +Tick exactly the scopes for your tier - extra scopes don't hurt but the connector won't ask for them, and missing a scope means the corresponding poller will return `200 OK` with no data (Tailscale doesn't error on missing scope, it just returns empty). + +**Standard (7 scopes)** + +- `logs:configuration:read` - audit log +- `devices:read` - device inventory, including tailnet-lock state, SSH enablement, advertised routes +- `users:read` - user roles and last-seen +- `keys:read` - auth keys and OAuth clients (for sprawl/expiry detection) +- `webhooks:read` - webhook configuration +- `dns:read` - nameservers, MagicDNS, split-DNS, search paths +- `settings:read` - tailnet-wide settings + +**Premium adds (2 scopes)** + +- `logs:network:read` - network flow logs +- `posture-integrations:read` - device posture integration list and status + +--- + +## 3. Installation + +1. Open **Microsoft Sentinel** -> **Content hub**, search for "Tailscale" and install **Tailscale (CCF)**. +2. Go to **Data connectors**, search "Tailscale", and open either **Tailscale Standard (CCF)** or **Tailscale Premium (CCF)**. +3. Supply: + - **Tailnet name** (e.g. `tailb094d7.ts.net`) + - **OAuth Client ID** + - **OAuth Client Secret** +4. Click **Connect**. The connector page shows "Connected" within ~30 seconds; the first audit poll completes within 5 minutes and the first snapshot pollers (devices, users, ...) within 60 minutes. + +That single Connect click deploys 9 (Standard) or 11 (Premium) Sentinel `RestApiPoller` data connectors behind the scenes - see [Architecture notes](#9-architecture-notes) for how that works. + +--- + +## 4. Verification + +Run these in **Sentinel** -> **Logs** after the first poll cycle completes (~5 min for audit, ~60 min for snapshots). + +```kql +// Audit logs received in the last 15 min (should be > 0 if any config activity happened) +Tailscale_Audit_CL +| where TimeGenerated > ago(15m) +| project TimeGenerated, EventTime, Action, Actor, Target + +// Snapshot of every tailnet device on the latest poll +Tailscale_Devices_CL +| summarize arg_max(TimeGenerated, *) by DeviceId +| project DeviceName, Hostname, User, Os, ClientVersion, LastSeen, Authorized, ConnectedToControl + +// All tables receiving data in the last 2 hours +union + (Tailscale_Audit_CL | extend _T = "Tailscale_Audit_CL"), + (Tailscale_Devices_CL | extend _T = "Tailscale_Devices_CL"), + (Tailscale_Users_CL | extend _T = "Tailscale_Users_CL"), + (Tailscale_Keys_CL | extend _T = "Tailscale_Keys_CL"), + (Tailscale_Webhooks_CL | extend _T = "Tailscale_Webhooks_CL"), + (Tailscale_Settings_CL | extend _T = "Tailscale_Settings_CL"), + (Tailscale_Dns_CL | extend _T = "Tailscale_Dns_CL"), + (Tailscale_Network_CL | extend _T = "Tailscale_Network_CL"), + (Tailscale_PostureIntegrations_CL | extend _T = "Tailscale_PostureIntegrations_CL") +| where TimeGenerated > ago(2h) +| summarize Rows = count(), Latest = max(TimeGenerated) by _T +| order by _T asc +``` + +A working Standard tier should return rows for 7 tables; Premium should return rows for 9. `Tailscale_Network_CL` and `Tailscale_PostureIntegrations_CL` are Premium-only. + +--- + +## 5. Custom tables + +All tables are Log Analytics custom tables (`_CL`) populated via Sentinel CCF poller -> DCE -> DCR transform. + +| Table | Cols | Cadence | Source endpoint | Tier | +|---|---|---|---|---| +| `Tailscale_Audit_CL` | 11 | 5 min | `/logging/configuration` | Standard + Premium | +| `Tailscale_Devices_CL` | 27 | 60 min | `/devices?fields=all` | Standard + Premium | +| `Tailscale_Users_CL` | 13 | 60 min | `/users` | Standard + Premium | +| `Tailscale_Keys_CL` | 10 | 60 min | `/keys?all=true` | Standard + Premium | +| `Tailscale_Webhooks_CL` | 8 | 60 min | `/webhooks` | Standard + Premium | +| `Tailscale_Settings_CL` | 9 | 60 min | `/settings` | Standard + Premium | +| `Tailscale_Dns_CL` | 5 | 60 min | merged from `/dns/nameservers`, `/dns/preferences`, `/dns/searchpaths` | Standard + Premium | +| `Tailscale_Network_CL` | 27 | 5 min | `/logging/network` | Premium only | +| `Tailscale_PostureIntegrations_CL` | 8 | 60 min | `/posture/integrations` | Premium only | + +**Snapshot semantics.** All `_CL` tables except `Audit` and `Network` are snapshot tables - each poll writes the full current state of the endpoint. Use `summarize arg_max(TimeGenerated, *) by ` to get the latest snapshot. The 5-min audit and network tables are append-only event streams. + +**`Tailscale_Devices_CL` is the richest snapshot table** at 27 columns. The 5 most interesting columns for detection are surfaced via `?fields=all`: + +- `AdvertisedRoutes` / `EnabledRoutes` - dynamic arrays of CIDRs the device offers/has approved +- `SshEnabled` - bool, is Tailscale SSH active on this device +- `ConnectedToControl` / `Authorized` - control-plane state pair (unauthorized + connected = the rule trigger) +- `TailnetLockKey` / `TailnetLockError` - cryptographic node-key validation state +- `UpdateAvailable` - bool, client behind latest release + +**`Tailscale_Network_CL` is the richest event table** (Premium only) at 27 columns. The DCR transform promotes the most useful inner-object fields out of the source `srcNode` / `dstNodes` dynamics so hunting queries don't have to traverse dynamic JSON: + +- `SrcUser` / `SrcNodeName` / `SrcOs` / `SrcTags` / `SrcAddresses` - src device identity + tagging +- `DstCount` / `DstNodeId` / `DstNodeName` / `DstUser` / `DstOs` / `DstTags` / `DstAddresses` - dst device identity + tagging +- `HasVirtualTraffic` / `HasSubnetTraffic` / `HasExitTraffic` / `HasPhysicalTraffic` - traffic-shape flags +- `IsRelayed` - bool, true when the flow used a DERP relay (detected via `127.3.3.40` in `physicalTraffic`) + +--- + +## 6. Analytic rules + +### Standard tier (16 rules) + +**Identity & access (5)** + +| Rule | Severity | Tactics | What it watches | +|---|---|---|---| +| New API access token or OAuth client created | Medium | Persistence, CredentialAccess | New `API_ACCESS_TOKEN_CREATE` / `OAUTH_CLIENT_CREATE` audit events | +| OAuth client or API key created with write scopes | High | Persistence, PrivilegeEscalation | New OAuth client or API key whose granted scopes include any `:write` permission | +| Auth key created | Low | Persistence | Any new auth key (incl. ephemeral / reusable / preauthorized) | +| User role elevated to admin or owner | High | PrivilegeEscalation, Persistence | `USER_ROLE_UPDATE` audit events targeting `admin` or `owner` | +| Unauthorized device connected to control plane | High | InitialAccess, Persistence | `Authorized=false AND ConnectedToControl=true` in devices snapshot | + +**Configuration (3)** + +| Rule | Severity | Tactics | What it watches | +|---|---|---|---| +| Policy file (ACL) modified | Medium | DefenseEvasion, Persistence | `ACL_FILE_UPDATE` events | +| Mass credential revocation in short window | High | DefenseEvasion, Impact | More than N delete-key events in a 30-min sliding window | +| External (shared-in) device added | Medium | InitialAccess | New `IsExternal=true` device vs 24-hour baseline | + +**Devices (3)** + +| Rule | Severity | Tactics | What it watches | +|---|---|---|---| +| Device started advertising subnet routes | Medium | LateralMovement, Persistence | Non-exit-node CIDRs newly appear in `AdvertisedRoutes` | +| Device key expiring within 7 days | Medium | InitialAccess | Devices whose key expiry is within 7 days | +| Device Tailscale SSH newly enabled | Medium | Persistence, LateralMovement | `SshEnabled` transition from false to true vs 24-hour baseline | + +**Network & exit (2)** + +| Rule | Severity | Tactics | What it watches | +|---|---|---|---| +| Exit node advertised or approved | Low | CommandAndControl, Exfiltration | `0.0.0.0/0` or `::/0` newly appears in `AdvertisedRoutes` / `EnabledRoutes` | +| Tailnet lock validation failed | High | DefenseEvasion, InitialAccess | Non-empty `TailnetLockError` field in devices snapshot | + +**DNS (3)** + +| Rule | Severity | Tactics | What it watches | +|---|---|---|---| +| DNS nameservers modified | High | DefenseEvasion, CommandAndControl | `DNS_UPDATE` audit events affecting global nameservers | +| MagicDNS disabled | Medium | DefenseEvasion | `MAGICDNS_DISABLE` audit event | +| Split-DNS configuration modified | High | DefenseEvasion, CommandAndControl | `SPLIT_DNS_UPDATE` audit events (per-domain DNS override) | + +### Premium tier (additional 8 rules) + +These require `Tailscale_Network_CL` (flow logs) or `Tailscale_PostureIntegrations_CL`. + +| Rule | Severity | Tactics | What it watches | +|---|---|---|---| +| Network flow beaconing detected | Medium | CommandAndControl, Exfiltration | Regular periodic flows from a single source over 24h (jitter-tolerant) | +| DERP relay traffic surge | Low | CommandAndControl | More than 75% of a source node's recent flows fell back to DERP relay (`IsRelayed=true`); signals NAT/firewall failure, UDP-blocking middlebox, or attempted evasion | +| Large outbound transfer over tailnet | Medium | Exfiltration, Collection | Single flow tx-bytes > 1GB in any 5-min window | +| Mass fan-out from single node | High | Discovery, LateralMovement | Source node initiated flows to N+ distinct destinations within 5 min | +| Subnet router throughput anomaly | Low | Exfiltration, CommandAndControl | Subnet-router src->dst throughput exceeds 3-sigma of its 7-day baseline | +| Unexpected exit-node egress | Medium | CommandAndControl, Exfiltration | Egress through a node that wasn't approved as exit node in the last hour | +| New posture integration added | Medium | Persistence | New entry in `Tailscale_PostureIntegrations_CL` snapshot vs prior | +| Posture integration disabled or removed | High | DefenseEvasion, Persistence | Posture integration disappeared or status changed to disabled | + +--- + +## 7. Hunting queries + +### Standard (12) + +| Query | Tactics | Use case | +|---|---|---| +| First-seen actor making configuration changes | InitialAccess, Persistence | New principal performing privileged audit actions | +| ACL policy churn | DefenseEvasion, PrivilegeEscalation | How often is the policy file edited - high churn = governance risk | +| Off-hours configuration changes | InitialAccess, Persistence | Privileged audit actions outside business hours | +| Auth key sprawl | Persistence, CredentialAccess | Users with many active reusable auth keys | +| Auth keys with no expiry | Persistence, CredentialAccess | Long-lived auth keys (compliance / hygiene) | +| Devices not seen in 30+ days | Discovery | Stale devices - candidates for offboarding | +| Devices with outdated client version | DefenseEvasion | Client behind latest release | +| Users with zero devices | InitialAccess | Orphaned user accounts | +| Split-DNS per-domain change history | DefenseEvasion, CommandAndControl | Audit-log slice of per-domain DNS routing changes | +| Devices with Tailscale SSH enabled | LateralMovement, Persistence | Cross-reference with the SSH ACL block | +| External (shared-in) device inventory | InitialAccess | Devices admitted via Tailscale sharing | +| Subnet router CIDR exposure inventory | LateralMovement | Every CIDR currently bridged into the tailnet | + +### Premium (10) + +| Query | Tactics | Use case | +|---|---|---| +| Beaconing candidates (regular periodic flows) | CommandAndControl, Exfiltration | Looser threshold than the analytic rule - investigation aid | +| Cross-tag flow matrix | LateralMovement, Discovery | Flows pivoted by src-tag x dst-tag over 7 days; surfaces tag-to-same-tag loops as worm/service-mesh signal | +| Devices with persistent DERP relay usage | CommandAndControl | Devices that consistently fall back to DERP relay over 24h; long-window companion to the surge rule | +| Exit-node usage patterns | CommandAndControl, Exfiltration | Who uses which exit node, how often, how much data | +| New src->dst node pairs (lateral movement candidates) | LateralMovement, Discovery | First-time observed flow pair vs 7-day baseline | +| Network flows outside business hours | Exfiltration, CommandAndControl | Off-hours flows with `TaggedSource` discriminator to separate service-account from human activity | +| Tagged services with broad inbound exposure | LateralMovement, InitialAccess | Tagged services ranked by inbound `DistinctSrcDevices` / `DistinctSrcUsers` / `DistinctSrcOs` - surfaces ACL drift | +| Top talkers by bytes (virtual traffic) | Exfiltration, Collection | Highest tx/rx nodes over time window | +| Users generating traffic from multiple devices | InitialAccess, Persistence | Multi-device users joined against `Tailscale_Devices_CL.Created` to flag newly-added devices | +| Current posture integration inventory | DefenseEvasion | Snapshot of every posture integration and its enabled state | + +--- + +## 8. Workbooks + +Both workbooks are wired automatically when you install the matching connector. Open Sentinel -> **Workbooks** -> search "Tailscale". + +**Tailscale Standard Operations** + +Tabs for Devices, Users, Keys, DNS, Audit and Health. Quick-look tiles for total devices, devices with updates available, devices with SSH enabled, subnet routers and exit nodes, dormant devices, tailnet-lock state. All KQL validated against live data. + +**Tailscale Premium Operations** + +Everything in Standard plus Network tab (top talkers, src->dst pairs, exit-node egress, beaconing candidates) and Posture tab (integration inventory, posture state per device). + +--- + +## 9. Architecture notes + +### Why two connectors + +Tailscale's `/logging/network` endpoint is gated to Premium and Enterprise tailnets. We could put a single connector behind a single Connect button and let pollers silently fail on Free/Standard, but that would generate noisy errors and confuse operators. Splitting the connector by tier means each card only registers pollers the user's API tier actually supports. + +### Why one Connect button per connector deploys N pollers + +Sentinel CCF normally maps one Connect card to one polling rule. To fan out to 9-11 pollers from a single click, the polling-rule contentTemplate uses the **Proofpoint TAP** pattern - a `guidValue` parameter (`defaultValue: '[newGuid()]'`) and an `innerWorkspace` parameter that defer evaluation to inner-deploy scope (Connect-click time), producing one shared GUID across every poller resource name. Without this exact shape, Sentinel silently deploys only the first poller - the most painful bug we hit in this project. + +### Why OAuth clients are required (not personal API tokens) + +Tailscale's `/logging/configuration`, `/logging/network`, posture, and DNS endpoints require scoped credentials. Personal API tokens (`tskey-api-...`) are unscoped - against scope-gated endpoints they return HTTP 200 with `"logs": null` (or empty arrays) rather than 401, so the misconfiguration is **silent**. OAuth client credentials carry explicit scopes and fail closed. + +### How three DNS endpoints become one table + +Tailscale exposes DNS configuration across three endpoints (`/dns/nameservers`, `/dns/preferences`, `/dns/searchpaths`) but the Sentinel UX is cleaner with one logical table. Both connectors land all three into a single `Tailscale_Dns_CL` table with a `ConfigType` discriminator column - the mechanism differs by connector because Azure DCRs are capped at 10 `dataFlows` and the Premium connector is already at the limit: + +- **Standard connector** (7 tables, 9 dataFlows): each DNS poller writes to its own input stream (`Custom-Tailscale_DnsNameservers_CL`, `Custom-Tailscale_DnsPreferences_CL`, `Custom-Tailscale_DnsSearchPaths_CL`) and three separate `dataFlows` apply per-source transforms before merging into the shared `Tailscale_Dns_CL` output. +- **Premium connector** (9 tables, 9 dataFlows): all three DNS pollers write to a single unified input stream (`Custom-Tailscale_DnsConfig_CL`) so the DCR uses just one `dataFlow` for DNS. Without this consolidation the Premium DCR would be at 11 dataFlows and Azure would reject the deployment. + +Net effect for the operator is identical: one table, filter by `ConfigType`. + +### Cadence rationale + +- **5 min** for `/logging/configuration` (Standard + Premium) and `/logging/network` (Premium only) - these are event streams with `start` / `end` query parameters +- **60 min** for snapshot endpoints (devices, users, keys, ...) - the data doesn't change frequently and a 1-hour granularity is enough for snapshot-based rules; reducing this would mostly burn ingestion cost without improving detection + +--- + +## 10. Limitations + +- **Network flow logs are Premium-only.** The eight Premium rules and ten Premium hunts depend on `Tailscale_Network_CL` and won't run on a Standard install. +- **Microsoft pre-built "Network Session Essentials" detections require a clone-and-rewire.** This solution ships a `vimNetworkSessionTailscale` ASIM NetworkSession parser (and the param-less `ASimNetworkSessionTailscale` wrapper) that maps `Tailscale_Network_CL` to the ASIM NetworkSession schema. Microsoft's pre-built detections call the sealed workspace function `_Im_NetworkSession`, which can't be extended from a Solution - so to apply those detections to Tailscale data, clone the rule and replace `_Im_NetworkSession(...)` with `vimNetworkSessionTailscale(...)` (or `union vimNetworkSessionTailscale(...), _Im_NetworkSession(...)` if you want both sources in one query). +- **Snapshot tables overwrite, they don't diff.** Each 60-min snapshot is a complete current-state poll, not a delta. Rules that need transition detection (e.g. "SSH newly enabled") compare the latest snapshot against a 24-hour baseline. + +--- + +## 11. Troubleshooting + +### "Connected" but no rows in any `_CL` table after 30 min + +```kql +// Were any pollers dispatched? (DCR ingestion is logged by the workspace) +union AzureDiagnostics +| where Category == "DataCollectionRuleLogs" +| where _ResourceId contains "dcr-tailscale" +| where TimeGenerated > ago(1h) +| project TimeGenerated, Status_s, ResultDescription_s +``` + +If you see no entries, the connector hasn't been dispatched yet - wait the 5-minute or 60-minute cadence. If you see entries with `Status_s != "Succeeded"`, paste `ResultDescription_s` into a support thread. + +### "Connected" but only `Tailscale_Audit_CL` has rows + +The remaining endpoints poll on 60-min cadence - the first non-audit snapshot won't land until 60 minutes after Connect. Wait an hour. + +### Audit has rows but specific endpoints (devices, dns, ...) don't + +Almost always a missing OAuth scope. Tailscale returns HTTP 200 with empty data when a scope is missing, so the poller doesn't error. Re-check the OAuth client at against the [scope checklist](#oauth-scope-checklist) above. + +### `Tailscale_Devices_CL` is missing `AdvertisedRoutes`, `SshEnabled`, etc. + +The connector polls `/devices?fields=all`. If those columns are empty in your workspace, the most likely cause is that the DCR transform isn't projecting them - confirm the deployed `dcr-tailscale*` DCR `transformKql` matches the version in this solution and re-Connect. + +### OAuth deployment fails with "Invalid Token Endpoint query parameters" + +You hit the Sentinel CCF reserved-key bug - the connector definition is including OAuth reserved keys (`client_id`, `client_secret`, `grant_type`, `scope`) inside `TokenEndpointQueryParameters`. The shipped connector definition omits these correctly; if you're seeing this error after edits, remove those keys from the connector definition JSON and redeploy. + +### Premium rules silently never fire + +Confirm you installed the **Premium** connector (not Standard) and that `Tailscale_Network_CL` and `Tailscale_PostureIntegrations_CL` are receiving rows (see the verification query in [section 4](#4-verification)). Premium rules query those two tables exclusively. + +--- + +## 12. Support + +This is a Community-tier solution. Bugs, feature requests, and PRs: + +- **GitHub Issues**: (tag the title with `[Tailscale (CCF)]`) +- **Maintainer**: noodlemctwoodle + +No SLA - the maintainer responds when convenient. PRs that include tests + a reproducer trip the response time considerably. + +--- + +## 13. Acknowledgements + +Thanks to [Tailscale](https://tailscale.com/) for the support that made the Premium-tier features of this solution (network flow log ingestion, posture integration inventory, the seven Premium analytic rules, the five Premium hunting queries, and the Premium Operations workbook) buildable and verifiable against live data. diff --git a/Solutions/Tailscale (CCF)/ReleaseNotes.md b/Solutions/Tailscale (CCF)/ReleaseNotes.md new file mode 100644 index 00000000000..b62de57a294 --- /dev/null +++ b/Solutions/Tailscale (CCF)/ReleaseNotes.md @@ -0,0 +1,3 @@ +| **Version** | **Date Modified (DD-MM-YYYY)** | **Change History** | +|---|---|---| +| 3.0.0 | 19-05-2026 | Initial Solution Release | diff --git a/Solutions/Tailscale (CCF)/SolutionMetadata.json b/Solutions/Tailscale (CCF)/SolutionMetadata.json new file mode 100644 index 00000000000..d619d1ac7cf --- /dev/null +++ b/Solutions/Tailscale (CCF)/SolutionMetadata.json @@ -0,0 +1,22 @@ +{ + "publisherId": "noodlemctwoodle", + "offerId": "azure-sentinel-solution-tailscale-ccf", + "firstPublishDate": "2026-05-19", + "lastPublishDate": "2026-05-19", + "providers": [ + "Community" + ], + "categories": { + "domains": [ + "Networking", + "Security - Network", + "Identity" + ] + }, + "support": { + "name": "Tailscale (CCF)", + "tier": "Community", + "email": "ccfconnectors.county118@passmail.com", + "link": "https://github.com/Azure/Azure-Sentinel/issues" + } +} diff --git a/Solutions/Tailscale (CCF)/Workbooks/TailscalePremiumOperations.json b/Solutions/Tailscale (CCF)/Workbooks/TailscalePremiumOperations.json new file mode 100644 index 00000000000..7a3e59eadcc --- /dev/null +++ b/Solutions/Tailscale (CCF)/Workbooks/TailscalePremiumOperations.json @@ -0,0 +1,2259 @@ +{ + "version": "Notebook/1.0", + "items": [ + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "7b05a598-5120-43f4-bf5d-576c2a7ff28d", + "version": "KqlParameterItem/1.0", + "name": "TimeRange", + "type": 4, + "isRequired": true, + "value": { + "durationMs": 86400000 + }, + "typeSettings": { + "selectableValues": [ + { + "durationMs": 3600000 + }, + { + "durationMs": 14400000 + }, + { + "durationMs": 43200000 + }, + { + "durationMs": 86400000 + }, + { + "durationMs": 172800000 + }, + { + "durationMs": 604800000 + }, + { + "durationMs": 2592000000 + } + ] + } + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "parameters" + }, + { + "type": 1, + "content": { + "json": "
Tailscale Operations (Premium)
Single-pane visibility into your Tailscale tailnet on Personal (Free), Starter and Premium tiers: who, what, when, where, and what changed. Scope every panel with the time range below; the Investigate tab adds Actor and Device pickers for drilldown. Premium-tier panels (network flow logs, posture integrations) live in the separate Tailscale Operations (Premium) workbook.
" + }, + "name": "header" + }, + { + "type": 11, + "content": { + "version": "LinkItem/1.0", + "style": "tabs", + "links": [ + { + "id": "9794e9fd-916b-494d-8e72-af63d2f4c6c7", + "cellValue": "selectedTab", + "linkTarget": "parameter", + "linkLabel": "Overview", + "subTarget": "overview", + "style": "link" + }, + { + "id": "71cf49db-33c5-4d4b-920a-2ec0c6a258dc", + "cellValue": "selectedTab", + "linkTarget": "parameter", + "linkLabel": "Investigate", + "subTarget": "investigate", + "style": "link" + }, + { + "id": "5c56bb37-2053-47a0-b6fd-9539768c144d", + "cellValue": "selectedTab", + "linkTarget": "parameter", + "linkLabel": "Hunts", + "subTarget": "hunts", + "style": "link" + }, + { + "id": "d2004ded-07f8-446a-a720-f0a63d1d9dda", + "cellValue": "selectedTab", + "linkTarget": "parameter", + "linkLabel": "Identity", + "subTarget": "identity", + "style": "link" + }, + { + "id": "f23b3e14-1511-4a29-bf5e-bd65e55dbb40", + "cellValue": "selectedTab", + "linkTarget": "parameter", + "linkLabel": "Devices", + "subTarget": "devices", + "style": "link" + }, + { + "id": "724f8352-e21b-45a6-9029-39dc92693c05", + "cellValue": "selectedTab", + "linkTarget": "parameter", + "linkLabel": "Credentials", + "subTarget": "credentials", + "style": "link" + }, + { + "id": "8400ec64-e118-44e0-ae29-84afe94b8e0e", + "cellValue": "selectedTab", + "linkTarget": "parameter", + "linkLabel": "Admin Audit", + "subTarget": "audit", + "style": "link" + }, + { + "id": "b7e04861-edfa-4426-b6be-f1481ca569b6", + "cellValue": "selectedTab", + "linkTarget": "parameter", + "linkLabel": "Network & DNS", + "subTarget": "network", + "style": "link" + }, + { + "id": "b8506d3d-e615-4775-8c6f-14d9cc7db943", + "cellValue": "selectedTab", + "linkTarget": "parameter", + "linkLabel": "Network Flows", + "subTarget": "network-flows", + "style": "link" + }, + { + "id": "c98574ec-7a60-4c1a-b4c6-4d24c5bd02ce", + "cellValue": "selectedTab", + "linkTarget": "parameter", + "linkLabel": "Posture", + "subTarget": "posture", + "style": "link" + }, + { + "id": "cfbc96e4-8585-4d89-8f65-8344c8cc6eb2", + "cellValue": "selectedTab", + "linkTarget": "parameter", + "linkLabel": "Pipeline Health", + "subTarget": "pipeline", + "style": "link" + } + ] + }, + "name": "tabs" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "
Tailnet at a glance
" + }, + "name": "div-tailnet-at-a-glance-04e62b" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let DEV = Tailscale_Devices_CL | summarize arg_max(TimeGenerated, *) by DeviceId;\nlet USR = Tailscale_Users_CL | summarize arg_max(TimeGenerated, *) by UserId;\nlet KEY = Tailscale_Keys_CL | summarize arg_max(TimeGenerated, *) by KeyId;\nunion\n (DEV | summarize V=toreal(count()) | extend Metric=\"Devices\", Order=1),\n (DEV | where Authorized == true | summarize V=toreal(count()) | extend Metric=\"Authorized\", Order=2),\n (DEV | where UpdateAvailable == true | summarize V=toreal(count()) | extend Metric=\"Updates Available\", Order=3),\n (DEV | where SshEnabled == true | summarize V=toreal(count()) | extend Metric=\"SSH-Enabled\", Order=4),\n (USR | summarize V=toreal(count()) | extend Metric=\"Users\", Order=5),\n (USR | where Role =~ \"admin\" or Role =~ \"owner\" or Role =~ \"network-admin\" | summarize V=toreal(count()) | extend Metric=\"Admins\", Order=6),\n (KEY | where isnull(Revoked) and (isnull(Expires) or Expires > now()) | summarize V=toreal(count()) | extend Metric=\"Active Keys\", Order=7),\n (Tailscale_Audit_CL | where TimeGenerated {TimeRange} | summarize V=toreal(count()) | extend Metric=\"Audit Events ({TimeRange:label})\", Order=8)\n,\n (Tailscale_Network_CL | where TimeGenerated {TimeRange} | summarize V=toreal(count()) | extend Metric=\"Flows ({TimeRange:label})\", Order=9),\n (Tailscale_Network_CL | where TimeGenerated {TimeRange} | summarize V=toreal(dcount(SrcNodeName)) | extend Metric=\"Active Talkers\", Order=10),\n (Tailscale_Network_CL | where TimeGenerated {TimeRange} | summarize V=toreal(iff(count()==0, 0.0, 100.0 * countif(IsRelayed) / count())) | extend Metric=\"DERP Relayed %\", Order=11),\n (Tailscale_PostureIntegrations_CL | where TimeGenerated {TimeRange} | summarize arg_max(TimeGenerated, *) by IntegrationId | summarize V=toreal(count()) | extend Metric=\"Posture Integrations\", Order=12)\n| order by Order asc | project Metric, Value=V", + "size": 3, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "tiles", + "tileSettings": { + "titleContent": { + "columnMatch": "Metric", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "Value", + "formatter": 12, + "formatOptions": { + "palette": "auto" + } + }, + "showBorder": false + } + }, + "name": "q-4e9d7cff" + }, + { + "type": 1, + "content": { + "json": "
Audit activity over time
" + }, + "name": "div-audit-activity-over--546688" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| summarize EventCount = count() by bin(TimeGenerated, 1h), Action\n| order by TimeGenerated asc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "timechart", + "title": "Audit events by action", + "noDataMessage": "No audit events in the selected window. Widen the time range; remember the Tailscale audit poll runs every ~30 min.", + "noDataMessageStyle": 5 + }, + "name": "q-effcd498" + }, + { + "type": 1, + "content": { + "json": "
Who's doing what
" + }, + "name": "div-who's-doing-what-52144e" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend Actor=tostring(coalesce(Actor.loginName, Actor.displayName, Actor.type))\n| where isnotempty(Actor)\n| summarize Events=count(), DistinctActions=dcount(Action), LastSeen=max(TimeGenerated) by Actor\n| order by Events desc | take 15", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Top actors (by event count)", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Events", + "formatter": 8, + "formatOptions": { + "palette": "blue" + } + }, + { + "columnMatch": "LastSeen", + "formatter": 6 + } + ] + } + }, + "name": "q-34c77fa9", + "customWidth": "50" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend TargetType=tostring(Target.type)\n| where isnotempty(TargetType)\n| summarize Events=count() by TargetType\n| order by Events desc | take 15", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "piechart", + "title": "Activity by target type" + }, + "name": "q-4b3b709d", + "customWidth": "50" + }, + { + "type": 1, + "content": { + "json": "
Recent admin events
" + }, + "name": "div-recent-admin-events-87c9e0" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend Actor=tostring(coalesce(Actor.loginName, Actor.displayName, Actor.type))\n| extend TargetType=tostring(Target.type), TargetName=tostring(coalesce(Target.name, Target.id))\n| project TimeGenerated, Action, Actor, TargetType, TargetName, Origin\n| order by TimeGenerated desc | take 30", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Most recent 30 audit events", + "gridSettings": { + "formatters": [ + { + "columnMatch": "TimeGenerated", + "formatter": 6 + } + ] + } + }, + "name": "q-5e7d6306" + } + ] + }, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "overview" + }, + "name": "group-overview" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "b83a25c1-da18-49a2-a444-6517f13d891c", + "version": "KqlParameterItem/1.0", + "name": "SelectedActor", + "label": "Actor", + "type": 2, + "isRequired": false, + "query": "let opts = Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\n| where isnotempty(ActorLogin)\n| summarize Events=count() by ActorLogin\n| project value=ActorLogin, label=strcat(ActorLogin, \" (\", tostring(Events), \" events)\");\n(print value=\"__ALL__\", label=\"(All actors)\")\n| union opts", + "typeSettings": { + "showDefault": false + }, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "value": "__ALL__" + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "investigate-picker-actor" + }, + { + "type": 1, + "content": { + "json": "
Actor activity timeline
" + }, + "name": "div-actor-activity-timel-5ec305" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\n| where \"{SelectedActor}\" == \"__ALL__\" or ActorLogin == \"{SelectedActor}\"\n| summarize Events=count() by bin(TimeGenerated, 1h), Action\n| order by TimeGenerated asc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "timechart", + "title": "Actions over time -- actor: {SelectedActor:label}", + "noDataMessage": "Select an actor from the Actor dropdown above, or leave on 'All' to see total activity.", + "noDataMessageStyle": 5 + }, + "name": "q-32d40848" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\n| where \"{SelectedActor}\" == \"__ALL__\" or ActorLogin == \"{SelectedActor}\"\n| extend TargetType=tostring(Target.type), TargetName=tostring(coalesce(Target.name, Target.id))\n| project TimeGenerated, ActorLogin, Action, TargetType, TargetName, Origin\n| order by TimeGenerated desc | take 100", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Recent events for actor: {SelectedActor:label}", + "gridSettings": { + "formatters": [ + { + "columnMatch": "TimeGenerated", + "formatter": 6 + } + ] + }, + "noDataMessage": "No events for this actor in the selected window.", + "noDataMessageStyle": 5 + }, + "name": "q-a742f6fd" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "a9cf7907-f201-4725-a072-a8bd34bef74e", + "version": "KqlParameterItem/1.0", + "name": "SelectedDevice", + "label": "Device", + "type": 2, + "isRequired": false, + "query": "let opts = Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| order by LastSeen desc | take 100\n| project value=DeviceName, label=strcat(coalesce(DeviceName, Hostname), \" (\", User, \")\");\n(print value=\"__ALL__\", label=\"(All devices)\")\n| union opts", + "typeSettings": { + "showDefault": false + }, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "value": "__ALL__" + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "investigate-picker-device" + }, + { + "type": 1, + "content": { + "json": "
Selected device timeline
" + }, + "name": "div-selected-device-time-00bead" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| where \"{SelectedDevice}\" == \"__ALL__\" or DeviceName == \"{SelectedDevice}\"\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| extend OnlineNow = ClientConnectivity.endpoints != \"\" or ConnectedToControl == true\n| project DeviceName, Hostname, User, Os, ClientVersion, UpdateAvailable, Authorized, IsExternal, SshEnabled, LastSeen, Expires, KeyExpiryDisabled, OnlineNow, Addresses, Tags, AdvertisedRoutes", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Summary for device: {SelectedDevice:label}", + "noDataMessage": "Select a device from the Device dropdown above. Defaults to 'All'.", + "noDataMessageStyle": 5 + }, + "name": "q-11094dc8" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend TargetType=tostring(Target.type), TargetName=tostring(Target.name), TargetId=tostring(Target.id)\n| where (\"{SelectedDevice}\" == \"__ALL__\" and TargetType == \"NODE\") or TargetName == \"{SelectedDevice}\"\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\n| project TimeGenerated, Action, ActorLogin, TargetName, TargetId, Origin\n| order by TimeGenerated desc | take 100", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Audit events touching device: {SelectedDevice:label}", + "gridSettings": { + "formatters": [ + { + "columnMatch": "TimeGenerated", + "formatter": 6 + } + ] + }, + "noDataMessage": "No audit events recorded against the selected device in this window. Tailscale tags device events with Target.type=NODE; the audit feed only emits NODE events on create/update/delete, so quiet devices stay quiet here.", + "noDataMessageStyle": 5 + }, + "name": "q-ead106ce" + } + ] + }, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "investigate" + }, + "name": "group-investigate" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "
First-seen actors in the last 24h
" + }, + "name": "div-first-seen-actors-in-ce38d9" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let recent = Tailscale_Audit_CL | where TimeGenerated > ago(24h) | extend A=tostring(coalesce(Actor.loginName, Actor.displayName)) | summarize FirstSeen24h=min(TimeGenerated), Events=count() by A;\nlet historical = Tailscale_Audit_CL | where TimeGenerated between(ago(30d) .. ago(24h)) | extend A=tostring(coalesce(Actor.loginName, Actor.displayName)) | distinct A;\nrecent | join kind=leftanti historical on A | where isnotempty(A) | project FirstSeen24h, Actor=A, Events | order by Events desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Actors who have NEVER appeared before (30d baseline)", + "gridSettings": { + "formatters": [ + { + "columnMatch": "FirstSeen24h", + "formatter": 6 + }, + { + "columnMatch": "Events", + "formatter": 8, + "formatOptions": { + "palette": "orange" + } + } + ] + }, + "noDataMessage": "Every actor seen in the last 24h has appeared at least once in the prior 30d. Healthy state.", + "noDataMessageStyle": 1 + }, + "name": "q-9ba7b85e" + }, + { + "type": 1, + "content": { + "json": "
Off-hours configuration changes
" + }, + "name": "div-off-hours-configurat-3c3787" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend Hour=hourofday(TimeGenerated), DayOfWeek=dayofweek(TimeGenerated)/1d\n| where Hour < 7 or Hour > 19 or DayOfWeek in (0, 6)\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\n| extend TargetType=tostring(Target.type)\n| where Action !in (\"LOGIN\", \"LOGOUT\")\n| project TimeGenerated, ActorLogin, Action, TargetType, Origin\n| order by TimeGenerated desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Admin actions outside 07:00-19:00 weekdays", + "gridSettings": { + "formatters": [ + { + "columnMatch": "TimeGenerated", + "formatter": 6 + } + ] + }, + "noDataMessage": "No off-hours admin changes recorded - healthy state for an organisation working business hours.", + "noDataMessageStyle": 1 + }, + "name": "q-721ee490" + }, + { + "type": 1, + "content": { + "json": "
Devices with key expiry disabled
" + }, + "name": "div-devices-with-key-exp-dfe9c1" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where KeyExpiryDisabled == true\n| project DeviceName, Hostname, User, Os, ClientVersion, LastSeen, Authorized, Tags\n| order by LastSeen desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Devices that will never re-authenticate (high-risk drift)", + "gridSettings": { + "formatters": [ + { + "columnMatch": "LastSeen", + "formatter": 6 + } + ] + }, + "noDataMessage": "No devices have key expiry disabled - good. Disabling key expiry creates devices that never re-auth, drifting from policy.", + "noDataMessageStyle": 1 + }, + "name": "q-8a8e2fb5" + }, + { + "type": 1, + "content": { + "json": "
Auth keys with no expiry
" + }, + "name": "div-auth-keys-with-no-ex-b11207" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Keys_CL\n| summarize arg_max(TimeGenerated, *) by KeyId\n| where isnull(Revoked) and (isnull(Expires) or ExpirySeconds == 0)\n| project KeyId, Description, UserId, KeyType, Created, Capabilities\n| order by Created desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Active keys that never expire", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Created", + "formatter": 6 + } + ] + }, + "noDataMessage": "No never-expiring auth keys - rotation hygiene is good.", + "noDataMessageStyle": 1 + }, + "name": "q-1372740c" + }, + { + "type": 1, + "content": { + "json": "
Devices running outdated clients
" + }, + "name": "div-devices-running-outd-bcd077" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where UpdateAvailable == true\n| project DeviceName, Hostname, User, Os, ClientVersion, LastSeen, Tags\n| order by LastSeen desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Devices flagged update-available by Tailscale", + "gridSettings": { + "formatters": [ + { + "columnMatch": "LastSeen", + "formatter": 6 + } + ] + }, + "noDataMessage": "All devices on current client - nothing to patch.", + "noDataMessageStyle": 1 + }, + "name": "q-ecebf1f7" + }, + { + "type": 1, + "content": { + "json": "
Dormant devices (LastSeen > 30 days)
" + }, + "name": "div-dormant-devices-(las-761156" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where LastSeen < ago(30d)\n| extend DaysIdle = toint((now() - LastSeen) / 1d)\n| project DeviceName, Hostname, User, Os, ClientVersion, LastSeen, DaysIdle, Authorized, Tags\n| order by DaysIdle desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Devices idle 30+ days - candidates for retirement", + "gridSettings": { + "formatters": [ + { + "columnMatch": "LastSeen", + "formatter": 6 + }, + { + "columnMatch": "DaysIdle", + "formatter": 8, + "formatOptions": { + "palette": "redBright" + } + } + ] + }, + "noDataMessage": "No devices idle 30+ days - inventory is fresh.", + "noDataMessageStyle": 1 + }, + "name": "q-fb6c1fcc" + }, + { + "type": 1, + "content": { + "json": "
Subnet route exposure
" + }, + "name": "div-subnet-route-exposur-289c42" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where array_length(AdvertisedRoutes) > 0 or array_length(EnabledRoutes) > 0\n| extend Routes = tostring(EnabledRoutes), Advertised = tostring(AdvertisedRoutes)\n| project DeviceName, Hostname, User, Os, Advertised, Routes, LastSeen, SshEnabled, Authorized\n| order by LastSeen desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Devices advertising or running subnet routes / exit-node duty", + "gridSettings": { + "formatters": [ + { + "columnMatch": "LastSeen", + "formatter": 6 + } + ] + }, + "noDataMessage": "No devices advertising subnet routes. Pure mesh topology.", + "noDataMessageStyle": 1 + }, + "name": "q-9533b081" + }, + { + "type": 1, + "content": { + "json": "
Devices with SSH enabled
" + }, + "name": "div-devices-with-ssh-ena-285238" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where SshEnabled == true\n| project DeviceName, Hostname, User, Os, ClientVersion, LastSeen, Authorized, Tags\n| order by LastSeen desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Devices with Tailscale SSH enabled (Tailscale-managed remote-shell access)", + "gridSettings": { + "formatters": [ + { + "columnMatch": "LastSeen", + "formatter": 6 + } + ] + }, + "noDataMessage": "No devices have Tailscale SSH enabled - no SSH-via-Tailscale risk surface.", + "noDataMessageStyle": 1 + }, + "name": "q-735368fb" + } + ] + }, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "hunts" + }, + "name": "group-hunts" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "
User inventory snapshot
" + }, + "name": "div-user-inventory-snaps-9dc96c" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let U = Tailscale_Users_CL | summarize arg_max(TimeGenerated, *) by UserId;\nunion\n (U | summarize V=toreal(count()) | extend Metric=\"Total users\", Order=1),\n (U | where Role in~ (\"admin\",\"owner\",\"network-admin\",\"it-admin\",\"billing-admin\") | summarize V=toreal(count()) | extend Metric=\"Admin-tier users\", Order=2),\n (U | where Status =~ \"active\" | summarize V=toreal(count()) | extend Metric=\"Active\", Order=3),\n (U | where CurrentlyConnected == true | summarize V=toreal(count()) | extend Metric=\"Connected now\", Order=4),\n (U | where Status =~ \"idle\" or LastSeen < ago(30d) | summarize V=toreal(count()) | extend Metric=\"Idle / dormant\", Order=5),\n (U | where UserType =~ \"shared\" | summarize V=toreal(count()) | extend Metric=\"Shared (external)\", Order=6)\n| order by Order asc | project Metric, Value=V", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "tiles", + "tileSettings": { + "titleContent": { + "columnMatch": "Metric", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "Value", + "formatter": 12, + "formatOptions": { + "palette": "auto" + } + }, + "showBorder": false + } + }, + "name": "q-5689d9e8" + }, + { + "type": 1, + "content": { + "json": "
Distribution
" + }, + "name": "div-distribution-de67ec" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Users_CL\n| summarize arg_max(TimeGenerated, *) by UserId\n| summarize Count=count() by Role\n| order by Count desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "piechart", + "title": "Users by role" + }, + "name": "q-0c47912a", + "customWidth": "33" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Users_CL\n| summarize arg_max(TimeGenerated, *) by UserId\n| summarize Count=count() by Status\n| order by Count desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "piechart", + "title": "Users by status" + }, + "name": "q-e8c20a69", + "customWidth": "33" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Users_CL\n| summarize arg_max(TimeGenerated, *) by UserId\n| summarize Count=count() by UserType\n| order by Count desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "piechart", + "title": "Users by type (member / shared)" + }, + "name": "q-2c51776d", + "customWidth": "33" + }, + { + "type": 1, + "content": { + "json": "
Activity heatmap
" + }, + "name": "div-activity-heatmap-ca6a21" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Users_CL\n| summarize arg_max(TimeGenerated, *) by UserId\n| extend DaysSinceLogin = toint((now() - LastSeen) / 1d)\n| extend Bucket = case(\n DaysSinceLogin < 1, \"Today\",\n DaysSinceLogin < 7, \"This week\",\n DaysSinceLogin < 30, \"This month\",\n DaysSinceLogin < 90, \"Past quarter\",\n \"90+ days\")\n| summarize Users=count() by Bucket\n| order by case(Bucket==\"Today\",1, Bucket==\"This week\",2, Bucket==\"This month\",3, Bucket==\"Past quarter\",4, 5) asc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "barchart", + "title": "Users by recency of last login" + }, + "name": "q-350e118d" + }, + { + "type": 1, + "content": { + "json": "
Full user list
" + }, + "name": "div-full-user-list-136aac" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Users_CL\n| summarize arg_max(TimeGenerated, *) by UserId\n| project DisplayName, LoginName, Role, Status, UserType, DeviceCount, CurrentlyConnected, Created, LastSeen\n| order by LastSeen desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "All users (latest snapshot per user ID)", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Created", + "formatter": 6 + }, + { + "columnMatch": "LastSeen", + "formatter": 6 + }, + { + "columnMatch": "Role", + "formatter": 1 + }, + { + "columnMatch": "Status", + "formatter": 1 + }, + { + "columnMatch": "DeviceCount", + "formatter": 8, + "formatOptions": { + "palette": "blue" + } + } + ] + } + }, + "name": "q-cee23d3c" + }, + { + "type": 1, + "content": { + "json": "
Orphaned users (active but no devices)
" + }, + "name": "div-orphaned-users-(acti-56d6f8" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Users_CL\n| summarize arg_max(TimeGenerated, *) by UserId\n| where Status =~ \"active\" and DeviceCount == 0\n| project DisplayName, LoginName, Role, UserType, Created, LastSeen\n| order by Created desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Active accounts with zero devices - candidates for offboarding review", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Created", + "formatter": 6 + }, + { + "columnMatch": "LastSeen", + "formatter": 6 + } + ] + }, + "noDataMessage": "Every active account has at least one device - good hygiene.", + "noDataMessageStyle": 1 + }, + "name": "q-83fcd942" + }, + { + "type": 1, + "content": { + "json": "
Role escalation history
" + }, + "name": "div-role-escalation-hist-bd8df4" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| where Action == \"USER_ROLE_UPDATE\" or Action == \"USER_ROLES_ASSIGNED\" or Action contains \"ROLE\"\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\n| extend TargetName=tostring(coalesce(Target.name, Target.id))\n| extend FromRole=tostring(Old.role), ToRole=tostring(New.role)\n| project TimeGenerated, ActorLogin, Action, TargetName, FromRole, ToRole, Origin\n| order by TimeGenerated desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Recent role changes", + "gridSettings": { + "formatters": [ + { + "columnMatch": "TimeGenerated", + "formatter": 6 + }, + { + "columnMatch": "ToRole", + "formatter": 1 + } + ] + }, + "noDataMessage": "No role changes in this window.", + "noDataMessageStyle": 1 + }, + "name": "q-f6c8358a" + } + ] + }, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "identity" + }, + "name": "group-identity" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "
Device fleet snapshot
" + }, + "name": "div-device-fleet-snapsho-939675" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let D = Tailscale_Devices_CL | summarize arg_max(TimeGenerated, *) by DeviceId;\nunion\n (D | summarize V=toreal(count()) | extend Metric=\"Total devices\", Order=1),\n (D | where Authorized == true | summarize V=toreal(count()) | extend Metric=\"Authorized\", Order=2),\n (D | where IsExternal == true | summarize V=toreal(count()) | extend Metric=\"External (shared)\", Order=3),\n (D | where UpdateAvailable == true | summarize V=toreal(count()) | extend Metric=\"Updates available\", Order=4),\n (D | where SshEnabled == true | summarize V=toreal(count()) | extend Metric=\"SSH-enabled\", Order=5),\n (D | where KeyExpiryDisabled == true | summarize V=toreal(count()) | extend Metric=\"No key expiry\", Order=6),\n (D | where array_length(AdvertisedRoutes) > 0 | summarize V=toreal(count()) | extend Metric=\"Subnet/exit-node\", Order=7),\n (D | where LastSeen < ago(30d) | summarize V=toreal(count()) | extend Metric=\"Stale (30+ days)\", Order=8)\n| order by Order asc | project Metric, Value=V", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "tiles", + "tileSettings": { + "titleContent": { + "columnMatch": "Metric", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "Value", + "formatter": 12, + "formatOptions": { + "palette": "auto" + } + }, + "showBorder": false + } + }, + "name": "q-366964fc" + }, + { + "type": 1, + "content": { + "json": "
Distribution
" + }, + "name": "div-distribution-396d03" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| summarize Count=count() by Os\n| order by Count desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "piechart", + "title": "Devices by OS" + }, + "name": "q-0c8f5988", + "customWidth": "33" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| summarize Count=count() by ClientVersion\n| order by Count desc | take 10", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "barchart", + "title": "Top 10 client versions" + }, + "name": "q-af1c45cd", + "customWidth": "33" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| mv-expand Tag = Tags to typeof(string)\n| summarize Devices=dcount(DeviceId) by Tag=iff(isempty(Tag), \"(untagged)\", Tag)\n| order by Devices desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "piechart", + "title": "Devices by tag" + }, + "name": "q-c3a0ef5b", + "customWidth": "33" + }, + { + "type": 1, + "content": { + "json": "
Devices needing attention
" + }, + "name": "div-devices-needing-atte-f04a47" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where UpdateAvailable == true or KeyExpiryDisabled == true or LastSeen < ago(30d) or Authorized == false\n| extend Issues = strcat_array(pack_array(\n iff(UpdateAvailable == true, \"needs-update\", \"\"),\n iff(KeyExpiryDisabled == true, \"key-never-expires\", \"\"),\n iff(LastSeen < ago(30d), \"stale\", \"\"),\n iff(Authorized == false, \"unauthorized\", \"\")), \",\")\n| extend Issues = trim(\",\", trim_start(\",\", trim_end(\",\", replace_string(Issues, \",,\", \",\"))))\n| project DeviceName, Hostname, User, Os, ClientVersion, LastSeen, Issues\n| order by LastSeen desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Devices flagged with one or more issues", + "gridSettings": { + "formatters": [ + { + "columnMatch": "LastSeen", + "formatter": 6 + }, + { + "columnMatch": "Issues", + "formatter": 1 + } + ] + }, + "noDataMessage": "No devices need attention - all updated, fresh, authorized, and key-rotating.", + "noDataMessageStyle": 1 + }, + "name": "q-dc5db84c" + }, + { + "type": 1, + "content": { + "json": "
Full device inventory
" + }, + "name": "div-full-device-inventor-b70642" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| project DeviceName, Hostname, User, Os, ClientVersion, UpdateAvailable, Authorized, IsExternal, SshEnabled, LastSeen, KeyExpiryDisabled, Tags, AdvertisedRoutes\n| order by LastSeen desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "All devices (latest snapshot per device ID)", + "gridSettings": { + "formatters": [ + { + "columnMatch": "LastSeen", + "formatter": 6 + }, + { + "columnMatch": "Os", + "formatter": 1 + }, + { + "columnMatch": "ClientVersion", + "formatter": 1 + } + ] + } + }, + "name": "q-7f7e7a9a" + }, + { + "type": 1, + "content": { + "json": "
Subnet routers / exit nodes
" + }, + "name": "div-subnet-routers-/-exi-b082d6" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where array_length(AdvertisedRoutes) > 0\n| extend AdvertisedSummary = tostring(AdvertisedRoutes), EnabledSummary = tostring(EnabledRoutes)\n| project DeviceName, Hostname, User, Os, AdvertisedSummary, EnabledSummary, LastSeen, Authorized\n| order by LastSeen desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Devices advertising subnet routes or exit-node capability", + "gridSettings": { + "formatters": [ + { + "columnMatch": "LastSeen", + "formatter": 6 + } + ] + }, + "noDataMessage": "No subnet routers in this tailnet - pure mesh topology.", + "noDataMessageStyle": 1 + }, + "name": "q-5004df25" + } + ] + }, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "devices" + }, + "name": "group-devices" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "
Credentials snapshot
" + }, + "name": "div-credentials-snapshot-fd464a" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let K = Tailscale_Keys_CL | summarize arg_max(TimeGenerated, *) by KeyId;\nunion\n (K | summarize V=toreal(count()) | extend Metric=\"Total keys\", Order=1),\n (K | where isnull(Revoked) and (isnull(Expires) or Expires > now()) | summarize V=toreal(count()) | extend Metric=\"Active\", Order=2),\n (K | where isnotnull(Revoked) | summarize V=toreal(count()) | extend Metric=\"Revoked\", Order=3),\n (K | where Expires < now() and isnull(Revoked) | summarize V=toreal(count()) | extend Metric=\"Expired\", Order=4),\n (K | where isnull(Revoked) and Expires between(now() .. ago(-7d)) | summarize V=toreal(count()) | extend Metric=\"Expiring in 7d\", Order=5),\n (K | where isnull(Revoked) and (isnull(Expires) or ExpirySeconds==0) | summarize V=toreal(count()) | extend Metric=\"Never expire\", Order=6),\n (K | where KeyType =~ \"auth\" | summarize V=toreal(count()) | extend Metric=\"Auth keys\", Order=7),\n (K | where KeyType =~ \"api\" or KeyType contains \"oauth\" | summarize V=toreal(count()) | extend Metric=\"API / OAuth\", Order=8)\n| order by Order asc | project Metric, Value=V", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "tiles", + "tileSettings": { + "titleContent": { + "columnMatch": "Metric", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "Value", + "formatter": 12, + "formatOptions": { + "palette": "auto" + } + }, + "showBorder": false + } + }, + "name": "q-79398bc0" + }, + { + "type": 1, + "content": { + "json": "
Distribution
" + }, + "name": "div-distribution-15f665" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Keys_CL\n| summarize arg_max(TimeGenerated, *) by KeyId\n| summarize Count=count() by KeyType\n| order by Count desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "piechart", + "title": "Keys by type" + }, + "name": "q-23e6618b", + "customWidth": "50" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Keys_CL\n| summarize arg_max(TimeGenerated, *) by KeyId\n| where isnull(Revoked)\n| extend Bucket = case(\n isnull(Expires) or ExpirySeconds == 0, \"Never\",\n Expires < now(), \"Already expired\",\n Expires < ago(-1d), \"<24h\",\n Expires < ago(-7d), \"1-7d\",\n Expires < ago(-30d), \"8-30d\",\n Expires < ago(-90d), \"31-90d\",\n \"90+d\")\n| summarize Keys=count() by Bucket\n| order by case(Bucket==\"Already expired\",1, Bucket==\"<24h\",2, Bucket==\"1-7d\",3, Bucket==\"8-30d\",4, Bucket==\"31-90d\",5, Bucket==\"90+d\",6, Bucket==\"Never\",7, 8) asc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "barchart", + "title": "Active key expiry distribution" + }, + "name": "q-7484d1a0", + "customWidth": "50" + }, + { + "type": 1, + "content": { + "json": "
Active credential register
" + }, + "name": "div-active-credential-re-c27539" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Keys_CL\n| summarize arg_max(TimeGenerated, *) by KeyId\n| where isnull(Revoked)\n| extend ExpiryStatus = case(\n isnull(Expires) or ExpirySeconds == 0, \"Never expires\",\n Expires < now(), \"Expired\",\n Expires < ago(-7d), \"Expires in 7d\",\n Expires < ago(-30d), \"Expires in 30d\",\n \"OK\")\n| project KeyId, KeyType, Description, UserId, Created, Expires, ExpiryStatus, Capabilities\n| order by Created desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "All active credentials with computed expiry status", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Created", + "formatter": 6 + }, + { + "columnMatch": "Expires", + "formatter": 6 + }, + { + "columnMatch": "ExpiryStatus", + "formatter": 1 + }, + { + "columnMatch": "KeyType", + "formatter": 1 + } + ] + } + }, + "name": "q-4b27a750" + }, + { + "type": 1, + "content": { + "json": "
Credential CRUD events
" + }, + "name": "div-credential-crud-even-e455b0" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| where Action contains \"API_KEY\" or Action contains \"AUTH_KEY\" or Action contains \"OAUTH\" or Action contains \"KEY_CREATE\" or Action contains \"KEY_REVOKE\"\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\n| extend TargetId=tostring(Target.id), TargetType=tostring(Target.type)\n| project TimeGenerated, Action, ActorLogin, TargetType, TargetId, Origin\n| order by TimeGenerated desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Recent credential create / revoke / rotate events", + "gridSettings": { + "formatters": [ + { + "columnMatch": "TimeGenerated", + "formatter": 6 + } + ] + }, + "noDataMessage": "No credential CRUD activity in this window.", + "noDataMessageStyle": 1 + }, + "name": "q-777693bd" + } + ] + }, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "credentials" + }, + "name": "group-credentials" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "
Audit volume
" + }, + "name": "div-audit-volume-de29a2" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| summarize Events=count() by bin(TimeGenerated, 1h)\n| order by TimeGenerated asc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "timechart", + "title": "Audit events per hour", + "noDataMessage": "No audit events in this window.", + "noDataMessageStyle": 5 + }, + "name": "q-f5eee265" + }, + { + "type": 1, + "content": { + "json": "
Action heatmap by hour of day
" + }, + "name": "div-action-heatmap-by-ho-c8bd5e" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend Hour=hourofday(TimeGenerated)\n| summarize Events=count() by Hour, Action\n| order by Hour asc, Events desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "categoricalbar", + "title": "When are admin actions happening?" + }, + "name": "q-c6f45d95" + }, + { + "type": 1, + "content": { + "json": "
Actor / Action heatmap
" + }, + "name": "div-actor-/-action-heatm-d820ba" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\n| where isnotempty(ActorLogin)\n| summarize Events=count() by ActorLogin, Action\n| order by Events desc | take 100", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Who is firing which action", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Events", + "formatter": 4, + "formatOptions": { + "palette": "blue" + } + } + ] + }, + "noDataMessage": "No audit events in this window.", + "noDataMessageStyle": 5 + }, + "name": "q-06c13b3b" + }, + { + "type": 1, + "content": { + "json": "
Recent activity
" + }, + "name": "div-recent-activity-63b210" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\n| extend TargetType=tostring(Target.type), TargetName=tostring(coalesce(Target.name, Target.id))\n| project TimeGenerated, Action, ActorLogin, TargetType, TargetName, Origin, EventGroupID\n| order by TimeGenerated desc | take 100", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Last 100 audit events", + "gridSettings": { + "formatters": [ + { + "columnMatch": "TimeGenerated", + "formatter": 6 + }, + { + "columnMatch": "Action", + "formatter": 1 + } + ] + }, + "noDataMessage": "No audit events in this window.", + "noDataMessageStyle": 5 + }, + "name": "q-07a25a40" + } + ] + }, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "audit" + }, + "name": "group-audit" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "
DNS configuration (current state)
" + }, + "name": "div-dns-configuration-(c-5f2e1e" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Dns_CL\n| summarize arg_max(TimeGenerated, *) by ConfigType\n| project ConfigType, Nameservers, MagicDNS, SearchPaths, LastSnapshot=TimeGenerated\n| order by ConfigType asc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "MagicDNS, nameservers, search paths", + "gridSettings": { + "formatters": [ + { + "columnMatch": "LastSnapshot", + "formatter": 6 + }, + { + "columnMatch": "ConfigType", + "formatter": 1 + } + ] + }, + "noDataMessage": "No DNS snapshots in the workspace yet. DNS polls runs at ~30 min cadence.", + "noDataMessageStyle": 5 + }, + "name": "q-d6a6a358" + }, + { + "type": 1, + "content": { + "json": "
Tailnet settings (current)
" + }, + "name": "div-tailnet-settings-(cu-e03b16" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Settings_CL\n| summarize arg_max(TimeGenerated, *) by TenantId\n| project DevicesApprovalOn, DevicesAutoUpdatesOn, DevicesKeyDurationDays, UsersApprovalOn, NetworkFlowLoggingOn, RegionalRoutingOn, PostureIdentityCollectionOn, UsersRoleAllowedToJoinExternalTailnets, LastSnapshot=TimeGenerated", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Tailnet policy gates", + "noDataMessage": "No settings snapshot yet.", + "noDataMessageStyle": 5 + }, + "name": "q-0622cfc3" + }, + { + "type": 1, + "content": { + "json": "
DNS change history
" + }, + "name": "div-dns-change-history-9dd376" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend TargetProperty=tostring(Target.property)\n| where Action contains \"DNS\" or TargetProperty has_any (\"DNS_NAMESERVERS\", \"DNS_SPLIT_DNS\", \"MAGICDNS\", \"DNS_SEARCH_PATHS\")\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\n| project TimeGenerated, ActorLogin, Action, TargetProperty, Origin\n| order by TimeGenerated desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Recent DNS-related admin changes", + "gridSettings": { + "formatters": [ + { + "columnMatch": "TimeGenerated", + "formatter": 6 + } + ] + }, + "noDataMessage": "No DNS changes in this window.", + "noDataMessageStyle": 1 + }, + "name": "q-a132ff6a" + }, + { + "type": 1, + "content": { + "json": "
ACL policy changes
" + }, + "name": "div-acl-policy-changes-ff0e68" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| where Action == \"ACL_UPDATE\" or Action contains \"ACL\"\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\n| project TimeGenerated, ActorLogin, Action, Origin, EventGroupID\n| order by TimeGenerated desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Recent ACL / policy file modifications", + "gridSettings": { + "formatters": [ + { + "columnMatch": "TimeGenerated", + "formatter": 6 + } + ] + }, + "noDataMessage": "No ACL changes in this window.", + "noDataMessageStyle": 1 + }, + "name": "q-94818c53" + }, + { + "type": 1, + "content": { + "json": "
Subnet routes & exit nodes
" + }, + "name": "div-subnet-routes-and-ex-eef47f" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where array_length(EnabledRoutes) > 0\n| project DeviceName, User, Os, EnabledRoutes=tostring(EnabledRoutes), AdvertisedRoutes=tostring(AdvertisedRoutes), LastSeen\n| order by LastSeen desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Routes currently being served from devices", + "gridSettings": { + "formatters": [ + { + "columnMatch": "LastSeen", + "formatter": 6 + } + ] + }, + "noDataMessage": "No subnet routers active in this tailnet.", + "noDataMessageStyle": 1 + }, + "name": "q-61a792cd" + } + ] + }, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "network" + }, + "name": "group-network" + }, + { + "type": 12, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "network-flows" + }, + "name": "group-network-flows", + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "
Activity snapshot
" + }, + "name": "div-flow-snapshot" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let NET = Tailscale_Network_CL | where TimeGenerated {TimeRange};\nunion\n (NET | summarize V=toreal(count()) | extend Metric=\"Total flows\", Order=1),\n (NET | summarize V=toreal(dcount(SrcNodeName)) | extend Metric=\"Src nodes\", Order=2),\n (NET | summarize V=toreal(dcount(DstNodeName)) | extend Metric=\"Dst nodes\", Order=3),\n (NET | where HasVirtualTraffic | summarize V=toreal(count())| extend Metric=\"Virtual\", Order=4),\n (NET | where HasSubnetTraffic | summarize V=toreal(count())| extend Metric=\"Subnet\", Order=5),\n (NET | where HasExitTraffic | summarize V=toreal(count())| extend Metric=\"Exit\", Order=6),\n (NET | where IsRelayed | summarize V=toreal(count())| extend Metric=\"Relayed (DERP)\", Order=7)\n| order by Order asc | project Metric, Value=V", + "size": 3, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "tiles", + "title": "Activity (selected time range)", + "noDataMessage": "No data in this window.", + "noDataMessageStyle": 5, + "tileSettings": { + "titleContent": { + "columnMatch": "Metric", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "Value", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "style": "decimal", + "maximumFractionDigits": 0 + } + } + }, + "showBorder": false + } + }, + "name": "q-flow-tiles" + }, + { + "type": 1, + "content": { + "json": "
Top talkers
" + }, + "name": "div-flow-top-talkers" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Network_CL\n| where TimeGenerated {TimeRange}\n| extend SrcLabel = case(\n isnotempty(SrcUser), strcat(SrcNodeName, \" - \", SrcUser),\n isnotempty(SrcTags), strcat(SrcNodeName, \" \", tostring(SrcTags)),\n SrcNodeName)\n| summarize Flows=count() by SrcLabel\n| top 10 by Flows", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "barchart", + "title": "Top source nodes by flow count", + "noDataMessage": "No flow records in this window.", + "noDataMessageStyle": 5 + }, + "name": "q-flow-top-src", + "customWidth": "50" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Network_CL\n| where TimeGenerated {TimeRange}\n| extend DstLabel = case(\n isnotempty(DstUser), strcat(DstNodeName, \" - \", DstUser),\n isnotempty(DstTags), strcat(DstNodeName, \" \", tostring(DstTags)),\n DstNodeName)\n| extend DstKind = case(\n isnotempty(DstUser), \"User device\",\n isnotempty(DstTags), \"Tagged service\",\n \"Other\")\n| summarize Flows=count() by DstLabel, DstKind\n| top 10 by Flows", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "categoricalbar", + "title": "Top destination nodes (split by user vs tagged service)", + "noDataMessage": "No flow records in this window.", + "noDataMessageStyle": 5 + }, + "name": "q-flow-top-dst", + "customWidth": "50" + }, + { + "type": 1, + "content": { + "json": "
Traffic mix over time (stacked area: Virtual / Subnet / Exit / Physical)
" + }, + "name": "div-flow-time-mix" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let NET = Tailscale_Network_CL | where TimeGenerated {TimeRange};\nunion\n (NET | where HasVirtualTraffic | summarize Flows=count() by bin(TimeGenerated, 10m) | extend Kind=\"Virtual\"),\n (NET | where HasSubnetTraffic | summarize Flows=count() by bin(TimeGenerated, 10m) | extend Kind=\"Subnet\"),\n (NET | where HasExitTraffic | summarize Flows=count() by bin(TimeGenerated, 10m) | extend Kind=\"Exit\"),\n (NET | where HasPhysicalTraffic | summarize Flows=count() by bin(TimeGenerated, 10m) | extend Kind=\"Physical\")\n| project TimeGenerated, Kind, Flows\n| order by TimeGenerated asc", + "size": 4, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "timechart", + "title": "Flow count by traffic kind over time", + "noDataMessage": "No flow records.", + "noDataMessageStyle": 5, + "chartSettings": { + "group": "Kind", + "createOtherGroup": 10, + "showLegend": true, + "ySettings": { + "min": 0 + }, + "chartType": "Area" + } + }, + "name": "q-flow-traffic-mix" + }, + { + "type": 1, + "content": { + "json": "
DERP relay health
" + }, + "name": "div-flow-derp-watch" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Network_CL\n| where TimeGenerated {TimeRange}\n| summarize Count=count() by Path = iff(IsRelayed, \"Relayed (DERP)\", \"Direct (P2P)\")\n| project Path, Count", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "piechart", + "title": "Direct vs relayed", + "noDataMessage": "No flow records.", + "noDataMessageStyle": 5 + }, + "name": "q-flow-derp-pie", + "customWidth": "40" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Network_CL\n| where TimeGenerated {TimeRange}\n| where IsRelayed\n| extend SrcLabel = strcat(SrcNodeName, iff(isempty(SrcUser),\"\",strcat(\" (\",SrcUser,\")\")))\n| summarize RelayedFlows=count(), LastRelay=max(TimeGenerated) by SrcLabel, SrcOs\n| top 10 by RelayedFlows", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Devices stuck on DERP (potential NAT/firewall issue)", + "noDataMessage": "No data in this window.", + "noDataMessageStyle": 5, + "gridSettings": { + "formatters": [ + { + "columnMatch": "RelayedFlows", + "formatter": 4, + "formatOptions": { + "palette": "orange" + } + }, + { + "columnMatch": "LastRelay", + "formatter": 6 + } + ] + } + }, + "name": "q-flow-derp-devices", + "customWidth": "60" + }, + { + "type": 1, + "content": { + "json": "
Tagged-service flows + anomaly hunt
" + }, + "name": "div-flow-tag-anomaly" + }, + { + "type": 12, + "name": "row-tag-anomaly-row", + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Network_CL\n| where TimeGenerated {TimeRange}\n| extend SrcKind = case(\n isnotempty(SrcTags), tostring(SrcTags),\n isnotempty(SrcUser), \"\",\n \"\")\n| extend DstKind = case(\n isnotempty(DstTags), tostring(DstTags),\n isnotempty(DstUser), \"\",\n \"\")\n| summarize Flows=count() by SrcKind, DstKind\n| order by Flows desc", + "size": 1, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Tagged-service flow matrix", + "noDataMessage": "No data in this window.", + "noDataMessageStyle": 5, + "gridSettings": { + "formatters": [ + { + "columnMatch": "Flows", + "formatter": 4, + "formatOptions": { + "palette": "blue" + } + } + ] + } + }, + "name": "q-flow-tag-matrix", + "customWidth": "50" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let baseline = Tailscale_Network_CL\n | where TimeGenerated between (ago(7d) .. ago(1h))\n | distinct SrcNodeName, DstNodeName;\nTailscale_Network_CL\n| where TimeGenerated {TimeRange}\n| where HasVirtualTraffic or HasSubnetTraffic or HasExitTraffic\n| extend ShortSrc = tostring(split(SrcNodeName, \".\")[0])\n| extend ShortDst = tostring(split(DstNodeName, \".\")[0])\n| extend ShortSrcUser = tostring(split(SrcUser, \"@\")[0])\n| extend ShortDstUser = tostring(split(DstUser, \"@\")[0])\n| extend Src = strcat(ShortSrc, iff(isempty(ShortSrcUser),\"\",strcat(\" - \",ShortSrcUser)))\n| extend Dst = strcat(ShortDst, iff(isempty(ShortDstUser),\"\",strcat(\" - \",ShortDstUser)))\n| summarize FirstSeen=min(TimeGenerated), Flows=count() by Src, Dst, SrcNodeName, DstNodeName\n| join kind=leftanti baseline on SrcNodeName, DstNodeName\n| project FirstSeen, Src, Dst, Flows\n| order by FirstSeen desc", + "size": 1, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Anomaly: pairs NOT seen in past 7 days", + "noDataMessage": "No new src/dst pairs - all flows match the 7-day baseline.", + "noDataMessageStyle": 5, + "gridSettings": { + "formatters": [ + { + "columnMatch": "FirstSeen", + "formatter": 6 + }, + { + "columnMatch": "Flows", + "formatter": 4, + "formatOptions": { + "palette": "red" + } + } + ] + } + }, + "name": "q-flow-new-pairs", + "customWidth": "50" + } + ] + } + }, + { + "type": 1, + "content": { + "json": "
Egress destinations - exit nodes + subnet routes
" + }, + "name": "div-flow-egress" + }, + { + "type": 12, + "name": "row-egress-row", + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Network_CL\n| where TimeGenerated {TimeRange}\n| where HasExitTraffic\n| extend ExitTag = iff(isempty(DstTags), DstNodeName, tostring(DstTags))\n| summarize Flows=count() by ExitTag", + "size": 1, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "piechart", + "title": "Exit-node tag distribution", + "noDataMessage": "No exit-node traffic in this window.", + "noDataMessageStyle": 5 + }, + "name": "q-flow-exit-providers", + "customWidth": "40" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Network_CL\n| where TimeGenerated {TimeRange}\n| where HasSubnetTraffic\n| mv-expand s=SubnetTraffic\n| extend DstHost = tostring(split(tostring(s.dst), \":\")[0])\n| extend Bytes = toint(coalesce(s.txBytes,0)) + toint(coalesce(s.rxBytes,0))\n| extend Pkts = toint(coalesce(s.txPkts,0)) + toint(coalesce(s.rxPkts,0))\n| summarize TotalBytes=sum(Bytes), TotalPkts=sum(Pkts), Talkers=dcount(SrcNodeName) by DstHost\n| top 10 by TotalBytes\n| project DstHost, TotalBytes, TotalPkts, Talkers", + "size": 1, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Top subnet route destinations", + "noDataMessage": "No subnet-route traffic in this window.", + "noDataMessageStyle": 5, + "gridSettings": { + "formatters": [ + { + "columnMatch": "TotalBytes", + "formatter": 4, + "formatOptions": { + "palette": "green" + } + }, + { + "columnMatch": "TotalPkts", + "formatter": 4, + "formatOptions": { + "palette": "blue" + } + }, + { + "columnMatch": "Talkers", + "formatter": 4, + "formatOptions": { + "palette": "purple" + } + } + ] + } + }, + "name": "q-flow-subnet-dests", + "customWidth": "60" + } + ] + } + }, + { + "type": 1, + "content": { + "json": "
Recent flow detail (last 100)
" + }, + "name": "div-flow-recent-detail" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Network_CL\n| where TimeGenerated {TimeRange}\n| order by TimeGenerated desc\n| take 100\n| project TimeGenerated,\n Src=SrcNodeName, SrcUser, SrcTags=tostring(SrcTags),\n Dst=DstNodeName, DstUser, DstTags=tostring(DstTags),\n Vir=HasVirtualTraffic, Sub=HasSubnetTraffic, Exi=HasExitTraffic, Phy=HasPhysicalTraffic, IsRelayed", + "size": 4, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Last 100 flow records in time range", + "noDataMessage": "No data in this window.", + "noDataMessageStyle": 5, + "gridSettings": { + "formatters": [ + { + "columnMatch": "TimeGenerated", + "formatter": 6 + }, + { + "columnMatch": "IsRelayed", + "formatter": 11 + } + ] + } + }, + "name": "q-flow-recent" + } + ] + } + }, + { + "type": 12, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "posture" + }, + "name": "group-posture", + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "
Posture integrations - MDM/EDR providers configured for device posture
" + }, + "name": "div-posture-overview" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let CUR = Tailscale_PostureIntegrations_CL\n | where TimeGenerated {TimeRange}\n | summarize arg_max(TimeGenerated, *) by IntegrationId;\nunion\n (CUR | summarize V=toreal(count()) | extend Metric=\"Integrations\", Order=1),\n (CUR | summarize V=toreal(dcount(Provider)) | extend Metric=\"Distinct providers\", Order=2),\n (CUR | where tostring(Status) has \"healthy\" or tostring(Status) has \"ok\"\n | summarize V=toreal(count()) | extend Metric=\"Healthy\", Order=3),\n (CUR | where not(tostring(Status) has \"healthy\" or tostring(Status) has \"ok\")\n | summarize V=toreal(count()) | extend Metric=\"Unhealthy / unknown\", Order=4)\n| order by Order asc | project Metric, Value=V", + "size": 3, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "tiles", + "title": "Integration inventory (selected time range)", + "noDataMessage": "No posture integrations configured. Set them up at https://login.tailscale.com/admin/settings/posture-integrations.", + "noDataMessageStyle": 5, + "tileSettings": { + "titleContent": { + "columnMatch": "Metric", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "Value", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "style": "decimal", + "maximumFractionDigits": 0 + } + } + }, + "showBorder": false + } + }, + "name": "q-posture-tile" + }, + { + "type": 1, + "content": { + "json": "
Distribution
" + }, + "name": "div-posture-distribution" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_PostureIntegrations_CL\n| where TimeGenerated {TimeRange}\n| summarize arg_max(TimeGenerated, *) by IntegrationId\n| summarize Count=count() by Provider", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "piechart", + "title": "Integrations by provider", + "noDataMessage": "No posture integrations configured.", + "noDataMessageStyle": 5 + }, + "name": "q-posture-by-provider", + "customWidth": "50" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_PostureIntegrations_CL\n| where TimeGenerated {TimeRange}\n| summarize arg_max(TimeGenerated, *) by IntegrationId\n| extend Health = case(\n tostring(Status) has \"healthy\" or tostring(Status) has \"ok\", \"Healthy\",\n tostring(Status) has \"error\" or tostring(Status) has \"failed\", \"Error\",\n isnotempty(tostring(Status)), \"Other\",\n \"Unknown\")\n| summarize Count=count() by Health", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "piechart", + "title": "Health status across integrations", + "noDataMessage": "No posture integrations configured.", + "noDataMessageStyle": 5 + }, + "name": "q-posture-by-status", + "customWidth": "50" + }, + { + "type": 1, + "content": { + "json": "
Inventory detail
" + }, + "name": "div-posture-inventory" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_PostureIntegrations_CL\n| where TimeGenerated {TimeRange}\n| summarize arg_max(TimeGenerated, *) by IntegrationId\n| project IntegrationId, Provider, CloudId, ClientId, TenantId_Provider, Status, LastSnapshot=TimeGenerated", + "size": 4, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Configured integrations (latest snapshot per IntegrationId)", + "noDataMessage": "No posture integrations configured.", + "noDataMessageStyle": 5, + "gridSettings": { + "formatters": [ + { + "columnMatch": "Provider", + "formatter": 11 + }, + { + "columnMatch": "LastSnapshot", + "formatter": 6 + } + ] + } + }, + "name": "q-posture-inventory" + }, + { + "type": 1, + "content": { + "json": "
Lifecycle (from audit log)
" + }, + "name": "div-posture-lifecycle" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| where EventType == \"CONFIG\"\n| where tostring(Target.type) contains \"POSTURE\" or tostring(Target.type) contains \"INTEGRATION\"\n| project TimeGenerated, Actor=tostring(Actor.loginName), Action,\n Target=tostring(Target.name), TargetType=tostring(Target.type), ActionDetails\n| order by TimeGenerated desc", + "size": 4, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Recent posture integration create/update/delete events", + "noDataMessage": "No posture-integration audit events in this window.", + "noDataMessageStyle": 5, + "gridSettings": { + "formatters": [ + { + "columnMatch": "TimeGenerated", + "formatter": 6 + }, + { + "columnMatch": "Action", + "formatter": 11 + } + ] + } + }, + "name": "q-posture-audit" + } + ] + } + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "
Ingest rate per table
" + }, + "name": "div-ingest-rate-per-tabl-24e5f6" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "union withsource=Table Tailscale_Audit_CL, Tailscale_Devices_CL, Tailscale_Users_CL, Tailscale_Keys_CL, Tailscale_Dns_CL, Tailscale_Settings_CL\n| where TimeGenerated > ago(24h)\n| summarize Rows=count() by Table, bin(TimeGenerated, 1h)\n| order by TimeGenerated asc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "timechart", + "title": "Rows ingested per Tailscale table per hour (last 24h)", + "noDataMessage": "No Tailscale data ingested in the last 24h - check the connector card under Sentinel Data Connectors.", + "noDataMessageStyle": 5 + }, + "name": "q-c6fd0143" + }, + { + "type": 1, + "content": { + "json": "
Last poll time per table
" + }, + "name": "div-last-poll-time-per-t-49b051" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "union withsource=Table Tailscale_Audit_CL, Tailscale_Devices_CL, Tailscale_Users_CL, Tailscale_Keys_CL, Tailscale_Dns_CL, Tailscale_Settings_CL\n| summarize LastRow=max(TimeGenerated), TotalRows=count() by Table\n| extend MinutesAgo=toint((now() - LastRow) / 1m)\n| extend Status=case(MinutesAgo < 60, \"Fresh\", MinutesAgo < 360, \"Recent\", MinutesAgo < 1440, \"Stale\", \"Very Stale\")\n| project Table, LastRow, MinutesAgo, TotalRows, Status\n| order by MinutesAgo asc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Per-table freshness", + "gridSettings": { + "formatters": [ + { + "columnMatch": "LastRow", + "formatter": 6 + }, + { + "columnMatch": "MinutesAgo", + "formatter": 8, + "formatOptions": { + "palette": "redBright" + } + }, + { + "columnMatch": "TotalRows", + "formatter": 8, + "formatOptions": { + "palette": "blue" + } + }, + { + "columnMatch": "Status", + "formatter": 1 + } + ] + } + }, + "name": "q-a02e37a8" + }, + { + "type": 1, + "content": { + "json": "
Log Analytics operational events
" + }, + "name": "div-log-analytics-operat-630749" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "_LogOperation\n| where TimeGenerated > ago(24h)\n| where _ResourceId contains \"tailscale\" or Detail contains \"Tailscale_\"\n| project TimeGenerated, Operation, Level, Detail\n| order by TimeGenerated desc | take 100", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Log Analytics operational events touching Tailscale tables", + "gridSettings": { + "formatters": [ + { + "columnMatch": "TimeGenerated", + "formatter": 6 + }, + { + "columnMatch": "Level", + "formatter": 1 + } + ] + }, + "noDataMessage": "No operational issues recorded in the last 24h.", + "noDataMessageStyle": 1 + }, + "name": "q-b492f150" + } + ] + }, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "pipeline" + }, + "name": "group-pipeline" + }, + { + "type": 1, + "content": { + "json": "
Tailscale Operations (Premium) (CCF) - Microsoft Sentinel content from the Tailscale (CCF) solution, Premium-tier surface. Tables polled from the Tailscale REST API: audit, devices, users, keys, dns, settings, network flows (/logging/network), posture integrations. Filter every panel via the time range above; the Investigate tab adds Actor and Device pickers for drilldown. The Network Flows and Posture tabs use the 15 promoted columns added in 3.1.0 (SrcUser, SrcTags, DstUser, DstTags, HasVirtualTraffic, HasSubnetTraffic, HasExitTraffic, HasPhysicalTraffic, IsRelayed, etc.). Companion workbook: Tailscale Operations (Standard) for Personal / Starter tailnets.
" + }, + "name": "footer" + } + ], + "fallbackResourceIds": [ + "Azure Monitor" + ], + "fromTemplateId": "sentinel-TailscalePremiumWorkbook", + "$schema": "https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json" +} \ No newline at end of file diff --git a/Solutions/Tailscale (CCF)/Workbooks/TailscaleStandardOperations.json b/Solutions/Tailscale (CCF)/Workbooks/TailscaleStandardOperations.json new file mode 100644 index 00000000000..9aed3bfe696 --- /dev/null +++ b/Solutions/Tailscale (CCF)/Workbooks/TailscaleStandardOperations.json @@ -0,0 +1,1713 @@ +{ + "version": "Notebook/1.0", + "items": [ + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "7b05a598-5120-43f4-bf5d-576c2a7ff28d", + "version": "KqlParameterItem/1.0", + "name": "TimeRange", + "type": 4, + "isRequired": true, + "value": { + "durationMs": 86400000 + }, + "typeSettings": { + "selectableValues": [ + { + "durationMs": 3600000 + }, + { + "durationMs": 14400000 + }, + { + "durationMs": 43200000 + }, + { + "durationMs": 86400000 + }, + { + "durationMs": 172800000 + }, + { + "durationMs": 604800000 + }, + { + "durationMs": 2592000000 + } + ] + } + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "parameters" + }, + { + "type": 1, + "content": { + "json": "
Tailscale Operations (Standard)
Single-pane visibility into your Tailscale tailnet on Personal (Free), Starter and Standard tiers: who, what, when, where, and what changed. Scope every panel with the time range below; the Investigate tab adds Actor and Device pickers for drilldown. Premium-tier panels (network flow logs, posture integrations) live in the separate Tailscale Operations (Premium) workbook.
" + }, + "name": "header" + }, + { + "type": 11, + "content": { + "version": "LinkItem/1.0", + "style": "tabs", + "links": [ + { + "id": "9794e9fd-916b-494d-8e72-af63d2f4c6c7", + "cellValue": "selectedTab", + "linkTarget": "parameter", + "linkLabel": "Overview", + "subTarget": "overview", + "style": "link" + }, + { + "id": "71cf49db-33c5-4d4b-920a-2ec0c6a258dc", + "cellValue": "selectedTab", + "linkTarget": "parameter", + "linkLabel": "Investigate", + "subTarget": "investigate", + "style": "link" + }, + { + "id": "5c56bb37-2053-47a0-b6fd-9539768c144d", + "cellValue": "selectedTab", + "linkTarget": "parameter", + "linkLabel": "Hunts", + "subTarget": "hunts", + "style": "link" + }, + { + "id": "d2004ded-07f8-446a-a720-f0a63d1d9dda", + "cellValue": "selectedTab", + "linkTarget": "parameter", + "linkLabel": "Identity", + "subTarget": "identity", + "style": "link" + }, + { + "id": "f23b3e14-1511-4a29-bf5e-bd65e55dbb40", + "cellValue": "selectedTab", + "linkTarget": "parameter", + "linkLabel": "Devices", + "subTarget": "devices", + "style": "link" + }, + { + "id": "724f8352-e21b-45a6-9029-39dc92693c05", + "cellValue": "selectedTab", + "linkTarget": "parameter", + "linkLabel": "Credentials", + "subTarget": "credentials", + "style": "link" + }, + { + "id": "8400ec64-e118-44e0-ae29-84afe94b8e0e", + "cellValue": "selectedTab", + "linkTarget": "parameter", + "linkLabel": "Admin Audit", + "subTarget": "audit", + "style": "link" + }, + { + "id": "b7e04861-edfa-4426-b6be-f1481ca569b6", + "cellValue": "selectedTab", + "linkTarget": "parameter", + "linkLabel": "Network & DNS", + "subTarget": "network", + "style": "link" + }, + { + "id": "cfbc96e4-8585-4d89-8f65-8344c8cc6eb2", + "cellValue": "selectedTab", + "linkTarget": "parameter", + "linkLabel": "Pipeline Health", + "subTarget": "pipeline", + "style": "link" + } + ] + }, + "name": "tabs" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "
Tailnet at a glance
" + }, + "name": "div-tailnet-at-a-glance-04e62b" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let DEV = Tailscale_Devices_CL | summarize arg_max(TimeGenerated, *) by DeviceId;\nlet USR = Tailscale_Users_CL | summarize arg_max(TimeGenerated, *) by UserId;\nlet KEY = Tailscale_Keys_CL | summarize arg_max(TimeGenerated, *) by KeyId;\nunion\n (DEV | summarize V=toreal(count()) | extend Metric=\"Devices\", Order=1),\n (DEV | where Authorized == true | summarize V=toreal(count()) | extend Metric=\"Authorized\", Order=2),\n (DEV | where UpdateAvailable == true | summarize V=toreal(count()) | extend Metric=\"Updates Available\", Order=3),\n (DEV | where SshEnabled == true | summarize V=toreal(count()) | extend Metric=\"SSH-Enabled\", Order=4),\n (USR | summarize V=toreal(count()) | extend Metric=\"Users\", Order=5),\n (USR | where Role =~ \"admin\" or Role =~ \"owner\" or Role =~ \"network-admin\" | summarize V=toreal(count()) | extend Metric=\"Admins\", Order=6),\n (KEY | where isnull(Revoked) and (isnull(Expires) or Expires > now()) | summarize V=toreal(count()) | extend Metric=\"Active Keys\", Order=7),\n (Tailscale_Audit_CL | where TimeGenerated {TimeRange} | summarize V=toreal(count()) | extend Metric=\"Audit Events ({TimeRange:label})\", Order=8)\n| order by Order asc | project Metric, Value=V", + "size": 3, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "tiles", + "tileSettings": { + "titleContent": { + "columnMatch": "Metric", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "Value", + "formatter": 12, + "formatOptions": { + "palette": "auto" + } + }, + "showBorder": false + } + }, + "name": "q-4e9d7cff" + }, + { + "type": 1, + "content": { + "json": "
Audit activity over time
" + }, + "name": "div-audit-activity-over--546688" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| summarize EventCount = count() by bin(TimeGenerated, 1h), Action\n| order by TimeGenerated asc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "timechart", + "title": "Audit events by action", + "noDataMessage": "No audit events in the selected window. Widen the time range; remember the Tailscale audit poll runs every ~30 min.", + "noDataMessageStyle": 5 + }, + "name": "q-effcd498" + }, + { + "type": 1, + "content": { + "json": "
Who's doing what
" + }, + "name": "div-who's-doing-what-52144e" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend Actor=tostring(coalesce(Actor.loginName, Actor.displayName, Actor.type))\n| where isnotempty(Actor)\n| summarize Events=count(), DistinctActions=dcount(Action), LastSeen=max(TimeGenerated) by Actor\n| order by Events desc | take 15", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Top actors (by event count)", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Events", + "formatter": 8, + "formatOptions": { + "palette": "blue" + } + }, + { + "columnMatch": "LastSeen", + "formatter": 6 + } + ] + } + }, + "name": "q-34c77fa9", + "customWidth": "50" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend TargetType=tostring(Target.type)\n| where isnotempty(TargetType)\n| summarize Events=count() by TargetType\n| order by Events desc | take 15", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "piechart", + "title": "Activity by target type" + }, + "name": "q-4b3b709d", + "customWidth": "50" + }, + { + "type": 1, + "content": { + "json": "
Recent admin events
" + }, + "name": "div-recent-admin-events-87c9e0" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend Actor=tostring(coalesce(Actor.loginName, Actor.displayName, Actor.type))\n| extend TargetType=tostring(Target.type), TargetName=tostring(coalesce(Target.name, Target.id))\n| project TimeGenerated, Action, Actor, TargetType, TargetName, Origin\n| order by TimeGenerated desc | take 30", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Most recent 30 audit events", + "gridSettings": { + "formatters": [ + { + "columnMatch": "TimeGenerated", + "formatter": 6 + } + ] + } + }, + "name": "q-5e7d6306" + } + ] + }, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "overview" + }, + "name": "group-overview" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "b83a25c1-da18-49a2-a444-6517f13d891c", + "version": "KqlParameterItem/1.0", + "name": "SelectedActor", + "label": "Actor", + "type": 2, + "isRequired": false, + "query": "let opts = Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\n| where isnotempty(ActorLogin)\n| summarize Events=count() by ActorLogin\n| project value=ActorLogin, label=strcat(ActorLogin, \" (\", tostring(Events), \" events)\");\n(print value=\"__ALL__\", label=\"(All actors)\")\n| union opts", + "typeSettings": { + "showDefault": false + }, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "value": "__ALL__" + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "investigate-picker-actor" + }, + { + "type": 1, + "content": { + "json": "
Actor activity timeline
" + }, + "name": "div-actor-activity-timel-5ec305" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\n| where \"{SelectedActor}\" == \"__ALL__\" or ActorLogin == \"{SelectedActor}\"\n| summarize Events=count() by bin(TimeGenerated, 1h), Action\n| order by TimeGenerated asc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "timechart", + "title": "Actions over time -- actor: {SelectedActor:label}", + "noDataMessage": "Select an actor from the Actor dropdown above, or leave on 'All' to see total activity.", + "noDataMessageStyle": 5 + }, + "name": "q-32d40848" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\n| where \"{SelectedActor}\" == \"__ALL__\" or ActorLogin == \"{SelectedActor}\"\n| extend TargetType=tostring(Target.type), TargetName=tostring(coalesce(Target.name, Target.id))\n| project TimeGenerated, ActorLogin, Action, TargetType, TargetName, Origin\n| order by TimeGenerated desc | take 100", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Recent events for actor: {SelectedActor:label}", + "gridSettings": { + "formatters": [ + { + "columnMatch": "TimeGenerated", + "formatter": 6 + } + ] + }, + "noDataMessage": "No events for this actor in the selected window.", + "noDataMessageStyle": 5 + }, + "name": "q-a742f6fd" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "a9cf7907-f201-4725-a072-a8bd34bef74e", + "version": "KqlParameterItem/1.0", + "name": "SelectedDevice", + "label": "Device", + "type": 2, + "isRequired": false, + "query": "let opts = Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| order by LastSeen desc | take 100\n| project value=DeviceName, label=strcat(coalesce(DeviceName, Hostname), \" (\", User, \")\");\n(print value=\"__ALL__\", label=\"(All devices)\")\n| union opts", + "typeSettings": { + "showDefault": false + }, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "value": "__ALL__" + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "investigate-picker-device" + }, + { + "type": 1, + "content": { + "json": "
Selected device timeline
" + }, + "name": "div-selected-device-time-00bead" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| where \"{SelectedDevice}\" == \"__ALL__\" or DeviceName == \"{SelectedDevice}\"\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| extend OnlineNow = ClientConnectivity.endpoints != \"\" or ConnectedToControl == true\n| project DeviceName, Hostname, User, Os, ClientVersion, UpdateAvailable, Authorized, IsExternal, SshEnabled, LastSeen, Expires, KeyExpiryDisabled, OnlineNow, Addresses, Tags, AdvertisedRoutes", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Summary for device: {SelectedDevice:label}", + "noDataMessage": "Select a device from the Device dropdown above. Defaults to 'All'.", + "noDataMessageStyle": 5 + }, + "name": "q-11094dc8" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend TargetType=tostring(Target.type), TargetName=tostring(Target.name), TargetId=tostring(Target.id)\n| where (\"{SelectedDevice}\" == \"__ALL__\" and TargetType == \"NODE\") or TargetName == \"{SelectedDevice}\"\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\n| project TimeGenerated, Action, ActorLogin, TargetName, TargetId, Origin\n| order by TimeGenerated desc | take 100", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Audit events touching device: {SelectedDevice:label}", + "gridSettings": { + "formatters": [ + { + "columnMatch": "TimeGenerated", + "formatter": 6 + } + ] + }, + "noDataMessage": "No audit events recorded against the selected device in this window. Tailscale tags device events with Target.type=NODE; the audit feed only emits NODE events on create/update/delete, so quiet devices stay quiet here.", + "noDataMessageStyle": 5 + }, + "name": "q-ead106ce" + } + ] + }, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "investigate" + }, + "name": "group-investigate" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "
First-seen actors in the last 24h
" + }, + "name": "div-first-seen-actors-in-ce38d9" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let recent = Tailscale_Audit_CL | where TimeGenerated > ago(24h) | extend A=tostring(coalesce(Actor.loginName, Actor.displayName)) | summarize FirstSeen24h=min(TimeGenerated), Events=count() by A;\nlet historical = Tailscale_Audit_CL | where TimeGenerated between(ago(30d) .. ago(24h)) | extend A=tostring(coalesce(Actor.loginName, Actor.displayName)) | distinct A;\nrecent | join kind=leftanti historical on A | where isnotempty(A) | project FirstSeen24h, Actor=A, Events | order by Events desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Actors who have NEVER appeared before (30d baseline)", + "gridSettings": { + "formatters": [ + { + "columnMatch": "FirstSeen24h", + "formatter": 6 + }, + { + "columnMatch": "Events", + "formatter": 8, + "formatOptions": { + "palette": "orange" + } + } + ] + }, + "noDataMessage": "Every actor seen in the last 24h has appeared at least once in the prior 30d. Healthy state.", + "noDataMessageStyle": 1 + }, + "name": "q-9ba7b85e" + }, + { + "type": 1, + "content": { + "json": "
Off-hours configuration changes
" + }, + "name": "div-off-hours-configurat-3c3787" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend Hour=hourofday(TimeGenerated), DayOfWeek=dayofweek(TimeGenerated)/1d\n| where Hour < 7 or Hour > 19 or DayOfWeek in (0, 6)\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\n| extend TargetType=tostring(Target.type)\n| where Action !in (\"LOGIN\", \"LOGOUT\")\n| project TimeGenerated, ActorLogin, Action, TargetType, Origin\n| order by TimeGenerated desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Admin actions outside 07:00-19:00 weekdays", + "gridSettings": { + "formatters": [ + { + "columnMatch": "TimeGenerated", + "formatter": 6 + } + ] + }, + "noDataMessage": "No off-hours admin changes recorded - healthy state for an organisation working business hours.", + "noDataMessageStyle": 1 + }, + "name": "q-721ee490" + }, + { + "type": 1, + "content": { + "json": "
Devices with key expiry disabled
" + }, + "name": "div-devices-with-key-exp-dfe9c1" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where KeyExpiryDisabled == true\n| project DeviceName, Hostname, User, Os, ClientVersion, LastSeen, Authorized, Tags\n| order by LastSeen desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Devices that will never re-authenticate (high-risk drift)", + "gridSettings": { + "formatters": [ + { + "columnMatch": "LastSeen", + "formatter": 6 + } + ] + }, + "noDataMessage": "No devices have key expiry disabled - good. Disabling key expiry creates devices that never re-auth, drifting from policy.", + "noDataMessageStyle": 1 + }, + "name": "q-8a8e2fb5" + }, + { + "type": 1, + "content": { + "json": "
Auth keys with no expiry
" + }, + "name": "div-auth-keys-with-no-ex-b11207" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Keys_CL\n| summarize arg_max(TimeGenerated, *) by KeyId\n| where isnull(Revoked) and (isnull(Expires) or ExpirySeconds == 0)\n| project KeyId, Description, UserId, KeyType, Created, Capabilities\n| order by Created desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Active keys that never expire", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Created", + "formatter": 6 + } + ] + }, + "noDataMessage": "No never-expiring auth keys - rotation hygiene is good.", + "noDataMessageStyle": 1 + }, + "name": "q-1372740c" + }, + { + "type": 1, + "content": { + "json": "
Devices running outdated clients
" + }, + "name": "div-devices-running-outd-bcd077" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where UpdateAvailable == true\n| project DeviceName, Hostname, User, Os, ClientVersion, LastSeen, Tags\n| order by LastSeen desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Devices flagged update-available by Tailscale", + "gridSettings": { + "formatters": [ + { + "columnMatch": "LastSeen", + "formatter": 6 + } + ] + }, + "noDataMessage": "All devices on current client - nothing to patch.", + "noDataMessageStyle": 1 + }, + "name": "q-ecebf1f7" + }, + { + "type": 1, + "content": { + "json": "
Dormant devices (LastSeen > 30 days)
" + }, + "name": "div-dormant-devices-(las-761156" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where LastSeen < ago(30d)\n| extend DaysIdle = toint((now() - LastSeen) / 1d)\n| project DeviceName, Hostname, User, Os, ClientVersion, LastSeen, DaysIdle, Authorized, Tags\n| order by DaysIdle desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Devices idle 30+ days - candidates for retirement", + "gridSettings": { + "formatters": [ + { + "columnMatch": "LastSeen", + "formatter": 6 + }, + { + "columnMatch": "DaysIdle", + "formatter": 8, + "formatOptions": { + "palette": "redBright" + } + } + ] + }, + "noDataMessage": "No devices idle 30+ days - inventory is fresh.", + "noDataMessageStyle": 1 + }, + "name": "q-fb6c1fcc" + }, + { + "type": 1, + "content": { + "json": "
Subnet route exposure
" + }, + "name": "div-subnet-route-exposur-289c42" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where array_length(AdvertisedRoutes) > 0 or array_length(EnabledRoutes) > 0\n| extend Routes = tostring(EnabledRoutes), Advertised = tostring(AdvertisedRoutes)\n| project DeviceName, Hostname, User, Os, Advertised, Routes, LastSeen, SshEnabled, Authorized\n| order by LastSeen desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Devices advertising or running subnet routes / exit-node duty", + "gridSettings": { + "formatters": [ + { + "columnMatch": "LastSeen", + "formatter": 6 + } + ] + }, + "noDataMessage": "No devices advertising subnet routes. Pure mesh topology.", + "noDataMessageStyle": 1 + }, + "name": "q-9533b081" + }, + { + "type": 1, + "content": { + "json": "
Devices with SSH enabled
" + }, + "name": "div-devices-with-ssh-ena-285238" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where SshEnabled == true\n| project DeviceName, Hostname, User, Os, ClientVersion, LastSeen, Authorized, Tags\n| order by LastSeen desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Devices with Tailscale SSH enabled (Tailscale-managed remote-shell access)", + "gridSettings": { + "formatters": [ + { + "columnMatch": "LastSeen", + "formatter": 6 + } + ] + }, + "noDataMessage": "No devices have Tailscale SSH enabled - no SSH-via-Tailscale risk surface.", + "noDataMessageStyle": 1 + }, + "name": "q-735368fb" + } + ] + }, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "hunts" + }, + "name": "group-hunts" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "
User inventory snapshot
" + }, + "name": "div-user-inventory-snaps-9dc96c" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let U = Tailscale_Users_CL | summarize arg_max(TimeGenerated, *) by UserId;\nunion\n (U | summarize V=toreal(count()) | extend Metric=\"Total users\", Order=1),\n (U | where Role in~ (\"admin\",\"owner\",\"network-admin\",\"it-admin\",\"billing-admin\") | summarize V=toreal(count()) | extend Metric=\"Admin-tier users\", Order=2),\n (U | where Status =~ \"active\" | summarize V=toreal(count()) | extend Metric=\"Active\", Order=3),\n (U | where CurrentlyConnected == true | summarize V=toreal(count()) | extend Metric=\"Connected now\", Order=4),\n (U | where Status =~ \"idle\" or LastSeen < ago(30d) | summarize V=toreal(count()) | extend Metric=\"Idle / dormant\", Order=5),\n (U | where UserType =~ \"shared\" | summarize V=toreal(count()) | extend Metric=\"Shared (external)\", Order=6)\n| order by Order asc | project Metric, Value=V", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "tiles", + "tileSettings": { + "titleContent": { + "columnMatch": "Metric", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "Value", + "formatter": 12, + "formatOptions": { + "palette": "auto" + } + }, + "showBorder": false + } + }, + "name": "q-5689d9e8" + }, + { + "type": 1, + "content": { + "json": "
Distribution
" + }, + "name": "div-distribution-de67ec" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Users_CL\n| summarize arg_max(TimeGenerated, *) by UserId\n| summarize Count=count() by Role\n| order by Count desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "piechart", + "title": "Users by role" + }, + "name": "q-0c47912a", + "customWidth": "33" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Users_CL\n| summarize arg_max(TimeGenerated, *) by UserId\n| summarize Count=count() by Status\n| order by Count desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "piechart", + "title": "Users by status" + }, + "name": "q-e8c20a69", + "customWidth": "33" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Users_CL\n| summarize arg_max(TimeGenerated, *) by UserId\n| summarize Count=count() by UserType\n| order by Count desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "piechart", + "title": "Users by type (member / shared)" + }, + "name": "q-2c51776d", + "customWidth": "33" + }, + { + "type": 1, + "content": { + "json": "
Activity heatmap
" + }, + "name": "div-activity-heatmap-ca6a21" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Users_CL\n| summarize arg_max(TimeGenerated, *) by UserId\n| extend DaysSinceLogin = toint((now() - LastSeen) / 1d)\n| extend Bucket = case(\n DaysSinceLogin < 1, \"Today\",\n DaysSinceLogin < 7, \"This week\",\n DaysSinceLogin < 30, \"This month\",\n DaysSinceLogin < 90, \"Past quarter\",\n \"90+ days\")\n| summarize Users=count() by Bucket\n| order by case(Bucket==\"Today\",1, Bucket==\"This week\",2, Bucket==\"This month\",3, Bucket==\"Past quarter\",4, 5) asc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "barchart", + "title": "Users by recency of last login" + }, + "name": "q-350e118d" + }, + { + "type": 1, + "content": { + "json": "
Full user list
" + }, + "name": "div-full-user-list-136aac" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Users_CL\n| summarize arg_max(TimeGenerated, *) by UserId\n| project DisplayName, LoginName, Role, Status, UserType, DeviceCount, CurrentlyConnected, Created, LastSeen\n| order by LastSeen desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "All users (latest snapshot per user ID)", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Created", + "formatter": 6 + }, + { + "columnMatch": "LastSeen", + "formatter": 6 + }, + { + "columnMatch": "Role", + "formatter": 1 + }, + { + "columnMatch": "Status", + "formatter": 1 + }, + { + "columnMatch": "DeviceCount", + "formatter": 8, + "formatOptions": { + "palette": "blue" + } + } + ] + } + }, + "name": "q-cee23d3c" + }, + { + "type": 1, + "content": { + "json": "
Orphaned users (active but no devices)
" + }, + "name": "div-orphaned-users-(acti-56d6f8" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Users_CL\n| summarize arg_max(TimeGenerated, *) by UserId\n| where Status =~ \"active\" and DeviceCount == 0\n| project DisplayName, LoginName, Role, UserType, Created, LastSeen\n| order by Created desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Active accounts with zero devices - candidates for offboarding review", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Created", + "formatter": 6 + }, + { + "columnMatch": "LastSeen", + "formatter": 6 + } + ] + }, + "noDataMessage": "Every active account has at least one device - good hygiene.", + "noDataMessageStyle": 1 + }, + "name": "q-83fcd942" + }, + { + "type": 1, + "content": { + "json": "
Role escalation history
" + }, + "name": "div-role-escalation-hist-bd8df4" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| where Action == \"USER_ROLE_UPDATE\" or Action == \"USER_ROLES_ASSIGNED\" or Action contains \"ROLE\"\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\n| extend TargetName=tostring(coalesce(Target.name, Target.id))\n| extend FromRole=tostring(Old.role), ToRole=tostring(New.role)\n| project TimeGenerated, ActorLogin, Action, TargetName, FromRole, ToRole, Origin\n| order by TimeGenerated desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Recent role changes", + "gridSettings": { + "formatters": [ + { + "columnMatch": "TimeGenerated", + "formatter": 6 + }, + { + "columnMatch": "ToRole", + "formatter": 1 + } + ] + }, + "noDataMessage": "No role changes in this window.", + "noDataMessageStyle": 1 + }, + "name": "q-f6c8358a" + } + ] + }, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "identity" + }, + "name": "group-identity" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "
Device fleet snapshot
" + }, + "name": "div-device-fleet-snapsho-939675" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let D = Tailscale_Devices_CL | summarize arg_max(TimeGenerated, *) by DeviceId;\nunion\n (D | summarize V=toreal(count()) | extend Metric=\"Total devices\", Order=1),\n (D | where Authorized == true | summarize V=toreal(count()) | extend Metric=\"Authorized\", Order=2),\n (D | where IsExternal == true | summarize V=toreal(count()) | extend Metric=\"External (shared)\", Order=3),\n (D | where UpdateAvailable == true | summarize V=toreal(count()) | extend Metric=\"Updates available\", Order=4),\n (D | where SshEnabled == true | summarize V=toreal(count()) | extend Metric=\"SSH-enabled\", Order=5),\n (D | where KeyExpiryDisabled == true | summarize V=toreal(count()) | extend Metric=\"No key expiry\", Order=6),\n (D | where array_length(AdvertisedRoutes) > 0 | summarize V=toreal(count()) | extend Metric=\"Subnet/exit-node\", Order=7),\n (D | where LastSeen < ago(30d) | summarize V=toreal(count()) | extend Metric=\"Stale (30+ days)\", Order=8)\n| order by Order asc | project Metric, Value=V", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "tiles", + "tileSettings": { + "titleContent": { + "columnMatch": "Metric", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "Value", + "formatter": 12, + "formatOptions": { + "palette": "auto" + } + }, + "showBorder": false + } + }, + "name": "q-366964fc" + }, + { + "type": 1, + "content": { + "json": "
Distribution
" + }, + "name": "div-distribution-396d03" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| summarize Count=count() by Os\n| order by Count desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "piechart", + "title": "Devices by OS" + }, + "name": "q-0c8f5988", + "customWidth": "33" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| summarize Count=count() by ClientVersion\n| order by Count desc | take 10", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "barchart", + "title": "Top 10 client versions" + }, + "name": "q-af1c45cd", + "customWidth": "33" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| mv-expand Tag = Tags to typeof(string)\n| summarize Devices=dcount(DeviceId) by Tag=iff(isempty(Tag), \"(untagged)\", Tag)\n| order by Devices desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "piechart", + "title": "Devices by tag" + }, + "name": "q-c3a0ef5b", + "customWidth": "33" + }, + { + "type": 1, + "content": { + "json": "
Devices needing attention
" + }, + "name": "div-devices-needing-atte-f04a47" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where UpdateAvailable == true or KeyExpiryDisabled == true or LastSeen < ago(30d) or Authorized == false\n| extend Issues = strcat_array(pack_array(\n iff(UpdateAvailable == true, \"needs-update\", \"\"),\n iff(KeyExpiryDisabled == true, \"key-never-expires\", \"\"),\n iff(LastSeen < ago(30d), \"stale\", \"\"),\n iff(Authorized == false, \"unauthorized\", \"\")), \",\")\n| extend Issues = trim(\",\", trim_start(\",\", trim_end(\",\", replace_string(Issues, \",,\", \",\"))))\n| project DeviceName, Hostname, User, Os, ClientVersion, LastSeen, Issues\n| order by LastSeen desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Devices flagged with one or more issues", + "gridSettings": { + "formatters": [ + { + "columnMatch": "LastSeen", + "formatter": 6 + }, + { + "columnMatch": "Issues", + "formatter": 1 + } + ] + }, + "noDataMessage": "No devices need attention - all updated, fresh, authorized, and key-rotating.", + "noDataMessageStyle": 1 + }, + "name": "q-dc5db84c" + }, + { + "type": 1, + "content": { + "json": "
Full device inventory
" + }, + "name": "div-full-device-inventor-b70642" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| project DeviceName, Hostname, User, Os, ClientVersion, UpdateAvailable, Authorized, IsExternal, SshEnabled, LastSeen, KeyExpiryDisabled, Tags, AdvertisedRoutes\n| order by LastSeen desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "All devices (latest snapshot per device ID)", + "gridSettings": { + "formatters": [ + { + "columnMatch": "LastSeen", + "formatter": 6 + }, + { + "columnMatch": "Os", + "formatter": 1 + }, + { + "columnMatch": "ClientVersion", + "formatter": 1 + } + ] + } + }, + "name": "q-7f7e7a9a" + }, + { + "type": 1, + "content": { + "json": "
Subnet routers / exit nodes
" + }, + "name": "div-subnet-routers-/-exi-b082d6" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where array_length(AdvertisedRoutes) > 0\n| extend AdvertisedSummary = tostring(AdvertisedRoutes), EnabledSummary = tostring(EnabledRoutes)\n| project DeviceName, Hostname, User, Os, AdvertisedSummary, EnabledSummary, LastSeen, Authorized\n| order by LastSeen desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Devices advertising subnet routes or exit-node capability", + "gridSettings": { + "formatters": [ + { + "columnMatch": "LastSeen", + "formatter": 6 + } + ] + }, + "noDataMessage": "No subnet routers in this tailnet - pure mesh topology.", + "noDataMessageStyle": 1 + }, + "name": "q-5004df25" + } + ] + }, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "devices" + }, + "name": "group-devices" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "
Credentials snapshot
" + }, + "name": "div-credentials-snapshot-fd464a" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let K = Tailscale_Keys_CL | summarize arg_max(TimeGenerated, *) by KeyId;\nunion\n (K | summarize V=toreal(count()) | extend Metric=\"Total keys\", Order=1),\n (K | where isnull(Revoked) and (isnull(Expires) or Expires > now()) | summarize V=toreal(count()) | extend Metric=\"Active\", Order=2),\n (K | where isnotnull(Revoked) | summarize V=toreal(count()) | extend Metric=\"Revoked\", Order=3),\n (K | where Expires < now() and isnull(Revoked) | summarize V=toreal(count()) | extend Metric=\"Expired\", Order=4),\n (K | where isnull(Revoked) and Expires between(now() .. ago(-7d)) | summarize V=toreal(count()) | extend Metric=\"Expiring in 7d\", Order=5),\n (K | where isnull(Revoked) and (isnull(Expires) or ExpirySeconds==0) | summarize V=toreal(count()) | extend Metric=\"Never expire\", Order=6),\n (K | where KeyType =~ \"auth\" | summarize V=toreal(count()) | extend Metric=\"Auth keys\", Order=7),\n (K | where KeyType =~ \"api\" or KeyType contains \"oauth\" | summarize V=toreal(count()) | extend Metric=\"API / OAuth\", Order=8)\n| order by Order asc | project Metric, Value=V", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "tiles", + "tileSettings": { + "titleContent": { + "columnMatch": "Metric", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "Value", + "formatter": 12, + "formatOptions": { + "palette": "auto" + } + }, + "showBorder": false + } + }, + "name": "q-79398bc0" + }, + { + "type": 1, + "content": { + "json": "
Distribution
" + }, + "name": "div-distribution-15f665" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Keys_CL\n| summarize arg_max(TimeGenerated, *) by KeyId\n| summarize Count=count() by KeyType\n| order by Count desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "piechart", + "title": "Keys by type" + }, + "name": "q-23e6618b", + "customWidth": "50" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Keys_CL\n| summarize arg_max(TimeGenerated, *) by KeyId\n| where isnull(Revoked)\n| extend Bucket = case(\n isnull(Expires) or ExpirySeconds == 0, \"Never\",\n Expires < now(), \"Already expired\",\n Expires < ago(-1d), \"<24h\",\n Expires < ago(-7d), \"1-7d\",\n Expires < ago(-30d), \"8-30d\",\n Expires < ago(-90d), \"31-90d\",\n \"90+d\")\n| summarize Keys=count() by Bucket\n| order by case(Bucket==\"Already expired\",1, Bucket==\"<24h\",2, Bucket==\"1-7d\",3, Bucket==\"8-30d\",4, Bucket==\"31-90d\",5, Bucket==\"90+d\",6, Bucket==\"Never\",7, 8) asc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "barchart", + "title": "Active key expiry distribution" + }, + "name": "q-7484d1a0", + "customWidth": "50" + }, + { + "type": 1, + "content": { + "json": "
Active credential register
" + }, + "name": "div-active-credential-re-c27539" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Keys_CL\n| summarize arg_max(TimeGenerated, *) by KeyId\n| where isnull(Revoked)\n| extend ExpiryStatus = case(\n isnull(Expires) or ExpirySeconds == 0, \"Never expires\",\n Expires < now(), \"Expired\",\n Expires < ago(-7d), \"Expires in 7d\",\n Expires < ago(-30d), \"Expires in 30d\",\n \"OK\")\n| project KeyId, KeyType, Description, UserId, Created, Expires, ExpiryStatus, Capabilities\n| order by Created desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "All active credentials with computed expiry status", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Created", + "formatter": 6 + }, + { + "columnMatch": "Expires", + "formatter": 6 + }, + { + "columnMatch": "ExpiryStatus", + "formatter": 1 + }, + { + "columnMatch": "KeyType", + "formatter": 1 + } + ] + } + }, + "name": "q-4b27a750" + }, + { + "type": 1, + "content": { + "json": "
Credential CRUD events
" + }, + "name": "div-credential-crud-even-e455b0" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| where Action contains \"API_KEY\" or Action contains \"AUTH_KEY\" or Action contains \"OAUTH\" or Action contains \"KEY_CREATE\" or Action contains \"KEY_REVOKE\"\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\n| extend TargetId=tostring(Target.id), TargetType=tostring(Target.type)\n| project TimeGenerated, Action, ActorLogin, TargetType, TargetId, Origin\n| order by TimeGenerated desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Recent credential create / revoke / rotate events", + "gridSettings": { + "formatters": [ + { + "columnMatch": "TimeGenerated", + "formatter": 6 + } + ] + }, + "noDataMessage": "No credential CRUD activity in this window.", + "noDataMessageStyle": 1 + }, + "name": "q-777693bd" + } + ] + }, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "credentials" + }, + "name": "group-credentials" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "
Audit volume
" + }, + "name": "div-audit-volume-de29a2" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| summarize Events=count() by bin(TimeGenerated, 1h)\n| order by TimeGenerated asc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "timechart", + "title": "Audit events per hour", + "noDataMessage": "No audit events in this window.", + "noDataMessageStyle": 5 + }, + "name": "q-f5eee265" + }, + { + "type": 1, + "content": { + "json": "
Action heatmap by hour of day
" + }, + "name": "div-action-heatmap-by-ho-c8bd5e" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend Hour=hourofday(TimeGenerated)\n| summarize Events=count() by Hour, Action\n| order by Hour asc, Events desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "categoricalbar", + "title": "When are admin actions happening?" + }, + "name": "q-c6f45d95" + }, + { + "type": 1, + "content": { + "json": "
Actor / Action heatmap
" + }, + "name": "div-actor-/-action-heatm-d820ba" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\n| where isnotempty(ActorLogin)\n| summarize Events=count() by ActorLogin, Action\n| order by Events desc | take 100", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Who is firing which action", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Events", + "formatter": 4, + "formatOptions": { + "palette": "blue" + } + } + ] + }, + "noDataMessage": "No audit events in this window.", + "noDataMessageStyle": 5 + }, + "name": "q-06c13b3b" + }, + { + "type": 1, + "content": { + "json": "
Recent activity
" + }, + "name": "div-recent-activity-63b210" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\n| extend TargetType=tostring(Target.type), TargetName=tostring(coalesce(Target.name, Target.id))\n| project TimeGenerated, Action, ActorLogin, TargetType, TargetName, Origin, EventGroupID\n| order by TimeGenerated desc | take 100", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Last 100 audit events", + "gridSettings": { + "formatters": [ + { + "columnMatch": "TimeGenerated", + "formatter": 6 + }, + { + "columnMatch": "Action", + "formatter": 1 + } + ] + }, + "noDataMessage": "No audit events in this window.", + "noDataMessageStyle": 5 + }, + "name": "q-07a25a40" + } + ] + }, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "audit" + }, + "name": "group-audit" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "
DNS configuration (current state)
" + }, + "name": "div-dns-configuration-(c-5f2e1e" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Dns_CL\n| summarize arg_max(TimeGenerated, *) by ConfigType\n| project ConfigType, Nameservers, MagicDNS, SearchPaths, LastSnapshot=TimeGenerated\n| order by ConfigType asc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "MagicDNS, nameservers, search paths", + "gridSettings": { + "formatters": [ + { + "columnMatch": "LastSnapshot", + "formatter": 6 + }, + { + "columnMatch": "ConfigType", + "formatter": 1 + } + ] + }, + "noDataMessage": "No DNS snapshots in the workspace yet. DNS polls runs at ~30 min cadence.", + "noDataMessageStyle": 5 + }, + "name": "q-d6a6a358" + }, + { + "type": 1, + "content": { + "json": "
Tailnet settings (current)
" + }, + "name": "div-tailnet-settings-(cu-e03b16" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Settings_CL\n| summarize arg_max(TimeGenerated, *) by TenantId\n| project DevicesApprovalOn, DevicesAutoUpdatesOn, DevicesKeyDurationDays, UsersApprovalOn, NetworkFlowLoggingOn, RegionalRoutingOn, PostureIdentityCollectionOn, UsersRoleAllowedToJoinExternalTailnets, LastSnapshot=TimeGenerated", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Tailnet policy gates", + "noDataMessage": "No settings snapshot yet.", + "noDataMessageStyle": 5 + }, + "name": "q-0622cfc3" + }, + { + "type": 1, + "content": { + "json": "
DNS change history
" + }, + "name": "div-dns-change-history-9dd376" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| extend TargetProperty=tostring(Target.property)\n| where Action contains \"DNS\" or TargetProperty has_any (\"DNS_NAMESERVERS\", \"DNS_SPLIT_DNS\", \"MAGICDNS\", \"DNS_SEARCH_PATHS\")\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\n| project TimeGenerated, ActorLogin, Action, TargetProperty, Origin\n| order by TimeGenerated desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Recent DNS-related admin changes", + "gridSettings": { + "formatters": [ + { + "columnMatch": "TimeGenerated", + "formatter": 6 + } + ] + }, + "noDataMessage": "No DNS changes in this window.", + "noDataMessageStyle": 1 + }, + "name": "q-a132ff6a" + }, + { + "type": 1, + "content": { + "json": "
ACL policy changes
" + }, + "name": "div-acl-policy-changes-ff0e68" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Audit_CL\n| where TimeGenerated {TimeRange}\n| where Action == \"ACL_UPDATE\" or Action contains \"ACL\"\n| extend ActorLogin=tostring(coalesce(Actor.loginName, Actor.displayName))\n| project TimeGenerated, ActorLogin, Action, Origin, EventGroupID\n| order by TimeGenerated desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Recent ACL / policy file modifications", + "gridSettings": { + "formatters": [ + { + "columnMatch": "TimeGenerated", + "formatter": 6 + } + ] + }, + "noDataMessage": "No ACL changes in this window.", + "noDataMessageStyle": 1 + }, + "name": "q-94818c53" + }, + { + "type": 1, + "content": { + "json": "
Subnet routes & exit nodes
" + }, + "name": "div-subnet-routes-and-ex-eef47f" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Tailscale_Devices_CL\n| summarize arg_max(TimeGenerated, *) by DeviceId\n| where array_length(EnabledRoutes) > 0\n| project DeviceName, User, Os, EnabledRoutes=tostring(EnabledRoutes), AdvertisedRoutes=tostring(AdvertisedRoutes), LastSeen\n| order by LastSeen desc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Routes currently being served from devices", + "gridSettings": { + "formatters": [ + { + "columnMatch": "LastSeen", + "formatter": 6 + } + ] + }, + "noDataMessage": "No subnet routers active in this tailnet.", + "noDataMessageStyle": 1 + }, + "name": "q-61a792cd" + } + ] + }, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "network" + }, + "name": "group-network" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "
Ingest rate per table
" + }, + "name": "div-ingest-rate-per-tabl-24e5f6" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "union withsource=Table Tailscale_Audit_CL, Tailscale_Devices_CL, Tailscale_Users_CL, Tailscale_Keys_CL, Tailscale_Dns_CL, Tailscale_Settings_CL\n| where TimeGenerated > ago(24h)\n| summarize Rows=count() by Table, bin(TimeGenerated, 1h)\n| order by TimeGenerated asc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "timechart", + "title": "Rows ingested per Tailscale table per hour (last 24h)", + "noDataMessage": "No Tailscale data ingested in the last 24h - check the connector card under Sentinel Data Connectors.", + "noDataMessageStyle": 5 + }, + "name": "q-c6fd0143" + }, + { + "type": 1, + "content": { + "json": "
Last poll time per table
" + }, + "name": "div-last-poll-time-per-t-49b051" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "union withsource=Table Tailscale_Audit_CL, Tailscale_Devices_CL, Tailscale_Users_CL, Tailscale_Keys_CL, Tailscale_Dns_CL, Tailscale_Settings_CL\n| summarize LastRow=max(TimeGenerated), TotalRows=count() by Table\n| extend MinutesAgo=toint((now() - LastRow) / 1m)\n| extend Status=case(MinutesAgo < 60, \"Fresh\", MinutesAgo < 360, \"Recent\", MinutesAgo < 1440, \"Stale\", \"Very Stale\")\n| project Table, LastRow, MinutesAgo, TotalRows, Status\n| order by MinutesAgo asc", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Per-table freshness", + "gridSettings": { + "formatters": [ + { + "columnMatch": "LastRow", + "formatter": 6 + }, + { + "columnMatch": "MinutesAgo", + "formatter": 8, + "formatOptions": { + "palette": "redBright" + } + }, + { + "columnMatch": "TotalRows", + "formatter": 8, + "formatOptions": { + "palette": "blue" + } + }, + { + "columnMatch": "Status", + "formatter": 1 + } + ] + } + }, + "name": "q-a02e37a8" + }, + { + "type": 1, + "content": { + "json": "
Log Analytics operational events
" + }, + "name": "div-log-analytics-operat-630749" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "_LogOperation\n| where TimeGenerated > ago(24h)\n| where _ResourceId contains \"tailscale\" or Detail contains \"Tailscale_\"\n| project TimeGenerated, Operation, Level, Detail\n| order by TimeGenerated desc | take 100", + "size": 0, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "title": "Log Analytics operational events touching Tailscale tables", + "gridSettings": { + "formatters": [ + { + "columnMatch": "TimeGenerated", + "formatter": 6 + }, + { + "columnMatch": "Level", + "formatter": 1 + } + ] + }, + "noDataMessage": "No operational issues recorded in the last 24h.", + "noDataMessageStyle": 1 + }, + "name": "q-b492f150" + } + ] + }, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "pipeline" + }, + "name": "group-pipeline" + }, + { + "type": 1, + "content": { + "json": "
Tailscale Operations (Standard) (CCF) - Microsoft Sentinel content from the Tailscale (CCF) solution, Standard-tier surface. Tables polled from the Tailscale REST API: audit, devices, users, keys, dns, settings. Filter every panel via the time range above; the Investigate tab adds Actor and Device pickers for drilldown. For network flow logs and posture integrations, install the companion Tailscale Operations (Premium) workbook on a Premium / Enterprise tailnet.
" + }, + "name": "footer" + } + ], + "fallbackResourceIds": [ + "Azure Monitor" + ], + "fromTemplateId": "sentinel-Tailscale-CCF", + "$schema": "https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json" +} \ No newline at end of file diff --git a/Workbooks/WorkbooksMetadata.json b/Workbooks/WorkbooksMetadata.json index 143ab9dc8e7..5e23a612b73 100644 --- a/Workbooks/WorkbooksMetadata.json +++ b/Workbooks/WorkbooksMetadata.json @@ -329,7 +329,7 @@ "tier": "Community" }, "author": { - "name": "Tomáš Kubica" + "name": "Tom\u00e1\u0161 Kubica" }, "source": { "kind": "Community" @@ -5184,7 +5184,7 @@ { "workbookKey": "ThreatAnalysis&Response", "logoFileName": "Azure_Sentinel.svg", - "description": "The Defenders for IoT workbook provide guided investigations for OT entities based on open incidents, alert notifications, and activities for OT assets. They also provide a hunting experience across the MITRE ATT&CK® framework for ICS, and are designed to enable analysts, security engineers, and MSSPs to gain situational awareness of OT security posture.", + "description": "The Defenders for IoT workbook provide guided investigations for OT entities based on open incidents, alert notifications, and activities for OT assets. They also provide a hunting experience across the MITRE ATT&CK\u00ae framework for ICS, and are designed to enable analysts, security engineers, and MSSPs to gain situational awareness of OT security posture.", "dataTypesDependencies": [ "SecurityAlert" ], @@ -6392,7 +6392,7 @@ { "workbookKey": "MicrosoftExchangeSearchAdminAuditLog", "logoFileName": "Azure_Sentinel.svg", - "description": "This workbook is dedicated to On-Premises Exchange organizations. It uses the MSExchange Management event logs to give you a simple way to view administrators’ activities in your Exchange environment with Cmdlets usage statistics and multiple pivots to understand who and/or what is affected to modifications on your environment. Required Data Connector: Exchange Audit Event logs via Legacy Agent.", + "description": "This workbook is dedicated to On-Premises Exchange organizations. It uses the MSExchange Management event logs to give you a simple way to view administrators\u2019 activities in your Exchange environment with Cmdlets usage statistics and multiple pivots to understand who and/or what is affected to modifications on your environment. Required Data Connector: Exchange Audit Event logs via Legacy Agent.", "dataTypesDependencies": [ "ESIExchangeConfig_CL" ], @@ -6413,7 +6413,7 @@ { "workbookKey": "MicrosoftExchangeSearchAdminAuditLog-Online", "logoFileName": "Azure_Sentinel.svg", - "description": "This workbook is dedicated to Online Exchange organizations. It uses the Office Activity logs to give you a simple way to view administrators’ activities in your Exchange environment with Cmdlets usage statistics and multiple pivots to understand who and/or what is affected to modifications on your environment. Required Data Connector: Microsoft 365 (Exchange).", + "description": "This workbook is dedicated to Online Exchange organizations. It uses the Office Activity logs to give you a simple way to view administrators\u2019 activities in your Exchange environment with Cmdlets usage statistics and multiple pivots to understand who and/or what is affected to modifications on your environment. Required Data Connector: Microsoft 365 (Exchange).", "dataTypesDependencies": [ "OfficeActivity" ], @@ -6935,7 +6935,7 @@ { "workbookKey": "MicrosoftSentinelCostGBP", "logoFileName": "Azure_Sentinel.svg", - "description": "This workbook provides an estimated cost in GBP (£) across the main billed items in Microsoft Sentinel: ingestion, retention and automation. It also provides insight about the possible impact of the Microsoft 365 E5 offer.", + "description": "This workbook provides an estimated cost in GBP (\u00a3) across the main billed items in Microsoft Sentinel: ingestion, retention and automation. It also provides insight about the possible impact of the Microsoft 365 E5 offer.", "dataTypesDependencies": [], "dataConnectorsDependencies": [], "previewImagesFileNames": [ @@ -7091,7 +7091,7 @@ { "workbookKey": "MicrosoftSentinelCostEUR", "logoFileName": "Azure_Sentinel.svg", - "description": "This workbook provides an estimated cost in EUR (€) across the main billed items in Microsoft Sentinel: ingestion, retention and automation. It also provides insight about the possible impact of the Microsoft 365 E5 offer.", + "description": "This workbook provides an estimated cost in EUR (\u20ac) across the main billed items in Microsoft Sentinel: ingestion, retention and automation. It also provides insight about the possible impact of the Microsoft 365 E5 offer.", "dataTypesDependencies": [], "dataConnectorsDependencies": [], "previewImagesFileNames": [ @@ -7607,7 +7607,7 @@ { "workbookKey": "InsiderRiskManagementWorkbook", "logoFileName": "Azure_Sentinel.svg", - "description": "The Microsoft Insider Risk Management Workbook integrates telemetry from 25+ Microsoft security products to provide actionable insights into insider risk management. Reporting tools provide “Go to Alert” links to provide deeper integration between products and a simplified user experience for exploring alerts. ", + "description": "The Microsoft Insider Risk Management Workbook integrates telemetry from 25+ Microsoft security products to provide actionable insights into insider risk management. Reporting tools provide \u201cGo to Alert\u201d links to provide deeper integration between products and a simplified user experience for exploring alerts. ", "dataTypesDependencies": [ "SigninLogsSigninLogs", "AuditLogs", @@ -10354,9 +10354,7 @@ "SentinelBehaviorInfo", "SentinelBehaviorEntities" ], - "dataConnectorsDependencies": [ - - ], + "dataConnectorsDependencies": [], "previewImagesFileNames": [ "UEBABehaviorsAnalysisWorkbookBlack1.png", "UEBABehaviorsAnalysisWorkbookBlack2.png", @@ -10389,70 +10387,72 @@ "provider": "KnowBe4" }, { - "workbookKey": "VTIFeedDashboard", - "logoFileName": "Visa_VTI_Logo.svg", - "description": "Visa Threat Intelligence Feed Dashboard", - "dataTypesDependencies": [ "VisaThreatIntelligenceIOC_CL" ], - "dataConnectorsDependencies": [ - "VisaThreatIntelligence" - ], - "previewImagesFileNames": [ - "VTIOverview_black.png", - "VTIOverview_white.png" - ], - "version": "1.0", - "title": "Visa Threat Intelligence Feed Overview", - "templateRelativePath": "VTI_IOC_Feed.json", - "subtitle": "", - "provider": "Visa" - }, - { - "workbookKey": "Censys", - "logoFileName": "Censys.svg", - "description": "This Workbook provides immediate insight into the data coming from Censys.", - "dataTypesDependencies": [ - "Censys_Host_Services_CL", - "Censys_Host_IOC_CL", - "Censys_Web_Property_IOC_CL", - "Censys_Web_Property_Vuln_CL", - "Censys_Web_Property_Endpoint_CL", - "Censys_Web_Property_Threat_CL", - "Censys_Certificate_IOC_Temp_CL", - "CensysHost_CL", - "Censyswebproperty_CL", - "CensysWebProperty_CL", - "CensysCert_CL", - "CensysCertificate_CL", - "CensysHostAlert_CL", - "CensysCertificateAlert_CL", - "CensysWebPropertyAlert_CL", - "CensysRescanHost_CL", - "CensysRescanWebProperty_CL", - "CensysRescanHostAlert_CL", - "CensysRescanWebPropertyAlert_CL", - "CensysRelatedInfrastructure_CL", - "Censys_Host_History_Data_CL", - "Incident_Enrich_Data_CL" - ], - "previewImagesFileNames": [ - "CensysWhite1.png", - "CensysWhite2.png", - "CensysWhite3.png", - "CensysWhite4.png", - "CensysWhite5.png", - "CensysWhite6.png", - "CensysBlack1.png", - "CensysBlack2.png", - "CensysBlack3.png", - "CensysBlack4.png", - "CensysBlack5.png", - "CensysBlack6.png" - ], - "version": "1.0", - "title": "Censys", - "templateRelativePath": "Censys.json", - "subtitle": "", - "provider": "Censys" + "workbookKey": "VTIFeedDashboard", + "logoFileName": "Visa_VTI_Logo.svg", + "description": "Visa Threat Intelligence Feed Dashboard", + "dataTypesDependencies": [ + "VisaThreatIntelligenceIOC_CL" + ], + "dataConnectorsDependencies": [ + "VisaThreatIntelligence" + ], + "previewImagesFileNames": [ + "VTIOverview_black.png", + "VTIOverview_white.png" + ], + "version": "1.0", + "title": "Visa Threat Intelligence Feed Overview", + "templateRelativePath": "VTI_IOC_Feed.json", + "subtitle": "", + "provider": "Visa" + }, + { + "workbookKey": "Censys", + "logoFileName": "Censys.svg", + "description": "This Workbook provides immediate insight into the data coming from Censys.", + "dataTypesDependencies": [ + "Censys_Host_Services_CL", + "Censys_Host_IOC_CL", + "Censys_Web_Property_IOC_CL", + "Censys_Web_Property_Vuln_CL", + "Censys_Web_Property_Endpoint_CL", + "Censys_Web_Property_Threat_CL", + "Censys_Certificate_IOC_Temp_CL", + "CensysHost_CL", + "Censyswebproperty_CL", + "CensysWebProperty_CL", + "CensysCert_CL", + "CensysCertificate_CL", + "CensysHostAlert_CL", + "CensysCertificateAlert_CL", + "CensysWebPropertyAlert_CL", + "CensysRescanHost_CL", + "CensysRescanWebProperty_CL", + "CensysRescanHostAlert_CL", + "CensysRescanWebPropertyAlert_CL", + "CensysRelatedInfrastructure_CL", + "Censys_Host_History_Data_CL", + "Incident_Enrich_Data_CL" + ], + "previewImagesFileNames": [ + "CensysWhite1.png", + "CensysWhite2.png", + "CensysWhite3.png", + "CensysWhite4.png", + "CensysWhite5.png", + "CensysWhite6.png", + "CensysBlack1.png", + "CensysBlack2.png", + "CensysBlack3.png", + "CensysBlack4.png", + "CensysBlack5.png", + "CensysBlack6.png" + ], + "version": "1.0", + "title": "Censys", + "templateRelativePath": "Censys.json", + "subtitle": "", + "provider": "Censys" }, { "workbookKey": "NetskopeWebTransactionsWorkbook", @@ -10466,13 +10466,13 @@ "NetskopeWebtxOverviewWhite01.png", "NetskopeWebtxOverviewBlack02.png", "NetskopeWebtxOverviewWhite02.png" - ], + ], "version": "1.0.0", "title": "Netskope Web Transactions", "templateRelativePath": "NetskopeWebTx_Workbook.json", "subtitle": "Web Traffic Analysis and Security Monitoring", "provider": "Netskope" -}, + }, { "workbookKey": "MicrosoftCopilotActivityMonitoring", "logoFileName": "Copilot_logo.svg", @@ -10493,22 +10493,22 @@ "subtitle": "", "provider": "Microsoft Sentinel Community" }, - { + { "workbookKey": "ExtraHopDetectionsOverview", "logoFileName": "ExtraHop.svg", "description": "This workbook provides immediate insight into detection data ingested from ExtraHop.", "dataTypesDependencies": [ "ExtraHop_Detections_CL" - ], + ], "dataConnectorsDependencies": [ "ExtraHopDataConnector" - ], + ], "previewImagesFileNames": [ "ExtraHopDetectionsOverviewBlack1.png", "ExtraHopDetectionsOverviewWhite1.png", - "ExtraHopDetectionsOverviewBlack2.png", - "ExtraHopDetectionsOverviewWhite2.png" - ], + "ExtraHopDetectionsOverviewBlack2.png", + "ExtraHopDetectionsOverviewWhite2.png" + ], "version": "1.0.0", "title": "ExtraHop Detections Overview", "templateRelativePath": "ExtraHopDetectionsOverview.json", @@ -10609,44 +10609,6 @@ "provider": "archTIS" }, { - "workbookKey": "UnifiSiteManager", - "logoFileName": "UnifiSiteManager.svg", - "description": "The UniFi Site Manager workbook provides comprehensive visibility into your UniFi network infrastructure. Monitor device status, host connections, site health, and ISP performance metrics across all your UniFi deployments.", - "dataTypesDependencies": [ - "Unifi_SiteManager_Devices_CL", - "Unifi_SiteManager_Hosts_CL", - "Unifi_SiteManager_Sites_CL", - "Unifi_SiteManager_ISPMetrics_CL" - ], - "dataConnectorsDependencies": [ - "UniFiSiteManagerConnectorDefinition" - ], - "previewImagesFileNames": [ - "UnifiSiteManagerBlack1.png", - "UnifiSiteManagerBlack2.png", - "UnifiSiteManagerBlack3.png", - "UnifiSiteManagerWhite1.png", - "UnifiSiteManagerWhite2.png", - "UnifiSiteManagerWhite3.png" - ], - "version": "1.0.0", - "title": "UniFi Site Manager Network Dashboard", - "templateRelativePath": "UnifiSiteManager.json", - "subtitle": "", - "provider": "Community", - "support": { - "tier": "Community" - }, - "source": { - "kind": "Solution", - "name": "UniFi Site Manager (CCF)" - }, - "categories": { - "domains": [ - "Networking", - "Security - Network" - ] - }, "workbookKey": "stealthTalkAnomalousAuthMonitor", "logoFileName": "st-ms-def-hub.svg", "description": "The StealthTalk Anomalous Auth Monitor workbook provides a real-time SOC dashboard for the four classes of anomalous authentication events forwarded by StealthTalk into Microsoft Sentinel. Across 17 panels organised into Overview, Off-Hours, New Devices, Geo Anomaly and Brute Force sections, the workbook surfaces a composite User Risk Leaderboard (High=10 / Medium=5 / Low=1 per event), a Multi-Vector Correlation view that flags users triggering 2 or more anomaly types simultaneously, an interactive World Map of anomalous login locations, and chronological raw-event logs for incident investigation. Three global filters (Time Range, Severity and User ID) scope every panel.", @@ -10664,31 +10626,31 @@ "StealthTalkDataConnector_black.png" ], "version": "1.0.0", - "lastPublishDate": "2026-05-19", "title": "StealthTalk Anomalous Auth Monitor", "templateRelativePath": "StealthTalkAnomalousAuthMonitor.json", "subtitle": "", "provider": "StealthTalk", - "source": { - "kind": "Solution", - "name": "StealthTalk Anomalous Authentication" - }, - "author": { - "name": "StealthTalk", - "email": "support@stealthtalk.com" - }, "support": { "name": "StealthTalk Support", "email": "support@stealthtalk.com", "tier": "Partner", "link": "https://stealthtalk.com/support" }, + "source": { + "kind": "Solution", + "name": "StealthTalk Anomalous Authentication" + }, "categories": { "domains": [ "Security - Threat Protection", "Identity", "Security - Insider Threat" ] + }, + "lastPublishDate": "2026-05-19", + "author": { + "name": "StealthTalk", + "email": "support@stealthtalk.com" } }, { @@ -10708,26 +10670,6 @@ "provider": "AWS Security Hub" }, { - "workbookKey": "ESKMworkbook", - "logoFileName": "UtimacoLogoSVG.svg", - "description": "Gain insights into Utimaco Enterprise Secure Key Manager (ESKM) KMIP server activity. This workbook visualizes authentication events, key management operations, client IPs, and operation outcomes to help detect anomalies, misuse, and configuration issues.", - "dataTypesDependencies": [ - "UtimacoESKMKmipServerLogs_CL" - ], - "dataConnectorsDependencies": [ - "UtimacoESKMConnector" - ], - "previewImagesFileNames": [ - "UtimacoESKMBlack1.png", - "UtimacoESKMBlack2.png", - "UtimacoESKMWhite1.png", - "UtimacoESKMWhite2.png" - ], - "version": "1.0.0", - "title": "Utimaco Enterprise Secure Key Manager", - "templateRelativePath": "ESKMworkbook.json", - "subtitle": "", - "provider": "Utimaco", "workbookKey": "GuardicoreInfoWorkbook", "logoFileName": "akamai-guardicore.svg", "description": "Gain insights into Guardicore workload protection coverage, policy enforcement effectiveness, and security posture. This workbook provides visibility into protected workloads, policy rules by action type, blocking rulesets, and application status trends over time.", @@ -10769,5 +10711,87 @@ "templateRelativePath": "GuardicoreIncident.json", "subtitle": "", "provider": "Akamai Guardicore" + }, + { + "workbookKey": "TailscaleStandardOperationsWorkbook", + "logoFileName": "Tailscale.svg", + "description": "Tailscale Operations workbook for Standard tier. Nine tabs covering an at-a-glance KPI hero row, audit activity overview, an actor + device drilldown (Investigate), embedded hunting queries (first-seen actors, off-hours changes, key-expiry-disabled devices, never-expire auth keys, outdated clients, dormant devices, subnet route exposure, SSH-enabled devices), identity (user roles, status, last login recency, role escalation history, orphaned users), devices (OS / version / tag distribution, devices needing attention, full inventory, subnet routers), credentials (expiry timeline, never-expire flag, CRUD events), admin audit (action heatmap, actor x action heatmap, recent 100), network and DNS (current snapshot, tailnet policy gates, ACL change history), and pipeline health (per-table freshness, ingest rate, operational events). Driven by data polled from the Tailscale REST API.", + "dataTypesDependencies": [ + "Tailscale_Audit_CL", + "Tailscale_Devices_CL", + "Tailscale_Users_CL", + "Tailscale_Keys_CL", + "Tailscale_Webhooks_CL", + "Tailscale_Settings_CL", + "Tailscale_Dns_CL" + ], + "dataConnectorsDependencies": [ + "TailscaleCCF" + ], + "previewImagesFileNames": [], + "version": "1.0.0", + "title": "Tailscale Operations (Standard)", + "templateRelativePath": "TailscaleStandardOperations.json", + "subtitle": "", + "provider": "Community", + "support": { + "tier": "Community" + }, + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)" + }, + "categories": { + "domains": [ + "Networking", + "Security - Network", + "Identity" + ] + }, + "author": { + "name": "noodlemctwoodle" + } + }, + { + "workbookKey": "TailscalePremiumOperationsWorkbook", + "logoFileName": "Tailscale.svg", + "description": "Tailscale Operations workbook for Premium / Enterprise tier - everything in the Standard workbook plus network flow analysis (top talkers, src-dst pairs, exit-node egress, beaconing candidates) and posture integration inventory.", + "dataTypesDependencies": [ + "Tailscale_Audit_CL", + "Tailscale_Devices_CL", + "Tailscale_Users_CL", + "Tailscale_Keys_CL", + "Tailscale_Webhooks_CL", + "Tailscale_Settings_CL", + "Tailscale_Dns_CL", + "Tailscale_Network_CL", + "Tailscale_PostureIntegrations_CL" + ], + "dataConnectorsDependencies": [ + "TailscalePremiumCCF" + ], + "previewImagesFileNames": [], + "version": "1.0.0", + "title": "Tailscale Operations (Premium)", + "templateRelativePath": "TailscalePremiumOperations.json", + "subtitle": "", + "provider": "Community", + "support": { + "tier": "Community" + }, + "source": { + "kind": "Solution", + "name": "Tailscale (CCF)" + }, + "categories": { + "domains": [ + "Networking", + "Security - Network", + "Identity" + ] + }, + "author": { + "name": "noodlemctwoodle" + } } ]