Query Details

CSL Zscaler Hunting Queries

Query

// =============================================================================
// CommonSecurityLog - Zscaler ZIA/ZPA Threat Hunting Queries
// Vendor: Zscaler ZIA, Zscaler ZPA
// Generated: 2026-03-18
// Queries: 15 | Table: CommonSecurityLog
// =============================================================================

// =============================================================================
// Q09 - Zscaler ZIA - Blocked Requests by Malicious Category
// MITRE Techniques: T1566
// Tactics        : InitialAccess
// Description    : Surfaces all Zscaler blocked web requests for malicious/botnet/phishing/C2 URL categories.
// =============================================================================
let MaliciousCategories = dynamic([
    "BOTNET_SITES", "MALWARE_SITES", "PHISHING", "SPYWARE_ADWARE_KEYLOGGERS",
    "RANSOMWARE", "C2_SITES", "COMMAND_CONTROL", "SUSPICIOUS_DESTINATIONS",
    "MALICIOUS_LINKS", "ANONYMOUS_PROXY"]);
CommonSecurityLog
| where TimeGenerated > ago(24h)
| where DeviceVendor == "Zscaler"
| where DeviceAction in ("block", "BLOCK", "Blocked", "blocked")
| where DeviceCustomString2 in (MaliciousCategories)
    or DeviceCustomString3 in (MaliciousCategories)
    or RequestContext has_any ("BOTNET", "MALWARE", "PHISHING", "COMMAND_CONTROL", "RANSOMWARE")
| summarize
    BlockCount       = count(),
    UniqueUsers      = dcount(SourceUserName),
    UserList         = make_set(SourceUserName, 20),
    DestinationURLs  = make_set(RequestURL, 20),
    DestIPs          = make_set(DestinationIP, 10),
    Categories       = make_set(DeviceCustomString2, 10),
    FirstSeen        = min(TimeGenerated),
    LastSeen         = max(TimeGenerated)
  by DeviceCustomString2, DeviceCustomString3
| order by BlockCount desc

// =============================================================================
// Q10 - Zscaler ZIA - Shadow IT and File-Sharing Access
// MITRE Techniques: T1567
// Tactics        : Exfiltration
// Description    : Identifies users accessing personal cloud storage or file-sharing exceeding volume thresholds.
// =============================================================================
let ShadowITCategories = dynamic([
    "PERSONAL_SITES_BLOGS", "FILE_HOST", "SHAREWARE_FREEWARE",
    "SOCIAL_NETWORKING", "PERSONAL_EMAIL", "WEB_BASED_EMAIL", "CLOUD_STORAGE",
    "ONLINE_STORAGE_AND_BACKUP", "GENERAL_FILESHARING"]);
CommonSecurityLog
| where TimeGenerated > ago(7d)
| where DeviceVendor == "Zscaler"
| where DeviceCustomString2 in (ShadowITCategories)
    or DeviceCustomString3 in (ShadowITCategories)
| summarize
    RequestCount     = count(),
    TotalBytesSent   = sum(SentBytes),
    TotalBytesRecv   = sum(ReceivedBytes),
    UniqueURLs       = dcount(RequestURL),
    URLSamples       = make_set(RequestURL, 10),
    FirstSeen        = min(TimeGenerated),
    LastSeen         = max(TimeGenerated)
  by SourceUserName, DeviceCustomString2
| extend TotalMBSent = round(toreal(TotalBytesSent) / 1048576, 2)
| where TotalMBSent > 50 or RequestCount > 200
| order by TotalMBSent desc

// =============================================================================
// Q11 - DNS Tunneling Indicators via Zscaler
// MITRE Techniques: T1071.004
// Tactics        : CommandAndControl
// Description    : Detects unusually long hostnames (>50 chars) or high DNS query volume to the same domain.
// =============================================================================
CommonSecurityLog
| where TimeGenerated > ago(24h)
| where DeviceVendor in ("Fortinet", "Palo Alto Networks", "Zscaler")
| where Protocol in ("DNS", "dns") or DestinationPort == 53
| where isnotempty(DestinationHostName)
| extend
    SubdomainLength = strlen(DestinationHostName),
    SubdomainParts  = countof(DestinationHostName, ".")
| where SubdomainLength > 50 or SubdomainParts > 5
| summarize
    QueryCount       = count(),
    UniqueSubdomains = dcount(DestinationHostName),
    LongestQuery     = max(SubdomainLength),
    SampleHosts      = make_set(DestinationHostName, 10),
    SourceIPs        = make_set(SourceIP, 10)
  by SourceIP, DeviceVendor
| where QueryCount > 10
| order by LongestQuery desc, QueryCount desc

// =============================================================================
// Q18 - Zscaler - Impossible Travel Same User Multiple Countries
// MITRE Techniques: T1078
// Tactics        : InitialAccess
// Description    : Detects the same Zscaler username generating requests from more than 1 country within a 1-hour window.
// =============================================================================
CommonSecurityLog
| where TimeGenerated > ago(24h)
| where DeviceVendor == "Zscaler"
| where isnotempty(SourceUserName)
| where isnotempty(SourceIP)
| extend
    UserCountry = tostring(geo_info_from_ip_address(SourceIP).country),
    HourBucket  = bin(TimeGenerated, 1h)
