Skip to content

KQL Detection Templates & Playbook Examples

Table of Contents

  1. Detection Templates by Category
  2. Playbook Templates
  3. Automation Rules
  4. Watchlist Integration

Detection Templates by Category

Identity-Based Detections

Failed MFA Attempts Followed by Success

// Detection: MFA Bypass Attempt
// MITRE: Credential Access - T1111 Multi-Factor Authentication Interception
// Description: Detects when multiple MFA failures are followed by success
// Severity: High

let mfaFailureWindow = 10m;
let mfaFailureThreshold = 3;

// Get MFA failures
let mfaFailures = SigninLogs
| where TimeGenerated > ago(1d)
| where ResultType in ("50074", "50076", "500121")  // MFA required/not completed
| summarize 
    MFAFailures = count(),
    FailureStart = min(TimeGenerated),
    FailureEnd = max(TimeGenerated)
    by UserPrincipalName, CorrelationId;

// Get successful sign-ins after MFA
let successfulSignins = SigninLogs
| where TimeGenerated > ago(1d)
| where ResultType == "0"
| where AuthenticationRequirement == "multiFactorAuthentication";

// Correlate failures followed by success
mfaFailures
| where MFAFailures >= mfaFailureThreshold
| join kind=inner (
    successfulSignins
    | project 
        SuccessTime = TimeGenerated,
        UserPrincipalName,
        IPAddress,
        DeviceDetail,
        Location
) on UserPrincipalName
| where SuccessTime between (FailureEnd .. (FailureEnd + mfaFailureWindow))
| project 
    TimeGenerated = SuccessTime,
    UserPrincipalName,
    MFAFailuresBeforeSuccess = MFAFailures,
    TimeToSuccess = SuccessTime - FailureEnd,
    IPAddress,
    Location

Privileged Account Sign-in from New Location

// Detection: Admin Sign-in from New Location
// MITRE: Initial Access - T1078.004 Cloud Accounts
// Description: Detects privileged accounts signing in from previously unseen locations
// Severity: Medium

let lookback = 30d;
let adminUsers = dynamic(["admin@contoso.com", "globaladmin@contoso.com"]);

// Alternatively, query admin users dynamically
// let adminUsers = IdentityInfo
// | where AssignedRoles has_any ("Global Administrator", "Privileged Role Administrator")
// | distinct AccountUPN;

// Get historical locations for admin users
let historicalLocations = SigninLogs
| where TimeGenerated between (ago(lookback) .. ago(1d))
| where UserPrincipalName in (adminUsers)
| where ResultType == "0"
| distinct UserPrincipalName, tostring(LocationDetails.countryOrRegion);

// Detect new locations
SigninLogs
| where TimeGenerated > ago(1d)
| where UserPrincipalName in (adminUsers)
| where ResultType == "0"
| extend Country = tostring(LocationDetails.countryOrRegion)
| join kind=leftanti historicalLocations 
    on UserPrincipalName, $left.Country == $right.Column1
| project 
    TimeGenerated,
    UserPrincipalName,
    NewCountry = Country,
    City = tostring(LocationDetails.city),
    IPAddress,
    AppDisplayName,
    DeviceDetail

Service Principal Secret Added

// Detection: Service Principal Secret/Certificate Added
// MITRE: Persistence - T1098.001 Additional Cloud Credentials
// Description: Detects when new credentials are added to service principals
// Severity: High

AuditLogs
| where TimeGenerated > ago(1d)
| where OperationName in (
    "Add service principal credentials",
    "Update application - Certificates and secrets management"
)
| extend 
    InitiatedByUPN = tostring(InitiatedBy.user.userPrincipalName),
    InitiatedByApp = tostring(InitiatedBy.app.displayName),
    TargetAppName = tostring(TargetResources[0].displayName),
    TargetAppId = tostring(TargetResources[0].id)
| extend 
    ModifiedProperties = TargetResources[0].modifiedProperties
| mv-expand ModifiedProperties
| where ModifiedProperties.displayName in ("KeyDescription", "FederatedIdentityCredentials")
| project 
    TimeGenerated,
    InitiatedBy = coalesce(InitiatedByUPN, InitiatedByApp),
    OperationName,
    TargetAppName,
    TargetAppId,
    CredentialType = tostring(ModifiedProperties.displayName),
    CorrelationId

Endpoint-Based Detections

Living Off the Land Binaries (LOLBins)

// Detection: Suspicious LOLBin Execution
// MITRE: Defense Evasion - T1218 System Binary Proxy Execution
// Description: Detects suspicious use of legitimate Windows binaries
// Severity: Medium