| where isnotempty(UserCountry)
| summarize
    CountryCount     = dcount(UserCountry),
    Countries        = make_set(UserCountry, 10),
    RequestCount     = count(),
    SourceIPs        = make_set(SourceIP, 10),
    URLSamples       = make_set(RequestURL, 5)
  by SourceUserName, HourBucket
| where CountryCount > 1
| order by CountryCount desc, RequestCount desc

// =============================================================================
// Q21 - Slow and Low Exfiltration - Aggregated Small Transfers
// MITRE Techniques: T1048.003
// Tactics        : Exfiltration
// Description    : Detects many small sessions (1KB-1MB each) from the same user where 7-day aggregate exceeds 100 MB.
// =============================================================================
CommonSecurityLog
| where TimeGenerated > ago(7d)
| where DeviceVendor in ("Fortinet", "Palo Alto Networks", "Zscaler")
| where DeviceAction !in ("deny", "block", "drop", "BLOCK", "DROP", "Reset-Both")
| where ipv4_is_private(SourceIP) == true
| where ipv4_is_private(DestinationIP) == false
| where isnotempty(SourceUserName)
| where SentBytes between (1024 .. 1048576)  // 1 KB – 1 MB per individual session
| summarize
    TotalSessions    = count(),
    TotalBytesSent   = sum(SentBytes),
    UniqueDestIPs    = dcount(DestinationIP),
    DestHosts        = make_set(DestinationHostName, 10),
    DestIPs          = make_set(DestinationIP, 10),
    ActiveDays       = dcount(startofday(TimeGenerated)),
    Categories       = make_set(DeviceCustomString2, 5),
    FirstSeen        = min(TimeGenerated),
    LastSeen         = max(TimeGenerated)
  by SourceUserName, DestinationIP
| extend TotalMBSent = round(toreal(TotalBytesSent) / 1048576, 2)
| where TotalMBSent > 100 and TotalSessions > 50
| where UniqueDestIPs <= 3  // concentrated on few destinations vs. random CDN scatter
| order by TotalMBSent desc

// =============================================================================
// Q22 - LOTL Web C2 - GitHub Pastebin Google Docs as C2 Staging
// MITRE Techniques: T1102,T1105
// Tactics        : CommandAndControl
// Description    : Identifies abuse of legitimate public platforms to host payloads or receive C2 tasking.
// =============================================================================
let LotLDomains = dynamic([
    "raw.githubusercontent.com", "gist.githubusercontent.com", "gist.github.com",
    "pastebin.com", "paste.ee", "hastebin.com", "ghostbin.com", "rentry.co",
    "docs.google.com", "drive.google.com",
    "cdn.discordapp.com", "media.discordapp.net",
    "onedrive.live.com", "1drv.ms"]);
CommonSecurityLog
| where TimeGenerated > ago(24h)
| where DeviceVendor == "Zscaler"
| where isnotempty(DestinationHostName)
| where DestinationHostName has_any (LotLDomains)
| where isnotempty(SourceUserName)
| summarize
    RequestCount     = count(),
    TotalBytesRecv   = sum(ReceivedBytes),
    TotalBytesSent   = sum(SentBytes),
    UniqueURLs       = dcount(RequestURL),
    URLSamples       = make_set(RequestURL, 10),
    UserAgents       = make_set(RequestClientApplication, 5),
    SourceIPs        = make_set(SourceIP, 5),
    FirstSeen        = min(TimeGenerated),
    LastSeen         = max(TimeGenerated)
  by SourceUserName, DestinationHostName
| extend TotalMBReceived = round(toreal(TotalBytesRecv) / 1048576, 2)
| extend SuspiciousUA = iff(
    UserAgents !has "Mozilla" and UserAgents !has "Chrome" and UserAgents !has "Safari",
    true, false)
| where TotalMBReceived > 20 or SuspiciousUA == true or RequestCount > 100
| project
    SourceUserName, DestinationHostName,
    RequestCount, TotalMBReceived, TotalBytesSent,
    URLSamples, UserAgents, SuspiciousUA,
    FirstSeen, LastSeen
| order by TotalMBReceived desc, RequestCount desc

// =============================================================================
// Q23 - Stealthy SaaS Attack - ZIA Proxy plus M365 Correlation
// MITRE Techniques: T1078,T1528,T1114
// Tactics        : InitialAccess,Exfiltration
// Description    : Correlates ZIA SaaS traffic with anomalous M365 operations within the same 2-hour window.
// =============================================================================
let ZIASaaSSessions =
    CommonSecurityLog
    | where TimeGenerated > ago(24h)
    | where DeviceVendor == "Zscaler"
    | where DeviceAction !in ("block", "BLOCK", "Blocked", "blocked")
    | where isnotempty(SourceUserName)
    | where DestinationHostName has_any (
        "sharepoint.com", "onedrive.com", "outlook.office.com",
        "graph.microsoft.com", "office.com", "microsoftonline.com",
        "drive.google.com", "mail.google.com", "gmail.com",
        "box.com", "dropbox.com", "salesforce.com",
        "slack.com", "teams.microsoft.com")
    | summarize
        ZIA_RequestCount = count(),
        ZIA_BytesSent    = sum(SentBytes),
        ZIA_BytesRecv    = sum(ReceivedBytes),
        ZIA_URLs         = make_set(RequestURL, 10),
        ZIA_SrcIPs       = make_set(SourceIP, 5),
        ZIA_FirstSeen    = min(TimeGenerated)
      by UserName = tolower(SourceUserName), DestinationHostName;
OfficeActivity
| where TimeGenerated > ago(24h)
| where isnotempty(UserId)
| where Operation in (
    "FileDownloaded", "FileSyncDownloadedFull", "AnonymousLinkUsed",
    "Set-Mailbox", "New-InboxRule", "AddedToGroup",
    "Add OAuth2PermissionGrant", "Consent to application",
    "MailItemsAccessed", "UserLoginFailed")
| summarize
    O365_OpCount     = count(),
    O365_Operations  = make_set(Operation, 10),
    O365_ClientIPs   = make_set(ClientIP, 5),
    O365_FirstSeen   = min(TimeGenerated)
  by UserName = tolower(UserId)
| join kind=inner ZIASaaSSessions on UserName
| where abs(datetime_diff('minute', O365_FirstSeen, ZIA_FirstSeen)) <= 120
| extend ZIA_TotalMBRecv = round(toreal(ZIA_BytesRecv) / 1048576, 2)
| project
    UserName, DestinationHostName,
    ZIA_RequestCount, ZIA_TotalMBRecv, ZIA_URLs, ZIA_SrcIPs,
    O365_OpCount, O365_Operations, O365_ClientIPs,
    ZIA_FirstSeen, O365_FirstSeen
| order by ZIA_TotalMBRecv desc

// =============================================================================
// Q24 - ZIA Allowed Traffic to TI-Listed Domains Categorized Unknown
// MITRE Techniques: T1071,T1568
// Tactics        : CommandAndControl
// Description    : Detects Zscaler-allowed traffic to uncategorized destinations matching active TI feed entries.
// =============================================================================
let TI_IOCs =
    ThreatIntelIndicators
    | where TimeGenerated > ago(30d)
    | where isempty(ValidUntil) or ValidUntil > now()
    | where (isnotnull(parse_ipv4(ObservableValue)) or ObservableKey has "domain")
    | where isnotempty(ObservableValue)
    | summarize
        TI_ThreatTypes = make_set(Tags),
        TI_Confidence  = max(Confidence),
        TI_Tags        = make_set(Tags)
      by IOC_Value = ObservableValue;
CommonSecurityLog
| where TimeGenerated > ago(24h)
| where DeviceVendor == "Zscaler"
| where DeviceAction !in ("block", "BLOCK", "Blocked", "blocked")  // ALLOWED only
| where DeviceCustomString2 in (
    "MISCELLANEOUS_OR_UNKNOWN", "UNKNOWN", "OTHER",
    "NEWLY_REGISTERED_DOMAINS", "MISCELLANEOUS")
    or isempty(DeviceCustomString2)
| where isnotempty(DestinationHostName) or isnotempty(DestinationIP)
| extend MatchKey = coalesce(DestinationHostName, tostring(DestinationIP))
| summarize
    RequestCount     = count(),
    UniqueUsers      = dcount(SourceUserName),
    UserList         = make_set(SourceUserName, 10),
    BytesRecv        = sum(ReceivedBytes),
    BytesSent        = sum(SentBytes),
    FirstSeen        = min(TimeGenerated),
    LastSeen         = max(TimeGenerated)
  by MatchKey, DestinationIP, DestinationHostName
| join kind=inner TI_IOCs on $left.MatchKey == $right.IOC_Value
| project
    MatchKey, DestinationHostName, DestinationIP,
    RequestCount, UniqueUsers, UserList,
    BytesRecv, BytesSent,
    TI_ThreatTypes, TI_Confidence, TI_Tags,
    FirstSeen, LastSeen
| order by TI_Confidence desc, RequestCount desc

// =============================================================================
// Q25 - ZIA C2 Block plus EDR SecurityAlert Escalation
// MITRE Techniques: T1071,T1078
// Tactics        : CommandAndControl
// Description    : Requires corroboration from Zscaler C2 block AND a Defender/Sentinel alert on the same host.
// =============================================================================
let ZscalerC2Blocks =
    CommonSecurityLog
    | where TimeGenerated > ago(24h)
    | where DeviceVendor == "Zscaler"
    | where DeviceAction in ("block", "BLOCK", "Blocked", "blocked")
    | where DeviceCustomString2 has_any (
        "BOTNET_SITES", "MALWARE_SITES", "C2_SITES", "COMMAND_CONTROL",
        "RANSOMWARE", "PHISHING", "SPYWARE_ADWARE_KEYLOGGERS")
    | summarize
        ZIA_BlockCount   = count(),
        ZIA_DestHosts    = make_set(DestinationHostName, 10),
        ZIA_Categories   = make_set(DeviceCustomString2, 5),
        ZIA_DestIPs      = make_set(DestinationIP, 10),
        ZIA_Users        = make_set(SourceUserName, 5),
        ZIA_FirstSeen    = min(TimeGenerated)
      by ZIA_SrcIP = SourceIP;