let lolbins = dynamic([
    "certutil.exe", "mshta.exe", "regsvr32.exe", "rundll32.exe",
    "wscript.exe", "cscript.exe", "msiexec.exe", "installutil.exe",
    "regasm.exe", "regsvcs.exe", "msconfig.exe", "wmic.exe",
    "msbuild.exe", "odbcconf.exe", "ieexec.exe", "cmstp.exe"
]);

let suspiciousPatterns = dynamic([
    "http://", "https://", "ftp://", "\\\\", 
    "-decode", "-urlcache", "-encode",
    "downloadstring", "downloadfile", "webclient",
    "/i:http", "scrobj.dll"
]);

DeviceProcessEvents
| where TimeGenerated > ago(1d)
| where FileName in~ (lolbins)
| where ProcessCommandLine has_any (suspiciousPatterns)
| extend 
    SuspiciousIndicators = extract_all(@"(https?://[^\s]+|\\\\[^\s]+|-decode|-encode|downloadstring|webclient)", ProcessCommandLine)
| where array_length(SuspiciousIndicators) > 0
| project 
    TimeGenerated,
    DeviceName,
    AccountName,
    FileName,
    ProcessCommandLine,
    InitiatingProcessFileName,
    InitiatingProcessCommandLine,
    FolderPath,
    SHA256,
    SuspiciousIndicators

Credential Dumping Indicators

// Detection: Credential Dumping Activity
// MITRE: Credential Access - T1003 OS Credential Dumping
// Description: Detects attempts to dump credentials from memory
// Severity: High

let credentialDumpingIndicators = dynamic([
    "sekurlsa", "logonpasswords", "lsadump", "kerberos::list",
    "procdump", "-ma lsass", "comsvcs.dll", "MiniDump",
    "ntdsutil", "vssadmin", "shadow", "ntds.dit",
    "reg save", "HKLM\\SAM", "HKLM\\SECURITY", "HKLM\\SYSTEM"
]);

union
// Process-based detection
(
    DeviceProcessEvents
    | where TimeGenerated > ago(1d)
    | where ProcessCommandLine has_any (credentialDumpingIndicators)
    | extend DetectionType = "Process Command Line"
),
// File-based detection (known tools)
(
    DeviceFileEvents
    | where TimeGenerated > ago(1d)
    | where FileName in~ ("mimikatz.exe", "procdump.exe", "gsecdump.exe", "wce.exe")
        or SHA256 in (
            "..." // Add known bad hashes
        )
    | extend DetectionType = "Known Tool File"
),
// LSASS access detection
(
    DeviceProcessEvents
    | where TimeGenerated > ago(1d)
    | where InitiatingProcessFileName !in~ ("csrss.exe", "smss.exe", "wininit.exe")
    | where FileName =~ "lsass.exe" or ProcessCommandLine contains "lsass"
    | extend DetectionType = "LSASS Access"
)
| project 
    TimeGenerated,
    DeviceName,
    AccountName,
    DetectionType,
    FileName,
    ProcessCommandLine,
    InitiatingProcessFileName,
    FolderPath

PowerShell Obfuscation Detection

// Detection: Obfuscated PowerShell
// MITRE: Defense Evasion - T1027 Obfuscated Files or Information
// Description: Detects obfuscated PowerShell commands
// Severity: Medium