SecurityAlert
| where TimeGenerated > ago(24h)
| where AlertSeverity in ("High", "Medium")
| mv-expand todynamic(Entities)
| extend
    EntityIP   = tostring(Entities.Address),
    EntityHost = tostring(Entities.HostName)
| where isnotempty(EntityIP)
| summarize
    Alert_Count      = count(),
    Alert_Names      = make_set(AlertName, 5),
    Alert_Products   = make_set(ProductName, 5),
    Alert_Severity   = make_set(AlertSeverity, 3),
    Alert_FirstSeen  = min(TimeGenerated)
  by EntityIP, EntityHost
| join kind=inner ZscalerC2Blocks on $left.EntityIP == $right.ZIA_SrcIP
| project
    ZIA_SrcIP, EntityHost,
    ZIA_BlockCount, ZIA_DestHosts, ZIA_Categories, ZIA_Users,
    Alert_Count, Alert_Names, Alert_Products, Alert_Severity,
    ZIA_FirstSeen, Alert_FirstSeen
| order by ZIA_BlockCount desc, Alert_Count desc

// =============================================================================
// Q26 - HTTP Tunneling - Long or Base64-Encoded URL Parameters
// MITRE Techniques: T1071.001
// Tactics        : CommandAndControl
// Description    : Identifies HTTP-based C2 channels via URL query strings over 150 chars or base64-like patterns.
// =============================================================================
CommonSecurityLog
| where TimeGenerated > ago(24h)
| where DeviceVendor in ("Fortinet", "Palo Alto Networks", "Zscaler")
| where DeviceAction !in ("deny", "block", "drop", "BLOCK", "DROP")
| where isnotempty(RequestURL)
| extend
    URLLength    = strlen(RequestURL),
    QueryString  = iff(RequestURL contains "?",
                       substring(RequestURL, indexof(RequestURL, "?") + 1), "")
| extend QueryLength = strlen(QueryString)
| where URLLength > 200 or QueryLength > 150
| extend
    HasBase64Pattern = QueryString matches regex @"[A-Za-z0-9+/=]{40,}",
    HasPctEncoding   = QueryString contains "%"
| summarize
    RequestCount     = count(),
    MaxURLLength     = max(URLLength),
    MaxQueryLength   = max(QueryLength),
    UniqueUsers      = dcount(SourceUserName),
    Base64Hits       = countif(HasBase64Pattern == true),
    URLSamples       = make_set(RequestURL, 5),
    UserAgents       = make_set(RequestClientApplication, 5),
    SourceIPs        = make_set(SourceIP, 5),
    FirstSeen        = min(TimeGenerated)
  by DestinationHostName, DestinationPort, DeviceVendor
| where RequestCount > 5 or Base64Hits > 2
| order by Base64Hits desc, MaxQueryLength desc

// =============================================================================
// Q27 - Domain Fronting - CDN SNI and Suspicious Path Detection
// MITRE Techniques: T1090.004
// Tactics        : CommandAndControl
// Description    : Detects domain fronting via CDN FQDNs with random/long path patterns or non-browser user agents.
// =============================================================================
let CDNDomains = dynamic([
    "cloudflare.com", "cloudfront.net", "azureedge.net", "azurefd.net",
    "fastly.net", "akamaihd.net", "akamaized.net", "akamai.net",
    "cdn.jsdelivr.net", "unpkg.com", "cdn77.com", "stackpathcdn.com"]);
CommonSecurityLog
| where TimeGenerated > ago(24h)
| where DeviceVendor in ("Fortinet", "Palo Alto Networks", "Zscaler")
| where DeviceAction !in ("deny", "block", "drop", "BLOCK", "DROP")
| where isnotempty(RequestURL) and isnotempty(DestinationHostName)
| where DestinationHostName has_any (CDNDomains)
| extend
    PathLength      = strlen(RequestURL),
    // Flag: long alphanumeric paths lacking file extensions — typical C2 routing paths
    HasRandomPath   = RequestURL matches regex @"/[A-Za-z0-9_\-]{20,}(\?|$|/)"
| summarize
    RequestCount     = count(),
    TotalBytesRecv   = sum(ReceivedBytes),
    TotalBytesSent   = sum(SentBytes),
    MaxPathLength    = max(PathLength),
    RandomPathCount  = countif(HasRandomPath == true),
    UniqueUsers      = dcount(SourceUserName),
    URLSamples       = make_set(RequestURL, 5),
    UserAgents       = make_set(RequestClientApplication, 5),
    SourceIPs        = make_set(SourceIP, 5),
    FirstSeen        = min(TimeGenerated)
  by DestinationHostName, DestinationPort