DeviceProcessEvents
| where TimeGenerated > ago(1d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| extend 
    CommandLength = strlen(ProcessCommandLine),
    // Count obfuscation indicators
    BacktickCount = countof(ProcessCommandLine, "`"),
    CaretCount = countof(ProcessCommandLine, "^"),
    SpecialCharRatio = (
        countof(ProcessCommandLine, "+") +
        countof(ProcessCommandLine, "$") +
        countof(ProcessCommandLine, "{") +
        countof(ProcessCommandLine, "[")
    ) * 1.0 / CommandLength,
    // Check for encoding
    HasEncodedCommand = ProcessCommandLine has_any ("-enc", "-encodedcommand", "-e ", "-ec "),
    // Check for common bypass techniques
    HasBypass = ProcessCommandLine has_any ("-bypass", "-nop", "-noprofile", "-w hidden", "-windowstyle hidden")
| where 
    (BacktickCount > 5 and CommandLength > 100) or
    (CaretCount > 5 and CommandLength > 100) or
    (SpecialCharRatio > 0.1 and CommandLength > 200) or
    (HasEncodedCommand and HasBypass)
| project 
    TimeGenerated,
    DeviceName,
    AccountName,
    CommandLength,
    ObfuscationIndicators = pack(
        "Backticks", BacktickCount,
        "Carets", CaretCount,
        "SpecialCharRatio", round(SpecialCharRatio, 3),
        "HasEncoding", HasEncodedCommand,
        "HasBypass", HasBypass
    ),
    ProcessCommandLine,
    InitiatingProcessFileName

Network-Based Detections

DNS Tunneling Detection

// Detection: Potential DNS Tunneling
// MITRE: Command and Control - T1071.004 DNS
// Description: Detects potential DNS tunneling based on query characteristics
// Severity: Medium

DnsEvents
| where TimeGenerated > ago(1d)
| where QueryType in ("TXT", "NULL", "CNAME")  // Common tunneling types
| extend 
    SubdomainLength = strlen(tostring(split(Name, ".")[0])),
    DomainDepth = array_length(split(Name, ".")),
    HasBase64Pattern = Name matches regex @"[A-Za-z0-9+/]{20,}",
    QueryLength = strlen(Name)
| where 
    SubdomainLength > 30 or
    DomainDepth > 5 or
    HasBase64Pattern or
    QueryLength > 100
| summarize 
    SuspiciousQueries = count(),
    UniqueSubdomains = dcount(tostring(split(Name, ".")[0])),
    AvgSubdomainLength = avg(SubdomainLength),
    MaxSubdomainLength = max(SubdomainLength),
    SampleQueries = make_set(Name, 5)
    by Computer, tostring(split(Name, ".")[-2]) + "." + tostring(split(Name, ".")[-1])
| where SuspiciousQueries > 50 or UniqueSubdomains > 20
| project 
    TimeGenerated = now(),
    SourceHost = Computer,
    SuspectedTunnelingDomain = Column1,
    SuspiciousQueryCount = SuspiciousQueries,
    UniqueSubdomains,
    AvgSubdomainLength = round(AvgSubdomainLength, 1),
    SampleQueries

Beaconing Detection

// Detection: C2 Beaconing Activity
// MITRE: Command and Control - T1071 Application Layer Protocol
// Description: Detects regular periodic connections indicating beaconing
// Severity: High

let minConnections = 20;
let maxJitterPercent = 15;

// For Defender for Endpoint
DeviceNetworkEvents
| where TimeGenerated > ago(1d)
| where RemoteIPType == "Public"
| where ActionType == "ConnectionSuccess"
| summarize 
    Connections = count(),
    Timestamps = make_list(TimeGenerated, 1000)
    by DeviceName, RemoteIP, RemoteUrl, RemotePort
| where Connections >= minConnections
| mv-apply Timestamps to typeof(datetime) on (
    order by Timestamps asc
    | extend 
        PrevTimestamp = prev(Timestamps),
        Interval = datetime_diff('second', Timestamps, prev(Timestamps))
    | where isnotnull(PrevTimestamp)
    | summarize 
        AvgInterval = avg(Interval),
        StdDevInterval = stdev(Interval),
        MinInterval = min(Interval),
        MaxInterval = max(Interval)
)
| extend 
    JitterPercent = (StdDevInterval / AvgInterval) * 100
| where JitterPercent < maxJitterPercent  // Low jitter = regular beaconing
| where AvgInterval between (10 .. 3600)  // 10 seconds to 1 hour intervals
| project 
    TimeGenerated = now(),
    DeviceName,
    RemoteIP,
    RemoteUrl,
    RemotePort,
    ConnectionCount = Connections,
    AvgIntervalSeconds = round(AvgInterval, 0),
    JitterPercent = round(JitterPercent, 1),
    BeaconingScore = 100 - JitterPercent
| order by BeaconingScore desc

Cloud-Based Detections

Suspicious Azure Resource Deployment

// Detection: Cryptocurrency Mining Resource Deployment
// MITRE: Impact - T1496 Resource Hijacking
// Description: Detects deployment of high-compute resources potentially for mining
// Severity: High

AzureActivity
| where TimeGenerated > ago(1d)
| where OperationNameValue has "Microsoft.Compute/virtualMachines/write"
| where ActivityStatusValue == "Success"
| extend 
    ResourceDetails = parse_json(Properties)
| extend 
    VMSize = tostring(ResourceDetails.responseBody.properties.hardwareProfile.vmSize),
    Location = tostring(ResourceDetails.responseBody.location)
| where VMSize has_any (
    "Standard_NC", "Standard_ND", "Standard_NV",  // GPU VMs
    "Standard_H", "Standard_HB", "Standard_HC",    // High-performance compute
    "Standard_F72", "Standard_F64", "Standard_E64" // Large compute
)
// Check for unusual deployment patterns
| summarize 
    DeploymentCount = count(),
    VMSizes = make_set(VMSize),
    Locations = make_set(Location),
    ResourceGroups = make_set(ResourceGroup)
    by Caller, CallerIpAddress, bin(TimeGenerated, 1h)
| where DeploymentCount > 3  // Multiple high-compute VMs in 1 hour
| project 
    TimeGenerated,
    DeployedBy = Caller,
    SourceIP = CallerIpAddress,
    HighComputeVMsDeployed = DeploymentCount,
    VMSizes,
    Locations,
    ResourceGroups

Storage Account Anonymous Access Enabled

// Detection: Storage Account Public Access Enabled
// MITRE: Defense Evasion - T1562 Impair Defenses
// Description: Detects when storage accounts are configured for public access
// Severity: High

AzureActivity
| where TimeGenerated > ago(1d)
| where OperationNameValue == "Microsoft.Storage/storageAccounts/write"
| where ActivityStatusValue == "Success"
| extend 
    Properties = parse_json(Properties_d)
| extend 
    AllowBlobPublicAccess = tostring(Properties.requestbody.properties.allowBlobPublicAccess),
    PublicNetworkAccess = tostring(Properties.requestbody.properties.publicNetworkAccess)
| where AllowBlobPublicAccess == "true" or PublicNetworkAccess == "Enabled"
| project 
    TimeGenerated,
    StorageAccount = Resource,
    ResourceGroup,
    ModifiedBy = Caller,
    SourceIP = CallerIpAddress,
    AllowBlobPublicAccess,
    PublicNetworkAccess,
    SubscriptionId

Playbook Templates

Incident Enrichment Playbook

{
    "definition": {
        "triggers": {
            "Microsoft_Sentinel_incident": {
                "type": "ApiConnectionWebhook",
                "inputs": {
                    "host": {
                        "connection": {
                            "name": "@parameters('$connections')['azuresentinel']['connectionId']"
                        }
                    },
                    "body": {
                        "callback_url": "@listCallbackUrl()"
                    },
                    "path": "/incident-creation"
                }
            }
        },
        "actions": {
            "Get_incident": {
                "type": "ApiConnection",
                "inputs": {
                    "host": {
                        "connection": {
                            "name": "@parameters('$connections')['azuresentinel']['connectionId']"
                        }
                    },
                    "method": "get",
                    "path": "/incidents/@{encodeURIComponent(triggerBody()?['properties']?['id'])}"
                }
            },
            "For_each_IP_entity": {
                "type": "Foreach",
                "foreach": "@body('Get_incident')?['properties']?['relatedEntities']",
                "actions": {
                    "Condition_Is_IP": {
                        "type": "If",
                        "expression": {
                            "equals": ["@items('For_each_IP_entity')?['kind']", "Ip"]
                        },
                        "actions": {
                            "Get_IP_reputation": {
                                "type": "Http",
                                "inputs": {
                                    "method": "GET",
                                    "uri": "https://api.abuseipdb.com/api/v2/check",
                                    "headers": {
                                        "Key": "@parameters('AbuseIPDB_ApiKey')"
                                    },
                                    "queries": {
                                        "ipAddress": "@items('For_each_IP_entity')?['properties']?['address']"
                                    }
                                }
                            },
                            "Add_comment_with_IP_intel": {
                                "type": "ApiConnection",
                                "inputs": {
                                    "host": {
                                        "connection": {
                                            "name": "@parameters('$connections')['azuresentinel']['connectionId']"
                                        }
                                    },
                                    "method": "post",
                                    "path": "/incidents/@{encodeURIComponent(triggerBody()?['properties']?['id'])}/comments",
                                    "body": {
                                        "message": "IP Enrichment: @{items('For_each_IP_entity')?['properties']?['address']}\nAbuse Score: @{body('Get_IP_reputation')?['data']?['abuseConfidenceScore']}\nCountry: @{body('Get_IP_reputation')?['data']?['countryCode']}"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

User Containment Playbook

{
    "description": "Disable compromised user account and revoke sessions",
    "actions": [
        {
            "name": "Get_User_Details",
            "type": "Microsoft Graph",
            "action": "GET /users/{userPrincipalName}"
        },
        {
            "name": "Disable_User_Account",
            "type": "Microsoft Graph",
            "action": "PATCH /users/{userPrincipalName}",
            "body": {
                "accountEnabled": false
            }
        },
        {
            "name": "Revoke_User_Sessions",
            "type": "Microsoft Graph",
            "action": "POST /users/{userPrincipalName}/revokeSignInSessions"
        },
        {
            "name": "Send_Teams_Notification",
            "type": "Microsoft Teams",
            "action": "Post message to channel",
            "body": {
                "text": "🚨 User Containment Alert\n\nUser: {userPrincipalName}\nAction: Account disabled and sessions revoked\nIncident: {incidentId}\nTime: {timestamp}"
            }
        },
        {
            "name": "Update_Incident",
            "type": "Azure Sentinel",
            "action": "Add comment",
            "body": {
                "message": "Automated Response: User account disabled and sessions revoked."
            }
        }
    ]
}

Automation Rules

Auto-Assign High Severity Incidents

{
    "name": "Auto-assign Critical Incidents",
    "triggerType": "IncidentCreated",
    "conditions": [
        {
            "property": "Severity",
            "operator": "Equals",
            "values": ["High"]
        }
    ],
    "actions": [
        {
            "actionType": "ModifyProperties",
            "properties": {
                "owner": "soc-tier2@contoso.com",
                "status": "Active"
            }
        },
        {
            "actionType": "RunPlaybook",
            "playbookResourceId": "/subscriptions/.../playbooks/Enrich-Incident"
        }
    ]
}

Auto-Close Known False Positives

{
    "name": "Auto-close Approved Scanning",
    "triggerType": "IncidentCreated",
    "conditions": [
        {
            "property": "Title",
            "operator": "Contains",
            "values": ["Vulnerability Scan Detected"]
        },
        {
            "property": "Entities:IP",
            "operator": "InWatchlist",
            "watchlistName": "ApprovedScanners"
        }
    ],
    "actions": [
        {
            "actionType": "ModifyProperties",
            "properties": {
                "status": "Closed",
                "classification": "BenignPositive",
                "classificationReason": "ConfirmedActivity"
            }
        },
        {
            "actionType": "AddComment",
            "comment": "Auto-closed: Source IP is an approved vulnerability scanner."
        }
    ]
}

Watchlist Integration

Creating a Watchlist for VIP Users

// Query to populate VIP users watchlist
IdentityInfo
| where AssignedRoles has_any ("Global Administrator", "Security Administrator", "Exchange Administrator")
| union (
    // Add C-level executives
    IdentityInfo
    | where JobTitle has_any ("CEO", "CFO", "CTO", "CISO", "COO")
)
| distinct 
    UserPrincipalName,
    DisplayName,
    JobTitle,
    Department = Department,
    VIPReason = case(
        AssignedRoles has "Global Administrator", "Global Admin",
        JobTitle has "CEO", "Executive",
        "High-Value User"
    )

Using Watchlist in Detection

// Detection using VIP watchlist
let VIPUsers = _GetWatchlist('VIPUsers')
| project UserPrincipalName;

SigninLogs
| where TimeGenerated > ago(1d)
| where UserPrincipalName in (VIPUsers)
| where ResultType != "0"  // Failed sign-ins
| summarize 
    FailedAttempts = count(),
    SourceIPs = make_set(IPAddress),
    Locations = make_set(Location)
    by UserPrincipalName
| where FailedAttempts > 5
| project 
    TimeGenerated = now(),
    VIPUser = UserPrincipalName,
    FailedLoginAttempts = FailedAttempts,
    SourceIPs,
    Locations

IP Allow/Block Lists

// Using IP watchlist for exclusions
let AllowedIPs = _GetWatchlist('AllowedIPs')
| project IPAddress;

let BlockedIPs = _GetWatchlist('BlockedIPs')
| project IPAddress;

SigninLogs
| where TimeGenerated > ago(1d)
| where IPAddress !in (AllowedIPs)  // Exclude known-good
| where IPAddress in (BlockedIPs)   // Focus on known-bad
| project 
    TimeGenerated,
    UserPrincipalName,
    BlockedIPAddress = IPAddress,
    Location,
    AppDisplayName

Detection Development Checklist

  • [ ] Define Objective: What attack/behavior are you detecting?
  • [ ] Map to MITRE: Identify tactics, techniques, sub-techniques
  • [ ] Identify Data Sources: Which tables contain relevant data?
  • [ ] Write Initial Query: Start with broad detection
  • [ ] Test with Historical Data: Validate detection works
  • [ ] Tune for False Positives: Add exclusions, adjust thresholds
  • [ ] Configure Entity Mapping: Map accounts, hosts, IPs
  • [ ] Set Severity: Based on impact and confidence
  • [ ] Add Documentation: Description, references, response steps
  • [ ] Create Automation: Playbooks for enrichment/response
  • [ ] Deploy to Production: Enable rule with monitoring
  • [ ] Review and Iterate: Regular tuning based on feedback