| extend TotalMBRecv = round(toreal(TotalBytesRecv) / 1048576, 2)
| where RandomPathCount > 5
    or (TotalMBRecv > 50 and UserAgents !has "Mozilla")
| order by RandomPathCount desc, TotalMBRecv desc

// =============================================================================
// Q28 - ZPA plus ZIA Combined Insider Threat Hunt
// MITRE Techniques: T1021,T1078
// Tactics        : LateralMovement,Exfiltration
// Description    : Detects stolen credential or insider scenario via simultaneous ZPA internal app access and ZIA risky categories.
// =============================================================================
let TimeWindow = 24h;
let ZPAActivity =
    CommonSecurityLog
    | where TimeGenerated > ago(TimeWindow)
    | where DeviceProduct has "ZPA"
    | where isnotempty(SourceUserName)
    | summarize
        ZPA_AppCount    = dcount(DestinationHostName),
        ZPA_Apps        = make_set(DestinationHostName, 10),
        ZPA_BytesRecv   = sum(ReceivedBytes),
        ZPA_SrcIPs      = make_set(SourceIP, 5),
        ZPA_GeoCountries = make_set(
            tostring(geo_info_from_ip_address(SourceIP).country), 3),
        ZPA_FirstSeen   = min(TimeGenerated)
      by UserName = tolower(SourceUserName);
let ZIARiskyActivity =
    CommonSecurityLog
    | where TimeGenerated > ago(TimeWindow)
    | where DeviceVendor == "Zscaler"
    | where isnotempty(SourceUserName)
    | where DeviceCustomString2 has_any (
        "REMOTE_ACCESS_TOOLS", "REMOTE_ADMINISTRATION", "TUNNELING_APPLICATIONS",
        "ANONYMOUS_PROXY", "TOR", "PASTE_SITES",
        "TUNNEL_SOCKS_PROXY", "ENCRYPTED_TUNNEL", "VPN_SERVICES")
    | summarize
        ZIA_CategoryCount   = dcount(DeviceCustomString2),
        ZIA_RiskyCategories = make_set(DeviceCustomString2, 10),
        ZIA_Destinations    = make_set(DestinationHostName, 10),
        ZIA_RequestCount    = count(),
        ZIA_FirstSeen       = min(TimeGenerated)
      by UserName = tolower(SourceUserName);
ZPAActivity
| join kind=inner ZIARiskyActivity on UserName
| project
    UserName,
    ZPA_AppCount, ZPA_Apps, ZPA_SrcIPs, ZPA_GeoCountries,
    ZIA_CategoryCount, ZIA_RiskyCategories, ZIA_Destinations, ZIA_RequestCount,
    ZPA_FirstSeen, ZIA_FirstSeen
| order by ZPA_AppCount desc, ZIA_CategoryCount desc

// =============================================================================
// Q29 - Patient APT - Multi-Channel Low and Slow Exfiltration
// MITRE Techniques: T1041,T1567,T1567.002
// Tactics        : Collection,Exfiltration
// Description    : Detects exfiltration spread across multiple cloud channels over 30 days using statistical Z-score peer-group deviation.
// =============================================================================
let HuntWindow    = 30d;
let ExfilDomains  = dynamic([
    // Non-corporate cloud storage
    "drive.google.com", "docs.google.com", "dropbox.com", "box.com",
    "mega.nz", "mediafire.com", "sendspace.com", "wetransfer.com",
    "4shared.com", "anonfiles.com",
    // Code-hosting platforms
    "github.com", "gitlab.com", "bitbucket.org", "gist.github.com",
    // Messaging apps
    "web.whatsapp.com", "telegram.org", "t.me", "discord.com",
    "discordapp.com", "signal.org",
    // Personal webmail / misc
    "mail.google.com", "outlook.live.com", "protonmail.com", "tutanota.com"]);
// Step 1 – ZIA daily upload activity per user to exfil-candidate domains
let ZIA_Daily =
    CommonSecurityLog
    | where TimeGenerated > ago(HuntWindow)
    | where DeviceVendor == "Zscaler"
    | where DeviceAction !in ("block", "BLOCK", "Blocked", "blocked", "deny")
    | where isnotempty(SourceUserName)
    | where DestinationHostName has_any (ExfilDomains)
    | summarize
        DaySentBytes    = sum(SentBytes),
        DayRequestCount = count(),
        DayDestinCount  = dcount(DestinationHostName)
      by UserName = tolower(SourceUserName), Day = bin(TimeGenerated, 1d);
// Step 2 – Aggregate per user across the full hunt window
let ZIA_Users =
    ZIA_Daily
    | summarize
        ZIA_TotalMBSent   = round(toreal(sum(DaySentBytes)) / 1048576, 2),
        ZIA_ActiveDays    = dcount(Day),
        ZIA_TotalRequests = sum(DayRequestCount),
        ZIA_UniqueDestCnt = sum(DayDestinCount)
      by UserName;
// Step 3 – Peer-group baseline statistics (all users in the same ZIA population)
let globalAvg = toscalar(ZIA_Users | summarize avg(ZIA_TotalMBSent));
let globalStd = toscalar(ZIA_Users | summarize stdev(ZIA_TotalMBSent));
// Step 4 – M365 OfficeActivity external-sharing / public-link events
let O365_Sharing =
    OfficeActivity
    | where TimeGenerated > ago(HuntWindow)
    | where isnotempty(UserId)
    | where Operation in (
        "FileDownloaded", "FileSyncDownloadedFull", "FileCopied",
        "AnonymousLinkCreated", "AnonymousLinkUsed",
        "SharingSet", "SharingInvitationCreated",
        "AddedToSecureLink", "SecureLinkUsed")
    | summarize
        O365_EventCount = count(),
        O365_Operations = make_set(Operation, 10),
        O365_AnonLinks  = countif(Operation has "Anonymous"),
        O365_LastEvent  = max(TimeGenerated)
      by UserName = tolower(UserId);
// Step 5 – Combine and flag statistical outliers
ZIA_Users
| join kind=leftouter O365_Sharing on UserName
| extend
    O365_EventCount = coalesce(O365_EventCount, 0),
    O365_AnonLinks  = coalesce(O365_AnonLinks, 0)
| extend
    Zscore = iff(globalStd > 0,
                 round((ZIA_TotalMBSent - globalAvg) / globalStd, 2),
                 0.0),
    DistinctChannels = iff(ZIA_TotalMBSent > 5, 1, 0)
                     + iff(O365_EventCount  > 0, 1, 0)
| where Zscore >= 3.0
    or (O365_AnonLinks  >= 3 and ZIA_TotalMBSent > 10)
    or (ZIA_ActiveDays  >= 20 and ZIA_TotalMBSent > 50)
| project
    UserName,
    ZIA_TotalMBSent, ZIA_ActiveDays, ZIA_TotalRequests, ZIA_UniqueDestCnt,
    O365_EventCount, O365_AnonLinks, O365_Operations,
    DistinctChannels, Zscore
| order by Zscore desc, ZIA_TotalMBSent desc

// =============================================================================
// Q30 - Perfect Impostor - Account Takeover in Normal Traffic
// MITRE Techniques: T1078,T1098,T1114,T1556
// Tactics        : Persistence,PrivilegeEscalation
// Description    : Detects stolen-credential scenario via ZPA new-country anomaly, ZIA suspicious categories, and IdP changes.
// =============================================================================
let baselineWindow = 14d;
let recentWindow   = 4h;
// Baseline: known source countries per user via ZPA over the last 14 days
let ZPA_Baseline =
    CommonSecurityLog
    | where TimeGenerated between (ago(baselineWindow) .. ago(recentWindow))
    | where DeviceVendor == "Zscaler" and DeviceProduct has "ZPA"
    | where DeviceAction !in ("block", "BLOCK", "Failed")
    | where isnotempty(SourceUserName)
    | extend BaselineCountry = tostring(geo_info_from_ip_address(SourceIP).country)
    | summarize
        BaselineCountries = make_set(BaselineCountry, 20),
        BaselineAppCount  = dcount(DestinationHostName)
      by UserName = tolower(SourceUserName);
// Recent ZPA activity (last 4 hours)
let ZPA_Recent =
    CommonSecurityLog
    | where TimeGenerated > ago(recentWindow)
    | where DeviceVendor == "Zscaler" and DeviceProduct has "ZPA"
    | where isnotempty(SourceUserName)
    | extend RecentCountry = tostring(geo_info_from_ip_address(SourceIP).country)
    | summarize
        RecentCountries = make_set(RecentCountry, 5),
        RecentApps      = make_set(DestinationHostName, 10),
        RecentIPs       = make_set(SourceIP, 5),
        RecentConnCount = count(),
        ZPA_FirstSeen   = min(TimeGenerated)
      by UserName = tolower(SourceUserName);
// ZIA suspicious categories: remote admin / account creation / proxy / tunnels
let ZIA_Suspicious =
    CommonSecurityLog
    | where TimeGenerated > ago(recentWindow)
    | where DeviceVendor == "Zscaler"
    | where isnotempty(SourceUserName)
    | where DeviceCustomString2 has_any (
        "REMOTE_ACCESS_TOOLS", "REMOTE_ADMINISTRATION", "WEB_BASED_ADMIN",
        "ACCOUNT_CREATION", "TUNNEL_SOCKS_PROXY", "ANONYMOUS_PROXY",
        "ONLINE_STORAGE", "FILE_SHARE", "TOR", "VPN_SERVICES",
        "NEWLY_REGISTERED_DOMAINS", "PHISHING")
    | summarize
        ZIA_Categories   = make_set(DeviceCustomString2, 10),
        ZIA_Destinations = make_set(DestinationHostName, 10),
        ZIA_RequestCount = count(),
        ZIA_FirstSeen    = min(TimeGenerated)
      by UserName = tolower(SourceUserName);
// IdP / SaaS security changes: MFA, OAuth, mailbox rules, privilege escalation
let IdP_Changes =
    OfficeActivity
    | where TimeGenerated > ago(recentWindow)
    | where isnotempty(UserId)
    | where Operation in (
        "New-InboxRule",                         // auto-forward / hide rules
        "Set-Mailbox",                           // mailbox delegation
        "Add-MailboxPermission",
        "Add OAuth2PermissionGrant",             // OAuth consent grant
        "Consent to application",
        "Add service principal credentials",
        "Update user",
        "Disable Strong Authentication",         // MFA removal
        "Update StsRefreshTokenValidFrom",       // token invalidation
        "Add member to role")                    // privilege escalation
    | summarize
        IdP_EventCount = count(),
        IdP_Operations = make_set(Operation, 10),
        IdP_FirstSeen  = min(TimeGenerated)
      by UserName = tolower(UserId);
// Combine layers, flag users with any cross-layer anomaly
ZPA_Recent
| join kind=inner ZPA_Baseline on UserName
| extend NewCountries = set_difference(RecentCountries, BaselineCountries)
| join kind=leftouter ZIA_Suspicious on UserName
| join kind=leftouter IdP_Changes on UserName
| extend
    ZIA_RequestCount = coalesce(ZIA_RequestCount, 0),
    IdP_EventCount   = coalesce(IdP_EventCount, 0)
| extend ImpostorScore =
    iff(array_length(NewCountries) > 0, 40, 0)
  + (ZIA_RequestCount * 5)
  + (IdP_EventCount * 30)
| where ImpostorScore >= 40
    or (IdP_EventCount >= 1 and ZIA_RequestCount >= 1)
    or array_length(NewCountries) > 0
| project
    UserName,
    NewCountries, RecentCountries, BaselineCountries,
    RecentApps, RecentIPs, RecentConnCount,
    ZIA_Categories, ZIA_Destinations, ZIA_RequestCount,
    IdP_Operations, IdP_EventCount,
    ImpostorScore,
    ZPA_FirstSeen
| order by ImpostorScore desc

// =============================================================================
// Q31 - APT Control Evasion - Degrading Zscaler and EDR Visibility
// MITRE Techniques: T1562,T1562.001,T1070
// Tactics        : DefenseEvasion
// Description    : Detects hosts with healthy ZIA baseline that go silent combined with Zscaler or EDR agent tampering.
// =============================================================================
let baselineWindow            = 7d;
let recentWindow              = 4h;
let minBaselineRequestsPerDay = 10;
// Hosts that had consistent ZIA activity during the baseline window
let ZIA_BaselineHosts =
    CommonSecurityLog
    | where TimeGenerated between (ago(baselineWindow) .. ago(recentWindow))
    | where DeviceVendor == "Zscaler"
    | where isnotempty(SourceIP) and isnotempty(SourceUserName)
    | summarize
        BaselineRequestsPerDay = round(toreal(count()) / (baselineWindow / 1d), 1),
        BaselineUsers          = make_set(SourceUserName, 5)
      by SourceIP
    | where BaselineRequestsPerDay >= minBaselineRequestsPerDay;
// Hosts that produced ANY ZIA traffic in the recent window
let ZIA_RecentActive =
    CommonSecurityLog
    | where TimeGenerated > ago(recentWindow)
    | where DeviceVendor == "Zscaler"
    | where isnotempty(SourceIP)
    | distinct SourceIP;
// Hosts that went silent: were in baseline but absent in recent window
let SilentHosts =
    ZIA_BaselineHosts
    | join kind=leftanti ZIA_RecentActive on SourceIP;
// MDE DeviceEvents: Zscaler Client Connector or EDR agent tampering
let AgentTampering =
    DeviceEvents
    | where TimeGenerated > ago(recentWindow)
    | where ActionType in (
        "ServiceUninstalled", "ServiceStopped", "DriverUnloaded",
        "TamperingAttempt", "AntivirusScanFailed")
      or (ActionType == "ProcessCreated"
          and InitiatingProcessCommandLine has_any (
              "ZscalerApp", "ZSATunnel", "ZSAUpdater", "ZscalerService",
              "sc stop", "sc delete", "net stop", "taskkill"))
    | summarize
        TamperEvents    = count(),
        TamperActions   = make_set(ActionType, 10),
        TamperCmds      = make_set(InitiatingProcessCommandLine, 5),
        TamperFirstSeen = min(TimeGenerated)
      by DeviceName;
// SecurityEvent: Windows Service Control Manager — service stop/delete events
let SvcTampering =
    SecurityEvent
    | where TimeGenerated > ago(recentWindow)
    | where EventID in (7034, 7035, 7036, 7040, 7045)
    | where ServiceName has_any (
        "ZscalerService", "ZSATunnel", "ZSAUpdater",
        "MsSense", "SenseSvc", "WdFilter", "AADConnect")
    | summarize
        SvcEvents      = count(),
        SvcNames       = make_set(ServiceName, 10),
        SvcStateChange = make_set(Activity, 5),
        SvcFirstSeen   = min(TimeGenerated)
      by Computer;
// Final correlation: silent hosts enriched with tampering evidence
SilentHosts
| join kind=leftouter AgentTampering on $left.SourceIP == $right.DeviceName
| join kind=leftouter SvcTampering   on $left.SourceIP == $right.Computer
| extend
    TamperEvents = coalesce(TamperEvents, 0),
    SvcEvents    = coalesce(SvcEvents, 0)
| extend EvasionScore =
    toint(BaselineRequestsPerDay * 2)
  + iff(TamperEvents > 0, 50, 0)
  + iff(SvcEvents    > 0, 40, 0)
| project
    SourceIP, BaselineUsers, BaselineRequestsPerDay,
    TamperEvents, TamperActions, TamperCmds,
    SvcEvents, SvcNames, SvcStateChange,
    EvasionScore,
    TamperFirstSeen, SvcFirstSeen
| order by EvasionScore desc, BaselineRequestsPerDay desc

Explanation

This query script is a collection of threat-hunting queries designed to analyze security logs from Zscaler ZIA/ZPA and other vendors like Fortinet and Palo Alto Networks. The queries aim to identify various security threats and anomalies within a network. Here's a simplified summary of each query:

  1. Blocked Requests by Malicious Category: Identifies web requests blocked by Zscaler due to malicious categories like botnet, malware, and phishing, and summarizes the data by category and user.

  2. Shadow IT and File-Sharing Access: Detects users accessing personal cloud storage or file-sharing services, exceeding certain data volume thresholds, indicating potential data exfiltration.

  3. DNS Tunneling Indicators: Identifies potential DNS tunneling by detecting unusually long hostnames or high DNS query volumes to the same domain.

  4. Impossible Travel Same User Multiple Countries: Detects instances where the same user account is accessed from multiple countries within a short time frame, indicating possible credential compromise.

  5. Slow and Low Exfiltration: Identifies users with many small data transfers that cumulatively exceed a threshold, suggesting stealthy data exfiltration.

  6. LOTL Web C2 - GitHub Pastebin Google Docs as C2 Staging: Detects the use of legitimate platforms like GitHub and Google Docs for command and control (C2) activities.

  7. Stealthy SaaS Attack - ZIA Proxy plus M365 Correlation: Correlates ZIA SaaS traffic with anomalous Microsoft 365 operations to detect potential insider threats or compromised accounts.

  8. ZIA Allowed Traffic to TI-Listed Domains Categorized Unknown: Identifies allowed traffic to uncategorized domains that match threat intelligence indicators, suggesting potential threats.

  9. ZIA C2 Block plus EDR SecurityAlert Escalation: Correlates Zscaler C2 blocks with security alerts from endpoint detection and response (EDR) tools to escalate potential threats.

  10. HTTP Tunneling - Long or Base64-Encoded URL Parameters: Detects potential HTTP-based C2 channels using long or base64-like URL query strings.

  11. Domain Fronting - CDN SNI and Suspicious Path Detection: Identifies domain fronting techniques using CDN domains with suspicious path patterns or non-browser user agents.

  12. ZPA plus ZIA Combined Insider Threat Hunt: Detects potential insider threats by correlating ZPA internal app access with ZIA risky categories.

  13. Patient APT - Multi-Channel Low and Slow Exfiltration: Detects exfiltration activities spread across multiple cloud channels over a month, using statistical analysis.

  14. Perfect Impostor - Account Takeover in Normal Traffic: Identifies potential account takeovers by detecting anomalies in user activity across ZPA, ZIA, and identity provider logs.

  15. APT Control Evasion - Degrading Zscaler and EDR Visibility: Detects hosts that go silent after consistent activity, potentially due to tampering with Zscaler or EDR agents.

Each query is designed to surface specific types of suspicious activities or anomalies that could indicate security threats, helping security teams to proactively identify and respond to potential incidents.

Details

David Alonso profile picture

David Alonso

Released: March 18, 2026

Tables

CommonSecurityLogOfficeActivityThreatIntelIndicatorsSecurityAlertDeviceEventsSecurityEvent

Keywords

CommonSecurityLogZscalerZIAZPAThreatHuntingQueriesDevicesUserIntuneSecurityAlertOfficeActivityDeviceEventsSecurityEventThreatIntelIndicators

Operators

letdynamicagoinhas_anysummarizecountdcountmake_setminmaxbyorder byextendtostringbincoalesceiffprojectbetweendistinctjoinonleftouterleftantiroundtorealtoscalarsummarizestdevabsdatetime_diffmv-expandtodynamicwhereisnotemptyisemptyparse_ipv4hasmatches regexstrlencountofset_differencearray_lengthtointnowdatetime_diffmv-expandtodynamiciffparse_ipv4hasmatches regexstrlencountofset_differencearray_lengthtointnow

Actions