Detection Query Library¶
Production-ready KQL (Microsoft Sentinel/Defender) and SPL (Splunk) queries organized by MITRE ATT&CK tactic. Copy, adapt, and deploy.
Query Standards
- All KQL queries target Microsoft Defender for Endpoint (
DeviceProcessEvents,DeviceNetworkEvents,DeviceFileEvents,DeviceRegistryEvents,IdentityLogonEvents,CloudAppEvents) - All SPL queries assume CIM-compliant sourcetypes (
endpoint,network,authentication) - Thresholds marked with
-- TUNE:comments should be baselined per environment - Test in staging for 7 days before production deployment (Nexus SecOps-203)
TA0001 — Initial Access¶
1. Phishing Attachment Execution (Office Macro → Child Process)¶
Detects Microsoft Office applications spawning suspicious child processes, a hallmark of macro-based phishing payloads.
High Fidelity
This query has low false-positive rates in most environments. Investigate all hits.
// TA0001 — Phishing: Office macro spawning suspicious child process
DeviceProcessEvents
| where Timestamp > ago(1d)
| where InitiatingProcessFileName in~ (
"winword.exe", "excel.exe", "powerpnt.exe",
"outlook.exe", "onenote.exe", "mspub.exe"
)
| where FileName in~ (
"cmd.exe", "powershell.exe", "pwsh.exe", "wscript.exe",
"cscript.exe", "mshta.exe", "regsvr32.exe", "certutil.exe",
"bitsadmin.exe", "rundll32.exe", "msiexec.exe"
)
| project Timestamp, DeviceName, AccountName,
InitiatingProcessFileName, FileName, ProcessCommandLine,
InitiatingProcessCommandLine
| order by Timestamp desc
| tstats `summariesonly` count min(_time) as firstTime max(_time) as lastTime
from datamodel=Endpoint.Processes
where Processes.parent_process_name IN ("winword.exe","excel.exe","powerpnt.exe",
"outlook.exe","onenote.exe","mspub.exe")
AND Processes.process_name IN ("cmd.exe","powershell.exe","wscript.exe",
"cscript.exe","mshta.exe","regsvr32.exe","certutil.exe","bitsadmin.exe")
by Processes.dest Processes.user Processes.parent_process_name
Processes.process_name Processes.process
| `drop_dm_object_name(Processes)`
| eval severity="high"
| `security_content_ctime(firstTime)`
| `security_content_ctime(lastTime)`
| table firstTime lastTime dest user parent_process_name process_name process severity
2. External Remote Services Anomaly (VPN Logins from Unusual Countries)¶
Flags authentication events from countries that have never previously appeared for a given account.
// TA0001 — Valid accounts: first-seen country for user
// TUNE: adjust lookback window for baseline (default 30d)
let baseline_window = 30d;
let detection_window = 1d;
let known_countries =
IdentityLogonEvents
| where Timestamp between (ago(baseline_window) .. ago(detection_window))
| where LogonType != "Failed"
| summarize KnownCountries = make_set(Location) by AccountUpn;
IdentityLogonEvents
| where Timestamp > ago(detection_window)
| where LogonType != "Failed"
| join kind=leftouter known_countries on AccountUpn
| where isnotempty(Location)
| where not(set_has_element(KnownCountries, Location))
| project Timestamp, AccountUpn, Location, IPAddress, Application, LogonType
| order by Timestamp desc
index=authentication sourcetype=okta OR sourcetype=azure:aad action=success
| eval user=coalesce(user, src_user)
| stats dc(src_country) as country_count values(src_country) as countries
earliest(_time) as firstTime latest(_time) as lastTime
by user
| where country_count > 1
| eval baseline_check=if(firstTime < relative_time(now(), "-30d@d"), "established","new_user")
| lookup geoip_baseline user OUTPUT known_countries
| eval new_country=mvfilter(match(countries, "^(?!".known_countries.").*"))
| where isnotnull(new_country)
| `security_content_ctime(firstTime)`
| table user countries new_country country_count firstTime lastTime
3. Valid Account Abuse — First-Seen User Agent and Geo Combination¶
Detects cloud application logins using a previously unseen (user-agent, country) pair for an established user.
// TA0001 — Valid account abuse: new UA + geo combo
// TUNE: baseline_window — increase to 60d for mature environments
let baseline_window = 30d;
let detection_window = 1h;
let baseline =
CloudAppEvents
| where Timestamp between (ago(baseline_window) .. ago(detection_window))
| summarize KnownCombos = make_set(strcat(CountryCode, "|", UserAgent))
by AccountObjectId;
CloudAppEvents
| where Timestamp > ago(detection_window)
| extend Combo = strcat(CountryCode, "|", UserAgent)
| join kind=leftouter baseline on AccountObjectId
| where not(set_has_element(KnownCombos, Combo))
| where isnotempty(CountryCode) and isnotempty(UserAgent)
| project Timestamp, AccountDisplayName, AccountObjectId,
CountryCode, UserAgent, IPAddress, Application, ActivityType
| order by Timestamp desc
index=authentication sourcetype=o365:management:activity OR sourcetype=gsuite
action=success
| stats earliest(_time) as firstSeen
by user http_user_agent src_country
| where firstSeen > relative_time(now(), "-1h@h")
| eval combo=user."|".http_user_agent."|".src_country
| lookup user_agent_baseline combo OUTPUT seen_before
| where seen_before!="true" OR isnull(seen_before)
| eval alert="First-seen UA+Geo combination"
| table user http_user_agent src_country firstSeen alert
TA0002 — Execution¶
1. PowerShell Encoded Command Execution¶
Detects Base64-encoded PowerShell commands, commonly used to evade basic string-based detection.
Decode on the fly
Pair this alert with a SOAR playbook that automatically decodes the Base64 payload and appends it to the alert.
// TA0002 — Execution: PowerShell encoded commands
DeviceProcessEvents
| where Timestamp > ago(1d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| where ProcessCommandLine has_any ("-enc", "-EncodedCommand", "-ec ", "-en ")
or ProcessCommandLine matches regex @"(?i)-e[nc]{1,2}\s+[A-Za-z0-9+/=]{20,}"
| extend DecodedHint = base64_decode_tostring(
extract(@"(?i)-e[nc]{0,2}\s+([A-Za-z0-9+/=]{20,})", 1, ProcessCommandLine)
)
| project Timestamp, DeviceName, AccountName,
ProcessCommandLine, DecodedHint, InitiatingProcessFileName
| order by Timestamp desc
index=endpoint sourcetype=WinEventLog:Security EventCode=4104
Message="*-enc*" OR Message="*-EncodedCommand*" OR Message="*-ec *"
| rex field=Message "(?i)-e(?:nc|ncodedCommand)?\s+(?<b64payload>[A-Za-z0-9+/=]{20,})"
| eval decoded=base64decode(b64payload)
| eval severity=case(
match(decoded,"(?i)iex|invoke-expression|downloadstring|webclient"), "critical",
match(decoded,"(?i)bypass|hidden|noprofile"), "high",
1==1, "medium"
)
| stats count by ComputerName, user, b64payload, decoded, severity
| sort - severity
| table ComputerName user b64payload decoded severity
2. WMI Process Creation (WmiPrvSE Spawning cmd/PowerShell)¶
WMI is a common living-off-the-land execution method. WmiPrvSE spawning shells is highly suspicious.
// TA0002 — Execution: WMI spawning shells
DeviceProcessEvents
| where Timestamp > ago(1d)
| where InitiatingProcessFileName =~ "WmiPrvSE.exe"
| where FileName in~ (
"cmd.exe", "powershell.exe", "pwsh.exe", "wscript.exe",
"cscript.exe", "mshta.exe", "regsvr32.exe", "rundll32.exe",
"certutil.exe", "net.exe", "net1.exe"
)
| project Timestamp, DeviceName, AccountName,
FileName, ProcessCommandLine,
InitiatingProcessFileName, InitiatingProcessCommandLine
| order by Timestamp desc
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=1
| where ParentImage LIKE "%WmiPrvSE.exe"
| where Image IN (
"C:\\Windows\\System32\\cmd.exe",
"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
"C:\\Windows\\System32\\wscript.exe",
"C:\\Windows\\System32\\cscript.exe"
)
| stats count by Computer, User, Image, CommandLine, ParentCommandLine
| eval severity="high"
| table Computer User Image CommandLine ParentCommandLine severity count
3. LOLBAS Abuse (mshta, regsvr32, certutil Downloading Files)¶
Living-off-the-land binaries used to download or execute remote payloads.
// TA0002 — Execution: LOLBAS downloading content
DeviceProcessEvents
| where Timestamp > ago(1d)
| where (
(FileName =~ "certutil.exe" and ProcessCommandLine has_any ("urlcache", "decode", "-f", "http"))
or (FileName =~ "mshta.exe" and ProcessCommandLine has_any ("http://", "https://", "javascript:", "vbscript:"))
or (FileName =~ "regsvr32.exe" and ProcessCommandLine has_any ("/s", "/u", "/i:", "http", "scrobj.dll"))
or (FileName =~ "bitsadmin.exe" and ProcessCommandLine has_any ("/transfer", "/addfile", "http"))
or (FileName =~ "wmic.exe" and ProcessCommandLine has_any ("process call create", "xsl", "http"))
)
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine,
InitiatingProcessFileName
| order by Timestamp desc
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=1
| eval lolbas=case(
match(Image,"(?i)certutil") AND match(CommandLine,"(?i)urlcache|decode|-f"), "certutil_download",
match(Image,"(?i)mshta") AND match(CommandLine,"(?i)http|javascript|vbscript"), "mshta_remote",
match(Image,"(?i)regsvr32") AND match(CommandLine,"(?i)/i:|scrobj|http"), "regsvr32_remote",
match(Image,"(?i)bitsadmin") AND match(CommandLine,"(?i)/transfer|/addfile|http"), "bitsadmin_download",
match(Image,"(?i)wmic") AND match(CommandLine,"(?i)process call create|xsl"), "wmic_exec",
true(), null()
)
| where isnotnull(lolbas)
| stats count by Computer, User, Image, CommandLine, lolbas
| eval severity="high"
| table Computer User Image CommandLine lolbas severity count
4. Scheduled Task Creation via schtasks.exe¶
Attackers frequently create scheduled tasks for persistence and execution.
// TA0002 / TA0003 — Execution/Persistence: Scheduled task creation
DeviceProcessEvents
| where Timestamp > ago(1d)
| where FileName =~ "schtasks.exe"
| where ProcessCommandLine has_any ("/create", "/sc", "/tr", "/tn")
// Exclude common admin tools — TUNE: add your management tool paths
| where not(InitiatingProcessFileName in~ ("mmc.exe", "taskschd.msc"))
| project Timestamp, DeviceName, AccountName, ProcessCommandLine,
InitiatingProcessFileName, InitiatingProcessCommandLine
| order by Timestamp desc
index=endpoint sourcetype=WinEventLog:Security EventCode=4698
| rex field=Message "Task Name:\s*(?<task_name>[^\r\n]+)"
| rex field=Message "Task Content:\s*(?<task_content>[\s\S]+)"
| eval suspicious=if(
match(task_content,"(?i)powershell|cmd|wscript|cscript|mshta|regsvr32|rundll32"),
"true", "false"
)
| where suspicious="true"
| stats count by Computer, SubjectUserName, task_name, task_content
| eval severity="medium"
| table Computer SubjectUserName task_name task_content severity count
TA0003 — Persistence¶
1. Registry Run Key Modification¶
Modifications to HKCU/HKLM Run keys provide persistent execution on login/boot.
// TA0003 — Persistence: Registry Run key modifications
DeviceRegistryEvents
| where Timestamp > ago(1d)
| where RegistryKey has_any (
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run",
@"SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce",
@"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run",
@"SYSTEM\CurrentControlSet\Services"
)
| where ActionType in ("RegistryValueSet", "RegistryKeyCreated")
// Exclude known software updates — TUNE: add your vendors
| where not(InitiatingProcessFileName in~ ("msiexec.exe", "setup.exe", "installer.exe"))
| project Timestamp, DeviceName, AccountName, RegistryKey,
RegistryValueName, RegistryValueData,
InitiatingProcessFileName, InitiatingProcessCommandLine
| order by Timestamp desc
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=13
| where TargetObject LIKE "%\\CurrentVersion\\Run%"
OR TargetObject LIKE "%\\CurrentVersion\\RunOnce%"
| eval suspicious=if(
match(Details,"(?i)powershell|cmd|wscript|cscript|mshta|regsvr32|rundll32|temp|appdata"),
"true","false"
)
| stats count by Computer, User, TargetObject, Details, suspicious
| eval severity=if(suspicious="true","high","medium")
| sort - severity
| table Computer User TargetObject Details severity count
2. Scheduled Task with Network-Facing Commands¶
Tasks that download content or reach out to external hosts are high-priority persistence indicators.
// TA0003 — Persistence: Scheduled task calling out to network
DeviceProcessEvents
| where Timestamp > ago(1d)
| where InitiatingProcessFileName =~ "taskeng.exe"
or InitiatingProcessParentFileName =~ "svchost.exe"
| where FileName in~ ("powershell.exe", "pwsh.exe", "cmd.exe",
"wscript.exe", "cscript.exe", "mshta.exe")
| where ProcessCommandLine has_any (
"http", "https", "ftp", "invoke-webrequest",
"downloadstring", "downloadfile", "WebClient", "iwr", "curl", "wget"
)
| project Timestamp, DeviceName, AccountName, FileName,
ProcessCommandLine, InitiatingProcessFileName
| order by Timestamp desc
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=1
| where ParentImage LIKE "%taskeng.exe" OR ParentImage LIKE "%svchost.exe"
| where Image IN (
"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
"C:\\Windows\\System32\\cmd.exe",
"C:\\Windows\\System32\\wscript.exe"
)
| where match(CommandLine,"(?i)http|https|invoke-webrequest|downloadstring|webclient")
| stats count by Computer, User, Image, CommandLine, ParentImage
| eval severity="high"
| table Computer User Image CommandLine ParentImage severity count
3. New Service Creation¶
Adversaries install malicious services for persistence. Event 4697 covers service installs.
// TA0003 — Persistence: New service creation
DeviceEvents
| where Timestamp > ago(1d)
| where ActionType == "ServiceInstalled"
// Exclude known update processes — TUNE: add software management tools
| where not(InitiatingProcessFileName in~ (
"msiexec.exe", "TiWorker.exe", "svchost.exe", "WaaSMedicAgent.exe"
))
| where not(FolderPath startswith @"C:\Windows\")
| project Timestamp, DeviceName, AccountName,
InitiatingProcessFileName, FileName, FolderPath,
AdditionalFields
| order by Timestamp desc
index=wineventlog sourcetype=WinEventLog:Security EventCode=4697
| rex field=Message "Service Name:\s*(?<service_name>[^\r\n]+)"
| rex field=Message "Service File Name:\s*(?<service_path>[^\r\n]+)"
| rex field=Message "Service Type:\s*(?<service_type>[^\r\n]+)"
| eval suspicious=if(
NOT match(service_path,"(?i)C:\\\\Windows\\\\|C:\\\\Program Files\\\\"),
"true","false"
)
| where suspicious="true"
| stats count by Computer, SubjectUserName, service_name, service_path, service_type
| eval severity="high"
| table Computer SubjectUserName service_name service_path service_type severity count
TA0004 — Privilege Escalation¶
1. Token Impersonation (SeImpersonatePrivilege Abuse)¶
Potato-style attacks and service abuse rely on impersonating high-privilege tokens.
// TA0004 — Privilege Escalation: SeImpersonatePrivilege token abuse
DeviceEvents
| where Timestamp > ago(1d)
| where ActionType == "ProcessPrimaryTokenModified"
or (ActionType == "CreateRemoteThreadApiCall" and
InitiatingProcessIntegrityLevel == "Low")
| join kind=inner (
DeviceProcessEvents
| where ProcessTokenElevationType == "Full"
| project DeviceId, ProcessId, FileName, AccountName, Timestamp
) on DeviceId, $left.InitiatingProcessId == $right.ProcessId
| project Timestamp, DeviceName, AccountName, FileName,
InitiatingProcessFileName, ActionType
| order by Timestamp desc
index=wineventlog sourcetype=WinEventLog:Security EventCode=4672
| eval priv_list=PrivilegeList
| where match(priv_list,"SeImpersonatePrivilege|SeAssignPrimaryTokenPrivilege|SeTcbPrivilege")
| where NOT match(SubjectUserName,"(?i)\$$|network service|local service|system")
| stats count by Computer, SubjectUserName, SubjectLogonId, priv_list
| eval severity="high"
| table Computer SubjectUserName SubjectLogonId priv_list severity count
2. UAC Bypass via fodhelper.exe¶
fodhelper.exe reads a user-controllable registry path and auto-elevates, bypassing UAC.
// TA0004 — Privilege Escalation: UAC bypass via fodhelper
DeviceProcessEvents
| where Timestamp > ago(1d)
| where InitiatingProcessFileName =~ "fodhelper.exe"
or (FileName =~ "fodhelper.exe" and
InitiatingProcessFileName !~ "explorer.exe")
| project Timestamp, DeviceName, AccountName,
FileName, ProcessCommandLine,
InitiatingProcessFileName, InitiatingProcessCommandLine,
ProcessTokenElevationType
| order by Timestamp desc
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=1
| where ParentImage LIKE "%fodhelper.exe"
OR (Image LIKE "%fodhelper.exe"
AND NOT ParentImage LIKE "%explorer.exe")
| stats count by Computer, User, Image, CommandLine, ParentImage, IntegrityLevel
| where IntegrityLevel="High" OR IntegrityLevel="System"
| eval severity="high"
| table Computer User Image CommandLine ParentImage IntegrityLevel severity count
TA0005 — Defense Evasion¶
1. Security Tool Tampering (EDR Service Stop/Disable)¶
Ransomware and advanced actors disable security tooling before deploying payloads.
// TA0005 — Defense Evasion: Security service stop or disable
// TUNE: add your EDR/AV service names below
let security_services = dynamic([
"MsSense", "WinDefend", "Sense", "SentinelAgent", "CrowdStrike",
"CylanceSvc", "CarbonBlack", "cbdefense", "McShield", "ekrn",
"MBAMService", "BDAgent", "SAVAdminService"
]);
DeviceProcessEvents
| where Timestamp > ago(1d)
| where FileName in~ ("sc.exe", "net.exe", "net1.exe", "taskkill.exe")
| where ProcessCommandLine has_any (security_services)
| where ProcessCommandLine has_any ("stop", "disable", "/kill", "/f")
| project Timestamp, DeviceName, AccountName, FileName,
ProcessCommandLine, InitiatingProcessFileName
| order by Timestamp desc
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=1
| where Image IN (
"C:\\Windows\\System32\\sc.exe",
"C:\\Windows\\System32\\net.exe",
"C:\\Windows\\System32\\net1.exe",
"C:\\Windows\\System32\\taskkill.exe"
)
| where match(CommandLine,
"(?i)MsSense|WinDefend|SentinelAgent|CrowdStrike|Cylance|cbdefense|McShield")
| where match(CommandLine,"(?i)stop|disable|/kill|/f")
| stats count by Computer, User, Image, CommandLine
| eval severity="critical"
| table Computer User Image CommandLine severity count
2. Windows Event Log Clearing (Event 1102 / 104)¶
Log clearing removes forensic evidence. Event 1102 = Security log cleared. Event 104 = System log cleared.
// TA0005 — Defense Evasion: Event log clearing
DeviceEvents
| where Timestamp > ago(1d)
| where ActionType == "SecurityLogCleared"
| project Timestamp, DeviceName, AccountName, ActionType, AdditionalFields
| union (
DeviceEvents
| where Timestamp > ago(1d)
| where ActionType == "OtherAlertRelatedActivity"
| where AdditionalFields has "EventLog"
| where AdditionalFields has "104"
)
| order by Timestamp desc
index=wineventlog sourcetype=WinEventLog:Security EventCode=1102
| eval log_type="Security"
| append [
search index=wineventlog sourcetype=WinEventLog:System EventCode=104
| eval log_type="System"
]
| stats count by _time Computer SubjectUserName log_type
| eval severity="critical"
| table _time Computer SubjectUserName log_type severity count
3. Timestomping Detection (File Creation Before Compile Time)¶
Adversaries alter file timestamps to blend in. $STANDARD_INFORMATION creation time earlier than $FILE_NAME is a key indicator.
// TA0005 — Defense Evasion: Timestomping ($SI < $FN)
// Requires Defender for Endpoint with advanced file telemetry
DeviceFileEvents
| where Timestamp > ago(1d)
| where ActionType == "FileCreated"
| extend FileProfile = parse_json(AdditionalFields)
| where isnotempty(FileProfile.Signer)
| where CreationTime < PreviousCreationTime
| project Timestamp, DeviceName, AccountName, FileName, FolderPath,
CreationTime, PreviousCreationTime,
InitiatingProcessFileName, InitiatingProcessCommandLine
| order by Timestamp desc
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=2
| eval std_info_time=strptime(CreationUtcTime,"%Y-%m-%d %H:%M:%S.%3N")
| eval fn_time=strptime(PreviousCreationUtcTime,"%Y-%m-%d %H:%M:%S.%3N")
| where std_info_time < fn_time
| eval time_diff_hrs=round((fn_time - std_info_time)/3600, 2)
| where time_diff_hrs > 1
| stats count by Computer, User, TargetFilename, std_info_time, fn_time, time_diff_hrs
| eval severity=if(time_diff_hrs>168,"high","medium")
| table Computer User TargetFilename std_info_time fn_time time_diff_hrs severity count
4. LSASS Dump via procdump or comsvcs.dll¶
The two most common LSASS dumping methods: Sysinternals procdump and the built-in comsvcs.dll MiniDump export.
// TA0005 / TA0006 — Defense Evasion + Credential Access: LSASS dump
DeviceProcessEvents
| where Timestamp > ago(1d)
| where (
// procdump targeting lsass
(FileName =~ "procdump.exe" or FileName =~ "procdump64.exe")
and ProcessCommandLine has "lsass"
)
or (
// comsvcs MiniDump
FileName in~ ("rundll32.exe", "cmd.exe", "powershell.exe")
and ProcessCommandLine has_all ("comsvcs", "MiniDump")
)
or (
// Task Manager dump (user-initiated)
FileName =~ "taskmgr.exe"
and ProcessCommandLine has "lsass.dmp"
)
| project Timestamp, DeviceName, AccountName, FileName,
ProcessCommandLine, InitiatingProcessFileName
| order by Timestamp desc
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=1
| eval dump_method=case(
match(Image,"(?i)procdump") AND match(CommandLine,"(?i)lsass"), "procdump_lsass",
match(CommandLine,"(?i)comsvcs.*MiniDump|MiniDump.*comsvcs"), "comsvcs_minidump",
match(Image,"(?i)taskmgr") AND match(CommandLine,"(?i)lsass"), "taskmgr_dump",
true(), null()
)
| where isnotnull(dump_method)
| stats count by Computer, User, Image, CommandLine, dump_method
| eval severity="critical"
| table Computer User Image CommandLine dump_method severity count
TA0006 — Credential Access¶
1. Kerberoasting Detection (Event 4769 with RC4 Encryption)¶
Kerberoasting requests service tickets using weak RC4 (0x17) encryption to crack offline.
// TA0006 — Credential Access: Kerberoasting — RC4 TGS requests
// TUNE: threshold — in most environments legitimate RC4 TGS requests are rare
IdentityLogonEvents
| where Timestamp > ago(1d)
| where Protocol == "Kerberos"
| where LogonType == "Resource access"
| extend TicketEncryption = tostring(parse_json(AdditionalFields).TicketEncryptionType)
| where TicketEncryption == "0x17" // RC4-HMAC
| where TargetAccountUpn !endswith "$" // exclude machine accounts
| summarize RequestCount = count(), TargetAccounts = make_set(TargetAccountUpn)
by AccountUpn, DeviceName, bin(Timestamp, 5m)
// TUNE: lower threshold for stricter detection
| where RequestCount > 3
| order by RequestCount desc
index=wineventlog sourcetype=WinEventLog:Security EventCode=4769
| where TicketEncryptionType="0x17" OR TicketEncryptionType="0x18"
| where NOT match(ServiceName,"(?i)\$$|krbtgt|_ldap_|_kerberos_")
| stats count as request_count
dc(ServiceName) as distinct_services
values(ServiceName) as services
by AccountName, IpAddress, bin(_time, 5m)
| where request_count > 3 OR distinct_services > 5
| eval severity=if(distinct_services>10,"critical","high")
| sort - request_count
| table _time AccountName IpAddress request_count distinct_services services severity
2. LSASS Memory Access (Sysmon Event 10)¶
Direct LSASS memory reads are a telltale sign of credential harvesting tools (Mimikatz, etc.).
// TA0006 — Credential Access: LSASS memory access
DeviceEvents
| where Timestamp > ago(1d)
| where ActionType == "OpenProcessApiCall"
| where FileName =~ "lsass.exe"
// Exclude known legitimate callers — TUNE: add your security products
| where not(InitiatingProcessFileName in~ (
"MsMpEng.exe", "SenseNdr.exe", "csrss.exe", "wininit.exe",
"services.exe", "taskhost.exe", "taskhostw.exe",
"lsm.exe", "svchost.exe", "WerFault.exe"
))
| project Timestamp, DeviceName, AccountName,
InitiatingProcessFileName, InitiatingProcessId,
InitiatingProcessCommandLine, FileName
| order by Timestamp desc
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=10
TargetImage="*lsass.exe"
| where NOT match(SourceImage,
"(?i)MsMpEng|SenseNdr|csrss|wininit|services|taskhost|lsm|svchost|WerFault")
| eval granted_access=GrantedAccess
| eval high_risk=if(
match(granted_access,"0x1010|0x1038|0x143a|0x1f3fff|0x1f1fff"),
"true","false"
)
| stats count by Computer, User, SourceImage, SourceProcessId,
TargetImage, GrantedAccess, high_risk
| eval severity=if(high_risk="true","critical","high")
| sort - severity
| table Computer User SourceImage SourceProcessId TargetImage GrantedAccess severity count
3. Password Spray Detection¶
Spraying attempts a single password against many accounts to avoid lockout thresholds.
// TA0006 — Credential Access: Password spray
// TUNE: failed_threshold and account_threshold per environment
let failed_threshold = 5; // TUNE: failed attempts per account
let account_threshold = 10; // TUNE: distinct accounts targeted
let time_window = 30m;
IdentityLogonEvents
| where Timestamp > ago(1d)
| where LogonType == "Failed"
| summarize
FailedCount = count(),
DistinctAccounts = dcount(AccountUpn),
AccountList = make_set(AccountUpn, 20)
by IPAddress, bin(Timestamp, time_window)
| where DistinctAccounts >= account_threshold
| project Timestamp, IPAddress, DistinctAccounts, FailedCount, AccountList
| order by DistinctAccounts desc
index=authentication sourcetype=WinEventLog:Security EventCode=4625
Logon_Type=3
| bin _time span=30m
| stats dc(TargetUserName) as distinct_accounts
count as total_failures
values(IpAddress) as src_ips
by _time, IpAddress
| where distinct_accounts >= 10 `comment("TUNE: account threshold")`
| eval severity=case(
distinct_accounts>=50, "critical",
distinct_accounts>=20, "high",
true(), "medium"
)
| sort - distinct_accounts
| table _time IpAddress distinct_accounts total_failures src_ips severity
4. DCSync Attack (Event 4662 with Replication Rights)¶
DCSync abuses domain replication permissions to pull password hashes without touching LSASS.
// TA0006 — Credential Access: DCSync — replication rights abuse
IdentityDirectoryEvents
| where Timestamp > ago(1d)
| where ActionType == "Directory Service Replication"
| where AdditionalFields has_any (
"1131f6aa-9c07-11d1-f79f-00c04fc2dcd2", // DS-Replication-Get-Changes
"1131f6ad-9c07-11d1-f79f-00c04fc2dcd2", // DS-Replication-Get-Changes-All
"89e95b76-444d-4c62-991a-0facbeda640c" // DS-Replication-Get-Changes-In-Filtered-Set
)
| where not(AccountDisplayName has_any ("MSOL_", "AADConnect"))
| project Timestamp, AccountDisplayName, AccountUpn,
DeviceName, IPAddress, ActionType, AdditionalFields
| order by Timestamp desc
index=wineventlog sourcetype=WinEventLog:Security EventCode=4662
| where ObjectType="%{19195a5b-6da0-11d0-afd3-00c04fd930c9}"
| where match(Properties,
"1131f6aa-9c07-11d1-f79f-00c04fc2dcd2|1131f6ad-9c07-11d1-f79f-00c04fc2dcd2|89e95b76-444d-4c62-991a-0facbeda640c")
| where NOT match(SubjectUserName,"(?i)MSOL_|AADConnect|\$$")
| stats count by Computer, SubjectUserName, SubjectLogonId, ObjectName, Properties
| eval severity="critical"
| table Computer SubjectUserName SubjectLogonId ObjectName Properties severity count
TA0007 — Discovery¶
1. Active Directory Enumeration (ADFind, BloodHound, nltest)¶
Attackers use these tools to map domain structure before lateral movement.
// TA0007 — Discovery: AD enumeration tools
DeviceProcessEvents
| where Timestamp > ago(1d)
| where (
FileName =~ "adfind.exe"
or (FileName =~ "nltest.exe" and ProcessCommandLine has_any (
"/dclist", "/domain_trusts", "/trusted_domains", "/all_trusts"
))
or (FileName in~ ("net.exe", "net1.exe") and ProcessCommandLine has_any (
"group /domain", "user /domain", "accounts /domain"
))
or ProcessCommandLine has_any (
"Invoke-BloodHound", "SharpHound", "bloodhound-python",
"PowerView", "Get-NetDomain", "Get-ADDomain"
)
)
| project Timestamp, DeviceName, AccountName, FileName,
ProcessCommandLine, InitiatingProcessFileName
| order by Timestamp desc
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=1
| eval ad_enum=case(
match(Image,"(?i)adfind"), "adfind",
match(Image,"(?i)nltest") AND match(CommandLine,"(?i)dclist|domain_trusts|trusted_domains"), "nltest",
match(Image,"(?i)net(1)?\.exe") AND match(CommandLine,"(?i)group /domain|user /domain"), "net_domain",
match(CommandLine,"(?i)Invoke-BloodHound|SharpHound|PowerView|Get-NetDomain"), "bloodhound_or_powerview",
true(), null()
)
| where isnotnull(ad_enum)
| stats count by Computer, User, Image, CommandLine, ad_enum
| eval severity="high"
| table Computer User Image CommandLine ad_enum severity count
2. Network Scanning (Mass ICMP/SYN from Single Host)¶
Internal hosts initiating connections to large numbers of unique IPs indicate scanning activity.
// TA0007 — Discovery: Network scanning (internal host → many destinations)
// TUNE: ip_threshold — lower in flat networks
let ip_threshold = 50; // TUNE: distinct IPs scanned in window
let time_window = 10m;
DeviceNetworkEvents
| where Timestamp > ago(1d)
| where ActionType in ("ConnectionAttempt", "NetworkSignatureInspected")
| where RemoteIPType == "Private"
| summarize
DistinctIPs = dcount(RemoteIP),
DistinctPorts = dcount(RemotePort),
TotalConnections = count()
by DeviceName, LocalIP, bin(Timestamp, time_window)
| where DistinctIPs >= ip_threshold
| project Timestamp, DeviceName, LocalIP,
DistinctIPs, DistinctPorts, TotalConnections
| order by DistinctIPs desc
index=network sourcetype=cisco:asa OR sourcetype=palo:traffic
| bin _time span=10m
| stats dc(dest_ip) as distinct_dests
dc(dest_port) as distinct_ports
count as total_conns
by _time, src_ip
| where distinct_dests >= 50 `comment("TUNE: IP scan threshold")`
| eval scan_type=case(
distinct_ports==1,"host_sweep",
distinct_dests==1,"port_scan",
true(),"combined_scan"
)
| eval severity=if(distinct_dests>=200,"critical","high")
| sort - distinct_dests
| table _time src_ip distinct_dests distinct_ports total_conns scan_type severity
3. Credential File Hunting (dir /s pass, findstr /si password)¶
Attackers search the filesystem for plaintext credential files.
// TA0007 — Discovery: Credential file searching
DeviceProcessEvents
| where Timestamp > ago(1d)
| where FileName in~ ("cmd.exe", "powershell.exe", "pwsh.exe")
| where ProcessCommandLine has_any (
"pass", "cred", "secret", "config", "web.config",
"unattend", "sysprep", ".vnc", "ntds.dit", "SAM", "SYSTEM"
)
| where ProcessCommandLine has_any (
"dir /s", "findstr", "Get-ChildItem", "ls -r",
"where /r", "type ", "Select-String"
)
| project Timestamp, DeviceName, AccountName, FileName,
ProcessCommandLine, InitiatingProcessFileName
| order by Timestamp desc
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=1
| where match(Image,"(?i)cmd\.exe|powershell")
| where match(CommandLine,
"(?i)(dir /s|findstr /si|Get-ChildItem|Select-String).*(pass|cred|secret|web\.config|unattend|sysprep|ntds|\.vnc)")
OR match(CommandLine,
"(?i)(pass|cred|secret|web\.config|unattend|sysprep|ntds|\.vnc).*(dir /s|findstr|Get-ChildItem)")
| stats count by Computer, User, Image, CommandLine
| eval severity="medium"
| table Computer User Image CommandLine severity count
TA0008 — Lateral Movement¶
1. Pass-the-Hash (NTLM Logon Type 3 from Non-Standard Host)¶
PtH uses NTLM authentication without the plaintext password. Type 3 logins from unusual sources flag this.
// TA0008 — Lateral Movement: Pass-the-Hash via NTLM
IdentityLogonEvents
| where Timestamp > ago(1d)
| where LogonType == "Network"
| where Protocol == "NTLM"
| where LogonType != "Failed"
// Exclude machine accounts and service accounts — TUNE: add your service accounts
| where not(AccountName endswith "$")
// Exclude known admin workstations — TUNE: populate AdminHosts list
| where not(DeviceName in~ (dynamic(["AdminWS01", "JumpBox01"])))
| project Timestamp, AccountName, DeviceName, IPAddress, LogonType, Protocol
| order by Timestamp desc
index=wineventlog sourcetype=WinEventLog:Security EventCode=4624
Logon_Type=3 Authentication_Package=NTLM
| where NOT match(TargetUserName,"(?i)\$$|ANONYMOUS LOGON")
| stats count as logon_count
dc(ComputerName) as dest_count
values(ComputerName) as destinations
by IpAddress, TargetUserName, WorkstationName
| where dest_count > 2 `comment("TUNE: lateral spread threshold")`
| eval severity=if(dest_count>5,"high","medium")
| sort - dest_count
| table IpAddress TargetUserName WorkstationName dest_count logon_count destinations severity
2. PsExec Lateral Movement (ADMIN$ Share + Service Creation)¶
PsExec writes a service binary to ADMIN$ and creates a service remotely.
// TA0008 — Lateral Movement: PsExec indicators
// Combine file write to ADMIN$ and service creation from same source
let psexec_share_write =
DeviceFileEvents
| where Timestamp > ago(1d)
| where FolderPath startswith @"\\ADMIN$"
| where FileName endswith ".exe"
| project DeviceId, TargetDeviceName = DeviceName,
SourceIP = RemoteIP, WrittenFile = FileName, FileWriteTime = Timestamp;
DeviceEvents
| where Timestamp > ago(1d)
| where ActionType == "ServiceInstalled"
| where FolderPath contains "PSEXESVC" or FileName contains "PSEXESVC"
| join kind=inner psexec_share_write on DeviceId
| project Timestamp, DeviceName, TargetDeviceName, SourceIP,
WrittenFile, FileName, FolderPath
| order by Timestamp desc
index=wineventlog sourcetype=WinEventLog:Security EventCode=5145
| where ShareName="\\\\*\\ADMIN$"
| where match(RelativeTargetName,"(?i)\.exe$")
| join type=inner Computer [
search index=wineventlog sourcetype=WinEventLog:Security EventCode=7045
| where match(ServiceFileName,"(?i)PSEXESVC|psexec")
| rename Computer as Computer
]
| stats count by Computer, SubjectUserName, IpAddress, RelativeTargetName, ServiceName
| eval severity="critical"
| table Computer SubjectUserName IpAddress RelativeTargetName ServiceName severity count
3. WinRM Abuse (wsmprovhost.exe Spawning Commands)¶
WinRM (Windows Remote Management) enables remote PowerShell — legitimate but often abused.
// TA0008 — Lateral Movement: WinRM abuse via wsmprovhost.exe
DeviceProcessEvents
| where Timestamp > ago(1d)
| where InitiatingProcessFileName =~ "wsmprovhost.exe"
| where FileName in~ (
"cmd.exe", "powershell.exe", "pwsh.exe",
"net.exe", "net1.exe", "whoami.exe",
"ipconfig.exe", "systeminfo.exe"
)
| project Timestamp, DeviceName, AccountName, FileName,
ProcessCommandLine, InitiatingProcessFileName
| order by Timestamp desc
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=1
| where ParentImage LIKE "%wsmprovhost.exe"
| where Image IN (
"C:\\Windows\\System32\\cmd.exe",
"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
"C:\\Windows\\System32\\net.exe",
"C:\\Windows\\System32\\whoami.exe",
"C:\\Windows\\System32\\systeminfo.exe"
)
| stats count by Computer, User, Image, CommandLine, ParentImage
| eval severity="high"
| table Computer User Image CommandLine ParentImage severity count
4. RDP from Unexpected Source¶
Flag RDP connections from external IPs or internal hosts that don't normally use RDP.
// TA0008 — Lateral Movement: RDP from unexpected source
// TUNE: populate known_jump_boxes with your legitimate RDP sources
let known_jump_boxes = dynamic(["10.1.1.10", "10.1.1.11"]);
DeviceNetworkEvents
| where Timestamp > ago(1d)
| where RemotePort == 3389 or LocalPort == 3389
| where ActionType == "InboundConnectionAccepted"
| where not(RemoteIP in (known_jump_boxes))
| where RemoteIPType != "Loopback"
| project Timestamp, DeviceName, LocalIP, RemoteIP,
LocalPort, RemotePort, InitiatingProcessFileName
| order by Timestamp desc
index=network sourcetype=cisco:asa OR sourcetype=palo:traffic
dest_port=3389 action=allow
| lookup jump_box_whitelist src_ip OUTPUT is_jump_box
| where is_jump_box!="true" OR isnull(is_jump_box)
| stats count as rdp_sessions
dc(dest_ip) as dest_count
values(dest_ip) as destinations
by src_ip, bin(_time, 1h)
| eval severity=case(
NOT match(src_ip,"^10\.|^172\.(1[6-9]|2[0-9]|3[01])\.|^192\.168\."),"critical",
dest_count > 5, "high",
true(),"medium"
)
| sort - severity
| table _time src_ip dest_count rdp_sessions destinations severity
TA0009 — Collection¶
1. Mass File Access (Thousands of File Opens in Short Window)¶
Ransomware and data staging both exhibit high-volume file access patterns.
// TA0009 — Collection: Mass file access (staging or ransomware pre-cursor)
// TUNE: file_access_threshold
let file_access_threshold = 500; // TUNE: files opened per minute
DeviceFileEvents
| where Timestamp > ago(1d)
| where ActionType in ("FileRead", "FileAccessed")
| summarize
FileCount = count(),
UniqueFiles = dcount(FileName),
Extensions = make_set(tostring(extract(@"\.([^.]+)$", 1, FileName)), 20)
by DeviceName, AccountName, InitiatingProcessFileName,
bin(Timestamp, 1m)
| where FileCount >= file_access_threshold
| project Timestamp, DeviceName, AccountName,
InitiatingProcessFileName, FileCount, UniqueFiles, Extensions
| order by FileCount desc
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=11 OR EventCode=15
| bin _time span=1m
| stats count as file_ops
dc(TargetFilename) as unique_files
values(eval(replace(TargetFilename,".*\.","."))) as extensions
by _time, Computer, User, Image
| where file_ops >= 500 `comment("TUNE: file access threshold")`
| eval severity=if(file_ops>=2000,"critical","high")
| sort - file_ops
| table _time Computer User Image file_ops unique_files extensions severity
2. Browser Credential Theft (Accessing Credential Store Files)¶
Infostealers target browser credential databases: Chrome Login Data, Firefox logins.json, Edge Login Data.
// TA0009 — Collection: Browser credential file access
DeviceFileEvents
| where Timestamp > ago(1d)
| where ActionType in ("FileRead", "FileAccessed", "FileCopied")
| where FolderPath has_any (
@"Chrome\User Data\Default\Login Data",
@"Edge\User Data\Default\Login Data",
@"Firefox\Profiles",
@"Opera\User Data\Default\Login Data",
@"Brave-Browser\User Data\Default\Login Data"
)
| where FolderPath has_any ("Login Data", "logins.json", "key4.db", "cookies")
// Exclude the browsers themselves — TUNE: add your enterprise browser path
| where not(InitiatingProcessFileName in~ (
"chrome.exe", "msedge.exe", "firefox.exe", "opera.exe", "brave.exe"
))
| project Timestamp, DeviceName, AccountName, FolderPath,
InitiatingProcessFileName, InitiatingProcessCommandLine
| order by Timestamp desc
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=11
| where match(TargetFilename,
"(?i)Chrome.User Data.Default.Login Data|Firefox.Profiles.logins\.json|Edge.User Data.Default.Login Data|key4\.db")
| where NOT match(Image,"(?i)chrome\.exe|msedge\.exe|firefox\.exe|opera\.exe|brave\.exe")
| stats count by Computer, User, Image, TargetFilename
| eval severity="critical"
| table Computer User Image TargetFilename severity count
3. Clipboard Data Theft¶
Malware may access clipboard contents to steal copied passwords and sensitive data.
// TA0009 — Collection: Clipboard access by unexpected process
DeviceEvents
| where Timestamp > ago(1d)
| where ActionType == "ClipboardDataRead" or ActionType == "GetClipboardData"
// Exclude common clipboard-using apps — TUNE: add your productivity tools
| where not(InitiatingProcessFileName in~ (
"explorer.exe", "notepad.exe", "WINWORD.exe",
"EXCEL.exe", "outlook.exe", "Teams.exe",
"Code.exe", "WindowsTerminal.exe"
))
| project Timestamp, DeviceName, AccountName,
InitiatingProcessFileName, InitiatingProcessCommandLine,
AdditionalFields
| order by Timestamp desc
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=1
| where match(CommandLine,"(?i)Get-Clipboard|GetClipboardData|clip\.exe|xclip|xsel")
| where NOT match(Image,
"(?i)explorer\.exe|notepad\.exe|WINWORD|EXCEL|outlook|Teams|Code\.exe")
| stats count by Computer, User, Image, CommandLine
| eval severity="medium"
| table Computer User Image CommandLine severity count
TA0010 — Exfiltration¶
1. Large Outbound Transfer to Uncommon Destination¶
Unusually large data transfers to new or infrequent external IPs are a primary exfiltration indicator.
// TA0010 — Exfiltration: Large outbound transfer to uncommon destination
// TUNE: bytes_threshold (default 50 MB per connection in 1h)
let bytes_threshold = 52428800; // TUNE: 50 MB
let baseline_window = 14d;
let known_destinations =
DeviceNetworkEvents
| where Timestamp between (ago(baseline_window) .. ago(1d))
| where RemoteIPType == "Public"
| summarize KnownIPs = make_set(RemoteIP) by DeviceName;
DeviceNetworkEvents
| where Timestamp > ago(1d)
| where RemoteIPType == "Public"
| where SentBytes >= bytes_threshold
| join kind=leftouter known_destinations on DeviceName
| where not(set_has_element(KnownIPs, RemoteIP))
| project Timestamp, DeviceName, AccountName,
RemoteIP, RemoteUrl, RemotePort,
SentBytes, InitiatingProcessFileName, InitiatingProcessCommandLine
| order by SentBytes desc
index=network sourcetype=cisco:asa OR sourcetype=palo:traffic
direction=outbound
| bin _time span=1h
| stats sum(bytes_out) as total_bytes
dc(dest_ip) as dest_count
values(dest_ip) as destinations
by _time, src_ip, src_user
| where total_bytes > 52428800 `comment("TUNE: 50MB threshold")`
| eval total_mb=round(total_bytes/1024/1024, 2)
| lookup known_dest_baseline src_ip dest_ip OUTPUT is_known
| where is_known!="true" OR isnull(is_known)
| eval severity=if(total_mb>500,"critical","high")
| sort - total_bytes
| table _time src_ip src_user total_mb dest_count destinations severity
2. rclone / MEGASync / Cloud Storage Exfiltration Tools¶
Known data exfiltration tools appearing on endpoints warrant immediate investigation.
// TA0010 — Exfiltration: Cloud sync/exfil tools
DeviceProcessEvents
| where Timestamp > ago(1d)
| where FileName in~ (
"rclone.exe", "MEGAsync.exe", "mega-cmd.exe",
"gdrive.exe", "dropbox.exe", "OneDriveSetup.exe",
"WinSCP.exe", "FileZilla.exe", "pscp.exe",
"putty.exe", "psftp.exe"
)
// Exclude known IT managed instances — TUNE: add your managed software hashes
| where not(
FileName =~ "OneDriveSetup.exe"
and InitiatingProcessFileName =~ "MicrosoftEdgeUpdate.exe"
)
| project Timestamp, DeviceName, AccountName, FileName,
ProcessCommandLine, FolderPath, InitiatingProcessFileName
| order by Timestamp desc
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=1
| eval exfil_tool=case(
match(Image,"(?i)rclone\.exe"), "rclone",
match(Image,"(?i)MEGAsync|mega-cmd"), "MEGA",
match(Image,"(?i)gdrive\.exe"), "Google_Drive_CLI",
match(Image,"(?i)WinSCP"), "WinSCP",
match(Image,"(?i)FileZilla"), "FileZilla",
match(Image,"(?i)pscp\.exe|psftp\.exe"), "PuTTY_SCP",
true(), null()
)
| where isnotnull(exfil_tool)
| stats count by Computer, User, Image, CommandLine, exfil_tool
| eval severity="high"
| table Computer User Image CommandLine exfil_tool severity count
3. DNS Exfiltration (High-Entropy DNS Queries)¶
Attackers encode data in DNS query subdomains. High entropy and unusual TXT record activity are key signals.
// TA0010 — Exfiltration: DNS exfiltration via high-entropy subdomains
// TUNE: entropy_threshold — typical legitimate domains score < 3.5
let entropy_threshold = 3.8; // TUNE: Shannon entropy cutoff
DeviceNetworkEvents
| where Timestamp > ago(1d)
| where RemotePort == 53
| extend QueryName = tostring(parse_json(AdditionalFields).DnsQueryString)
| where isnotempty(QueryName)
| extend SubLabel = tostring(split(QueryName, ".")[0])
| where strlen(SubLabel) > 20
| extend Entropy = log2(1.0) // placeholder — implement custom entropy UDF or use ml plugin
| where strlen(SubLabel) > 30 // length proxy for entropy when UDF unavailable
| summarize
QueryCount = count(),
UniqueDomains = dcount(QueryName),
Samples = make_set(QueryName, 5)
by DeviceName, AccountName, bin(Timestamp, 5m)
| where QueryCount > 20
| project Timestamp, DeviceName, AccountName, QueryCount, UniqueDomains, Samples
| order by QueryCount desc
index=dns sourcetype=stream:dns
| eval subdomain=mvindex(split(query,"."),0)
| eval sub_len=len(subdomain)
| where sub_len > 25
| eval char_set=length(replace(subdomain,"[^a-zA-Z0-9]",""))
| eval entropy_proxy=log(sub_len)/log(2)
| bin _time span=5m
| stats count as query_count
dc(query) as unique_queries
values(query) as sample_queries
by _time, src_ip, src_user
| where query_count > 20 AND unique_queries > 10
| eval severity=if(unique_queries>50,"critical","high")
| sort - query_count
| table _time src_ip src_user query_count unique_queries sample_queries severity
TA0011 — Command and Control¶
1. Beaconing Detection (Periodic Outbound Connections, Low Jitter)¶
C2 implants beacon at regular intervals. Calculate standard deviation of connection intervals per destination.
// TA0011 — C2: Beaconing detection via connection interval analysis
// TUNE: min_connections, max_stdev (lower = stricter)
let min_connections = 10; // TUNE: minimum connections to analyze
let max_stdev = 30.0; // TUNE: max seconds std deviation (lower = more regular)
DeviceNetworkEvents
| where Timestamp > ago(24h)
| where RemoteIPType == "Public"
| where ActionType in ("ConnectionSuccess", "ConnectionAttempt")
| summarize
ConnectionTimes = make_list(Timestamp),
ConnectionCount = count(),
BytesSent = sum(SentBytes)
by DeviceName, RemoteIP, RemotePort, InitiatingProcessFileName
| where ConnectionCount >= min_connections
| extend Intervals = series_subtract(
array_slice(ConnectionTimes, 1, -1),
array_slice(ConnectionTimes, 0, -2)
)
| extend MeanInterval = series_stats(Intervals).mean
| extend StdevInterval = series_stats(Intervals).stdev
| where StdevInterval < max_stdev
| where MeanInterval between (10 .. 3600) // 10 seconds to 1 hour
| project DeviceName, RemoteIP, RemotePort, InitiatingProcessFileName,
ConnectionCount, MeanInterval, StdevInterval, BytesSent
| order by StdevInterval asc
index=network sourcetype=cisco:asa OR sourcetype=palo:traffic
direction=outbound action=allow
| bin _time span=1s
| sort 0 src_ip dest_ip _time
| streamstats window=2 current=true
last(_time) as prev_time
by src_ip, dest_ip, dest_port
| eval interval=_time - prev_time
| where interval > 0
| stats count as conn_count
avg(interval) as mean_interval
stdev(interval) as stdev_interval
sum(bytes_out) as total_bytes
by src_ip, dest_ip, dest_port
| where conn_count >= 10
AND stdev_interval < 30 `comment("TUNE: jitter threshold in seconds")`
AND mean_interval BETWEEN 10 AND 3600
| eval beacon_score=round((1 - stdev_interval/mean_interval)*100, 1)
| where beacon_score > 70 `comment("TUNE: higher = more regular")`
| sort - beacon_score
| table src_ip dest_ip dest_port conn_count mean_interval stdev_interval beacon_score total_bytes
2. C2 over HTTPS to New Domain (Domain Age < 30 Days)¶
Adversaries register new domains for C2 to avoid reputation-based blocking.
// TA0011 — C2: HTTPS connections to newly registered domains
// Requires domain age enrichment via threat intelligence (TI) table
DeviceNetworkEvents
| where Timestamp > ago(1d)
| where RemotePort == 443
| where RemoteIPType == "Public"
| where isnotempty(RemoteUrl)
| extend Domain = tostring(parse_url(RemoteUrl).Host)
| join kind=inner (
ThreatIntelligenceIndicator
| where isnotempty(DomainName)
| where AdditionalInformation has "domain_age"
| extend DomainAge = toint(extract("domain_age:(\\d+)", 1, AdditionalInformation))
| where DomainAge < 30 // TUNE: days since registration
| project Domain = DomainName, DomainAge
) on Domain
| project Timestamp, DeviceName, AccountName, RemoteUrl,
Domain, RemoteIP, DomainAge, InitiatingProcessFileName
| order by DomainAge asc
index=proxy sourcetype=bluecoat:proxysg:access:kv OR sourcetype=squid
cs_uri_scheme=https
| rex field=cs_host "(?<domain>[^.]+\.[^.]+$)"
| lookup domaintools_lookup domain OUTPUT domain_age_days, create_date
| where domain_age_days < 30 OR (isnull(domain_age_days) AND isnotnull(domain))
| stats count as conn_count
sum(sc_bytes) as bytes_recv
sum(cs_bytes) as bytes_sent
by src_ip, cs_host, domain, domain_age_days, create_date
| where conn_count > 3
| eval severity=if(domain_age_days<7,"critical","high")
| sort - conn_count
| table src_ip cs_host domain domain_age_days create_date conn_count bytes_sent bytes_recv severity
3. Cobalt Strike Default Profile Detection (URI Patterns)¶
CS default malleable C2 profiles have recognizable URI patterns and User-Agent strings.
// TA0011 — C2: Cobalt Strike default profile indicators
DeviceNetworkEvents
| where Timestamp > ago(1d)
| where RemotePort in (80, 443, 8080, 8443)
| where RemoteUrl matches regex
@"(?i)(/submit\.php|/ca|/dpixel|/__utm\.gif|/pixel\.gif|/g\.pixel|/dot\.gif|/updates\.rss|/fwlink)"
or RemoteUrl matches regex
@"(?i)(/jquery-[0-9]+\.[0-9]+\.[0-9]+\.min\.js)"
| extend UserAgent = tostring(parse_json(AdditionalFields).HttpUserAgent)
| where UserAgent has_any (
"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)",
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)"
)
or RemoteUrl contains "/submit.php"
| project Timestamp, DeviceName, AccountName, RemoteUrl,
RemoteIP, RemotePort, UserAgent, InitiatingProcessFileName
| order by Timestamp desc
index=proxy sourcetype=bluecoat:proxysg:access:kv OR sourcetype=squid
| eval cs_uri=cs_uri_path
| eval cs_ua=cs_useragent
| eval cs_score=case(
match(cs_uri,"(?i)/submit\.php|/ca$|/dpixel|/__utm\.gif|/pixel\.gif|/dot\.gif"), 2,
match(cs_uri,"(?i)/jquery-[0-9]+\.[0-9]+\.[0-9]+\.min\.js") AND match(cs_ua,"(?i)MSIE [678]"), 3,
match(cs_ua,"Mozilla/4\.0 \(compatible; MSIE 8\.0; Windows NT 6\.1"), 2,
true(), 0
)
| where cs_score >= 2
| stats count sum(cs_score) as risk_score values(cs_uri) as uris by src_ip, cs_host, cs_ua
| eval severity=if(risk_score>=5,"critical","high")
| sort - risk_score
| table src_ip cs_host cs_ua risk_score uris severity count
TA0040 — Impact¶
1. VSS Deletion (Ransomware Pre-Cursor)¶
Volume Shadow Copy deletion prevents recovery. This is one of the most reliable ransomware pre-cursors.
Tier-1 Priority
VSS deletion should trigger an immediate escalation workflow. Do not wait for analyst triage.
// TA0040 — Impact: VSS deletion (ransomware pre-cursor)
DeviceProcessEvents
| where Timestamp > ago(1d)
| where (
(FileName =~ "vssadmin.exe" and ProcessCommandLine has_any ("delete", "resize"))
or (FileName in~ ("wmic.exe", "wbadmin.exe") and
ProcessCommandLine has_any ("shadowcopy delete", "delete catalog", "delete backup"))
or (FileName =~ "powershell.exe" and
ProcessCommandLine has_any ("Win32_Shadowcopy", "DeleteObject", "vssadmin"))
or (FileName =~ "bcdedit.exe" and
ProcessCommandLine has_any ("recoveryenabled no", "bootstatuspolicy ignoreallfailures"))
)
| project Timestamp, DeviceName, AccountName, FileName,
ProcessCommandLine, InitiatingProcessFileName
| order by Timestamp desc
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=1
| eval vss_del=case(
match(Image,"(?i)vssadmin") AND match(CommandLine,"(?i)delete|resize"), "vssadmin_delete",
match(Image,"(?i)wmic") AND match(CommandLine,"(?i)shadowcopy delete"), "wmic_shadow_delete",
match(Image,"(?i)wbadmin") AND match(CommandLine,"(?i)delete catalog|delete backup"), "wbadmin_delete",
match(Image,"(?i)bcdedit") AND match(CommandLine,"(?i)recoveryenabled no|ignoreallfailures"), "bcdedit_recovery_disable",
match(Image,"(?i)powershell") AND match(CommandLine,"(?i)Win32_Shadowcopy.*DeleteObject"), "ps_shadow_delete",
true(), null()
)
| where isnotnull(vss_del)
| stats count by Computer, User, Image, CommandLine, vss_del
| eval severity="critical"
| table Computer User Image CommandLine vss_del severity count
2. Bulk File Rename (Encryption in Progress)¶
Ransomware renames files as it encrypts them. A burst of renames with new extensions is a strong indicator.
// TA0040 — Impact: Bulk file rename (encryption in progress)
// TUNE: rename_threshold — lower triggers faster but may false-positive on archive tools
let rename_threshold = 200; // TUNE: file renames per minute
DeviceFileEvents
| where Timestamp > ago(1d)
| where ActionType == "FileRenamed"
| extend OldExt = tostring(extract(@"\.([^.]+)$", 1, PreviousFileName))
| extend NewExt = tostring(extract(@"\.([^.]+)$", 1, FileName))
| where OldExt != NewExt
// Exclude common safe extension changes — TUNE: add your legitimate rename patterns
| where not(NewExt in~ ("tmp", "bak", "old", "log"))
| summarize
RenameCount = count(),
UniqueExtensions = dcount(NewExt),
SampleNewExt = make_set(NewExt, 5),
AffectedFolders = dcount(FolderPath)
by DeviceName, AccountName, InitiatingProcessFileName,
bin(Timestamp, 1m)
| where RenameCount >= rename_threshold
| project Timestamp, DeviceName, AccountName,
InitiatingProcessFileName, RenameCount,
UniqueExtensions, SampleNewExt, AffectedFolders
| order by RenameCount desc
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=2
| eval old_ext=lower(replace(TargetFilename,".*\.",""))
| eval new_ext=lower(replace(Image,".*\.",""))
| eval old_ext=mvindex(split(TargetFilename,"."), -1)
| eval new_ext=mvindex(split(CreationUtcTime,"."), -1)
| bin _time span=1m
| stats count as rename_count
dc(TargetFilename) as unique_files
dc(old_ext) as src_exts
values(old_ext) as sample_src_ext
by _time, Computer, User, Image
| where rename_count >= 200 `comment("TUNE: rename threshold")`
| eval severity=if(rename_count>=500,"critical","high")
| sort - rename_count
| table _time Computer User Image rename_count unique_files sample_src_ext severity
3. Disk Wipe / MBR Overwrite Detection¶
Disk-wiping malware (Shamoon, WhisperGate) uses raw disk I/O or known wiper binaries.
// TA0040 — Impact: Disk wipe / MBR overwrite
DeviceProcessEvents
| where Timestamp > ago(1d)
| where (
ProcessCommandLine has_any (
"\\\\.\\PhysicalDrive", "\\\\.\\GLOBALROOT",
"Format-Volume", "Clear-Disk",
"dd if=/dev/zero", "sdelete"
)
or (FileName =~ "cipher.exe" and ProcessCommandLine has "/w")
or (FileName =~ "sdelete.exe" and ProcessCommandLine has "-z")
or ProcessCommandLine has_any ("bootrec /fixmbr", "bootrec /fixboot")
)
| project Timestamp, DeviceName, AccountName, FileName,
ProcessCommandLine, InitiatingProcessFileName
| order by Timestamp desc
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=1
| eval wipe_indicator=case(
match(CommandLine,"(?i)\\\\\\.\\\\PhysicalDrive|Format-Volume|Clear-Disk"), "raw_disk_access",
match(Image,"(?i)cipher\.exe") AND match(CommandLine,"/w"), "cipher_wipe",
match(Image,"(?i)sdelete") AND match(CommandLine,"-z"), "sdelete_wipe",
match(CommandLine,"(?i)bootrec /fixmbr|bootrec /fixboot"), "mbr_overwrite",
match(CommandLine,"(?i)dd if=/dev/zero"), "dd_zero",
true(), null()
)
| where isnotnull(wipe_indicator)
| stats count by Computer, User, Image, CommandLine, wipe_indicator
| eval severity="critical"
| table Computer User Image CommandLine wipe_indicator severity count
4. Service / System Shutdown Commands¶
Attackers disable recovery options and shut down systems as part of destructive operations.
// TA0040 — Impact: System shutdown or restart commands
DeviceProcessEvents
| where Timestamp > ago(1d)
| where (
(FileName =~ "shutdown.exe" and ProcessCommandLine has_any ("/s", "/r", "/f"))
or (FileName in~ ("powershell.exe", "pwsh.exe") and
ProcessCommandLine has_any ("Stop-Computer", "Restart-Computer"))
or (FileName =~ "wmic.exe" and
ProcessCommandLine has_any ("poweroff", "reboot"))
)
// Exclude known maintenance accounts — TUNE: populate maintenance_accounts
| where AccountName !in~ (dynamic(["SYSTEM", "svc_patching"]))
| project Timestamp, DeviceName, AccountName, FileName,
ProcessCommandLine, InitiatingProcessFileName
| order by Timestamp desc
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=1
| eval shutdown_cmd=case(
match(Image,"(?i)shutdown\.exe") AND match(CommandLine,"(?i)/s|/r|/f"), "shutdown_exe",
match(Image,"(?i)powershell") AND match(CommandLine,"(?i)Stop-Computer|Restart-Computer"), "ps_shutdown",
match(Image,"(?i)wmic") AND match(CommandLine,"(?i)poweroff|reboot"), "wmic_shutdown",
true(), null()
)
| where isnotnull(shutdown_cmd)
| where NOT match(User,"(?i)SYSTEM|svc_patching|svc_maintenance")
| stats count by Computer, User, Image, CommandLine, shutdown_cmd
| eval severity="high"
| table Computer User Image CommandLine shutdown_cmd severity count
Compound Detection Queries (Multi-Stage)¶
Multi-stage queries correlate events across tactics to detect attack chains with high confidence and low false-positive rates.
1. Ransomware Pre-Encryption Chain¶
Correlates VSS deletion, bulk file renames, and SMB lateral spread within a 30-minute window.
// COMPOUND — Ransomware pre-encryption chain
// Correlates: VSS delete + bulk rename + mass file access (same device, 30m window)
let time_window = 30m;
let vss_delete =
DeviceProcessEvents
| where Timestamp > ago(1d)
| where (FileName =~ "vssadmin.exe" and ProcessCommandLine has "delete")
or (FileName =~ "wmic.exe" and ProcessCommandLine has "shadowcopy delete")
| project DeviceId, DeviceName, VssTime = Timestamp, AccountName;
let bulk_rename =
DeviceFileEvents
| where Timestamp > ago(1d)
| where ActionType == "FileRenamed"
| summarize RenameCount = count() by DeviceId, DeviceName, bin(Timestamp, 1m)
| where RenameCount > 100
| project DeviceId, RenameTime = Timestamp, RenameCount;
let mass_access =
DeviceFileEvents
| where Timestamp > ago(1d)
| where ActionType in ("FileRead", "FileAccessed")
| summarize AccessCount = count() by DeviceId, DeviceName, bin(Timestamp, 1m)
| where AccessCount > 300
| project DeviceId, AccessTime = Timestamp, AccessCount;
vss_delete
| join kind=inner bulk_rename on DeviceId
| where abs(datetime_diff('minute', VssTime, RenameTime)) <= 30
| join kind=inner mass_access on DeviceId
| where abs(datetime_diff('minute', VssTime, AccessTime)) <= 30
| project VssTime, DeviceName, AccountName, RenameCount, AccessCount
| extend Alert = "RANSOMWARE_CHAIN_DETECTED"
| extend Severity = "CRITICAL"
| order by VssTime desc
`comment("COMPOUND — Ransomware pre-encryption chain")`
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
| eval chain_event=case(
EventCode=1 AND match(CommandLine,"(?i)vssadmin.*delete|wmic.*shadowcopy.*delete"), "vss_delete",
EventCode=2, "file_rename",
EventCode=11, "file_create",
true(), null()
)
| where isnotnull(chain_event)
| bin _time span=30m
| stats
values(eval(if(chain_event="vss_delete",CommandLine,null()))) as vss_cmds
count(eval(chain_event="file_rename")) as rename_count
count(eval(chain_event="file_create")) as create_count
dc(chain_event) as distinct_stages
by _time, Computer, User
| where isnotnull(vss_cmds) AND rename_count > 100 AND distinct_stages >= 2
| eval severity="critical"
| eval alert="RANSOMWARE_CHAIN_DETECTED"
| table _time Computer User vss_cmds rename_count create_count distinct_stages alert severity
2. Credential Theft Chain¶
Correlates LSASS access, subsequent network logon with captured credentials, and lateral movement.
// COMPOUND — Credential theft → lateral movement chain
// Stage 1: LSASS access | Stage 2: New network logon | Stage 3: Remote process
let lsass_access =
DeviceEvents
| where Timestamp > ago(1d)
| where ActionType == "OpenProcessApiCall"
| where FileName =~ "lsass.exe"
| where not(InitiatingProcessFileName in~ (
"MsMpEng.exe", "SenseNdr.exe", "csrss.exe", "wininit.exe"
))
| project DeviceId, DeviceName, LsassTime = Timestamp,
DumpProcess = InitiatingProcessFileName;
let lateral_logon =
IdentityLogonEvents
| where Timestamp > ago(1d)
| where LogonType == "Network"
| where Protocol == "NTLM"
| project AccountUpn, TargetDeviceName = DeviceName, LogonTime = Timestamp, IPAddress;
lsass_access
| join kind=inner (
lateral_logon
| project AccountUpn, TargetDeviceName, LogonTime
) on $left.DeviceName == $right.TargetDeviceName
| where LogonTime > LsassTime and LogonTime < LsassTime + 2h
| project LsassTime, DeviceName, DumpProcess,
AccountUpn, TargetDeviceName, LogonTime
| extend Alert = "CRED_THEFT_LATERAL_CHAIN"
| extend Severity = "CRITICAL"
| order by LsassTime desc
`comment("COMPOUND — Credential theft then lateral NTLM logon")`
(index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=10 TargetImage="*lsass.exe"
NOT SourceImage IN ("*MsMpEng.exe","*csrss.exe","*wininit.exe","*SenseNdr.exe"))
| eval stage="lsass_access", host_key=Computer
| append [
search index=wineventlog sourcetype=WinEventLog:Security
EventCode=4624 Logon_Type=3 Authentication_Package=NTLM
NOT TargetUserName="*$"
| eval stage="lateral_logon", host_key=WorkstationName
]
| sort _time
| streamstats window=50 count(eval(stage="lsass_access")) as lsass_count
count(eval(stage="lateral_logon")) as logon_count
by host_key
| where lsass_count >= 1 AND logon_count >= 1
| where stage="lateral_logon"
| eval alert="CRED_THEFT_LATERAL_CHAIN", severity="critical"
| stats values(eval(if(stage="lsass_access",SourceImage,null()))) as dump_tools
values(eval(if(stage="lateral_logon",TargetUserName,null()))) as accounts
count by host_key, alert, severity
| table host_key dump_tools accounts alert severity count
3. Exfiltration Chain¶
Correlates mass file access, a new outbound connection, and large data transfer within 1 hour.
// COMPOUND — Exfiltration chain
// Stage 1: Mass file access | Stage 2: New outbound dest | Stage 3: Large transfer
let mass_access =
DeviceFileEvents
| where Timestamp > ago(1d)
| where ActionType in ("FileRead", "FileAccessed")
| summarize AccessCount = count() by DeviceId, DeviceName, bin(Timestamp, 5m)
| where AccessCount > 200
| project DeviceId, DeviceName, AccessTime = Timestamp, AccessCount;
let new_outbound =
DeviceNetworkEvents
| where Timestamp > ago(1d)
| where RemoteIPType == "Public"
| where SentBytes > 10485760 // TUNE: 10 MB
| project DeviceId, RemoteIP, RemoteUrl,
SentBytes, NetTime = Timestamp, InitiatingProcessFileName;
mass_access
| join kind=inner new_outbound on DeviceId
| where NetTime > AccessTime and NetTime < AccessTime + 1h
| project AccessTime, NetTime, DeviceName, AccessCount,
RemoteIP, RemoteUrl, SentBytes, InitiatingProcessFileName
| extend SentMB = round(SentBytes / 1048576.0, 1)
| extend Alert = "EXFILTRATION_CHAIN_DETECTED"
| extend Severity = "CRITICAL"
| order by SentBytes desc
`comment("COMPOUND — Mass file access then large outbound transfer")`
(index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=11)
| bin _time span=5m
| stats count as file_access_count by _time, Computer, User
| where file_access_count > 200
| eval stage="mass_file_access"
| append [
search index=network sourcetype=palo:traffic direction=outbound
bytes_out > 10485760
| eval stage="large_transfer"
| eval Computer=src_ip
| bin _time span=5m
]
| sort Computer _time
| streamstats window=20 count(eval(stage="mass_file_access")) as access_stage
count(eval(stage="large_transfer")) as transfer_stage
by Computer
| where access_stage >= 1 AND transfer_stage >= 1 AND stage="large_transfer"
| eval alert="EXFILTRATION_CHAIN_DETECTED", severity="critical"
| stats values(dest_ip) as exfil_dests
sum(bytes_out) as total_bytes
by Computer, User, alert, severity
| eval total_mb=round(total_bytes/1024/1024, 2)
| table Computer User exfil_dests total_mb alert severity
Hunting Queries¶
Proactive threat hunting queries for scheduled or analyst-driven hunts. Run weekly or when threat intelligence indicates active campaigns.
1. Beaconing Analysis Using Jitter Calculation¶
Advanced beaconing hunt with full jitter score calculation across all endpoints.
// HUNT — Beaconing: full jitter analysis across all endpoints
// Run over 24h of data; may be slow on large environments — scope to subnet if needed
DeviceNetworkEvents
| where Timestamp > ago(24h)
| where RemoteIPType == "Public"
| where ActionType == "ConnectionSuccess"
| where RemotePort in (80, 443, 8080, 8443, 4444, 1337)
| summarize
Times = make_list(Timestamp, 500),
ConnCount = count()
by DeviceName, RemoteIP, RemotePort, InitiatingProcessFileName
| where ConnCount between (8 .. 500)
| extend Intervals = series_subtract(
array_slice(Times, 1, array_length(Times)-1),
array_slice(Times, 0, array_length(Times)-2)
)
| extend Stats = series_stats(Intervals)
| extend MeanSec = round(Stats.mean / 1e7, 1) // 100ns ticks → seconds
| extend StdevSec = round(Stats.stdev / 1e7, 1)
| extend JitterPct = round(iff(MeanSec > 0, StdevSec / MeanSec * 100, 100.0), 1)
| where JitterPct < 15 // TUNE: lower % = more regular beacon
| where MeanSec between (10 .. 7200)
| project DeviceName, RemoteIP, RemotePort, InitiatingProcessFileName,
ConnCount, MeanSec, StdevSec, JitterPct
| order by JitterPct asc
`comment("HUNT — Full beaconing analysis with jitter score")`
index=network sourcetype=palo:traffic OR sourcetype=cisco:asa
direction=outbound action=allow
| sort 0 src_ip dest_ip dest_port _time
| streamstats window=2 current=true last(_time) as prev_conn by src_ip, dest_ip, dest_port
| eval interval_s=_time - prev_conn
| where interval_s > 0
| stats count as conn_count
avg(interval_s) as mean_interval
stdev(interval_s) as stdev_interval
min(interval_s) as min_interval
max(interval_s) as max_interval
by src_ip, dest_ip, dest_port
| where conn_count >= 8
AND mean_interval BETWEEN 10 AND 7200
| eval jitter_pct=round(stdev_interval/mean_interval*100, 1)
| where jitter_pct < 15
| eval beacon_score=round(100 - jitter_pct, 1)
| sort - beacon_score
| table src_ip dest_ip dest_port conn_count mean_interval stdev_interval jitter_pct beacon_score
2. User Agent String Anomaly (Rare User Agents per Host)¶
Malware frequently uses hardcoded user agents that stand out against the normal browser UA landscape.
// HUNT — Rare user agent strings per host (bottom 1% of frequency)
let ua_frequency =
DeviceNetworkEvents
| where Timestamp > ago(7d)
| extend UA = tostring(parse_json(AdditionalFields).HttpUserAgent)
| where isnotempty(UA)
| summarize GlobalCount = count(), HostCount = dcount(DeviceName) by UA;
DeviceNetworkEvents
| where Timestamp > ago(1d)
| extend UA = tostring(parse_json(AdditionalFields).HttpUserAgent)
| where isnotempty(UA)
| join kind=inner ua_frequency on UA
| where HostCount <= 2 // TUNE: seen on <= N hosts in 7 days
| summarize
ConnectionCount = count(),
Destinations = make_set(RemoteUrl, 5)
by DeviceName, AccountName, UA, HostCount, InitiatingProcessFileName
| where ConnectionCount > 3
| order by HostCount asc
`comment("HUNT — Rare user agent strings (bottom frequency tail)")`
index=proxy sourcetype=bluecoat:proxysg:access:kv OR sourcetype=squid
| stats count as global_count dc(src_ip) as host_count by cs_useragent
| where global_count > 5 `comment("eliminate one-offs — focus on small but persistent UAs")`
| where host_count <= 2 `comment("TUNE: seen on very few hosts = suspicious")`
| sort host_count
| join cs_useragent [
search index=proxy sourcetype=bluecoat:proxysg:access:kv OR sourcetype=squid
| stats count values(cs_host) as domains by src_ip, cs_useragent
]
| eval severity="medium"
| table src_ip cs_useragent host_count global_count count domains severity
3. Rare Parent-Child Process Relationships¶
Builds a baseline of common parent→child pairs and surfaces anomalous combinations.
// HUNT — Rare parent-child process relationships (long-tail analysis)
let baseline_window = 14d;
let common_pairs =
DeviceProcessEvents
| where Timestamp between (ago(baseline_window) .. ago(1d))
| summarize PairCount = count()
by InitiatingProcessFileName, FileName
| where PairCount > 100; // TUNE: minimum occurrences to be "common"
DeviceProcessEvents
| where Timestamp > ago(1d)
| where isnotempty(InitiatingProcessFileName) and isnotempty(FileName)
| join kind=leftanti common_pairs on InitiatingProcessFileName, FileName
// Exclude noisy pairs that are inherently variable
| where not(InitiatingProcessFileName in~ ("explorer.exe", "svchost.exe"))
| summarize
OccurrenceCount = count(),
Devices = dcount(DeviceName),
SampleCommands = make_set(ProcessCommandLine, 3)
by InitiatingProcessFileName, FileName
| where OccurrenceCount < 5 // TUNE: truly rare
| order by OccurrenceCount asc
`comment("HUNT — Rare parent-child pairs via long-tail frequency analysis")`
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=1 earliest=-15d latest=-1d
| stats count as baseline_count by ParentImage, Image
| where baseline_count > 100
| rename ParentImage as parent_baseline Image as child_baseline
| appendcols [
search index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=1 earliest=-1d latest=now
| stats count as recent_count values(CommandLine) as sample_cmd by ParentImage, Image, Computer, User
]
| where isnull(baseline_count) AND recent_count > 0
| eval severity=if(recent_count<3,"high","medium")
| sort severity recent_count
| table ParentImage Image Computer User recent_count sample_cmd severity
4. Long-Tail Analysis: Processes Running for the First Time¶
Identifies processes that have never executed before on a given endpoint (within the observation window).
// HUNT — First-time process execution per device (long-tail)
// Compare last 24h against prior 30 days
let historical_procs =
DeviceProcessEvents
| where Timestamp between (ago(31d) .. ago(1d))
| summarize make_set(SHA256) by DeviceName, FileName;
DeviceProcessEvents
| where Timestamp > ago(1d)
| where isnotempty(SHA256)
| join kind=leftanti (
historical_procs
| mv-expand SHA256 = HistoricalHashes
| project DeviceName, FileName, SHA256 = tostring(SHA256)
) on DeviceName, SHA256
// Enrich with file reputation if available
| project Timestamp, DeviceName, AccountName,
FileName, SHA256, ProcessCommandLine,
FolderPath, InitiatingProcessFileName
| summarize FirstSeen = min(Timestamp), ExecCount = count()
by DeviceName, FileName, SHA256, FolderPath, InitiatingProcessFileName
| order by FirstSeen desc
`comment("HUNT — First-ever process execution per host (new binary)")`
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=1
| stats min(_time) as first_seen max(_time) as last_seen count by
Computer, Image, Hashes
| eval days_seen=round((last_seen - first_seen)/86400, 1)
| where first_seen > relative_time(now(), "-1d@d")
| where days_seen < 1 `comment("appeared today for the first time")`
| rex field=Hashes "SHA256=(?<sha256>[A-F0-9]{64})"
| lookup malware_hashes sha256 OUTPUT is_malicious, threat_name
| eval severity=case(
is_malicious="true", "critical",
days_seen < 0.1, "high",
true(), "medium"
)
| sort - severity
| table Computer Image sha256 first_seen days_seen is_malicious threat_name severity count
5. Geographic Impossibility (Same User in Two Countries Within 1 Hour)¶
Detects impossible travel — a sign of credential compromise or shared accounts.
// HUNT — Geographic impossibility (impossible travel)
// TUNE: max_travel_hours — 1h catches most; increase to 2h for edge cases
let max_travel_hours = 1;
IdentityLogonEvents
| where Timestamp > ago(7d)
| where isnotempty(Location) and isnotempty(AccountUpn)
| where LogonType != "Failed"
| project AccountUpn, Location, Timestamp, IPAddress, Application
| order by AccountUpn, Timestamp asc
| serialize
| extend PrevLocation = prev(Location, 1)
| extend PrevTime = prev(Timestamp, 1)
| extend PrevUser = prev(AccountUpn, 1)
| where AccountUpn == PrevUser
| where Location != PrevLocation
| extend HoursBetween = datetime_diff('hour', Timestamp, PrevTime)
| where HoursBetween >= 0 and HoursBetween <= max_travel_hours
| project AccountUpn, PrevLocation, Location,
PrevTime, Timestamp, HoursBetween, IPAddress
| order by HoursBetween asc
`comment("HUNT — Impossible travel: same user, 2 countries, <= 1 hour apart")`
index=authentication (sourcetype=okta OR sourcetype=azure:aad
OR sourcetype=WinEventLog:Security)
action=success
| eval user=coalesce(user, src_user, TargetUserName)
| iplocation src_ip
| where isnotnull(Country)
| sort user _time
| streamstats window=2 current=true
last(Country) as prev_country
last(_time) as prev_time
last(src_ip) as prev_ip
by user
| where isnotnull(prev_country)
| where Country != prev_country
| eval travel_minutes=round((_time - prev_time)/60, 0)
| where travel_minutes <= 60
| eval severity=if(travel_minutes<=30,"critical","high")
| stats count by user prev_country Country prev_ip src_ip prev_time _time travel_minutes severity
| sort travel_minutes
| table user prev_country Country prev_ip src_ip travel_minutes severity
Query Tuning Guide¶
Deploying detection queries without tuning generates alert fatigue and causes analysts to disable rules. Follow this process for every query deployed to production.
Setting Dynamic Thresholds¶
Use historical percentiles to anchor thresholds to your environment's actual baseline rather than fixed numbers.
// Dynamic threshold: calculate 95th percentile of file renames per process per hour
// Run this FIRST, then substitute the p95 value into the detection query
DeviceFileEvents
| where Timestamp between (ago(30d) .. ago(1d))
| where ActionType == "FileRenamed"
| summarize RenameCount = count()
by DeviceName, InitiatingProcessFileName, bin(Timestamp, 1h)
| summarize
p50 = percentile(RenameCount, 50),
p90 = percentile(RenameCount, 90),
p95 = percentile(RenameCount, 95),
p99 = percentile(RenameCount, 99)
// Use p95 as your TUNE threshold for the rename detection query
| tstats count as rename_count
from datamodel=Endpoint.Filesystem
where Filesystem.action=modified
by Filesystem.dest Filesystem.process_name _time span=1h
| stats perc50(rename_count) as p50
perc90(rename_count) as p90
perc95(rename_count) as p95
perc99(rename_count) as p99
`comment("Use p95 value as your TUNE threshold")`
Whitelisting by Host, User, or Process Path¶
Add exclusions as close to the data as possible (before summarization) to avoid performance degradation.
// Whitelist pattern — apply BEFORE summarize for best performance
// Populate the dynamic() lists from your CMDB or change management system
let whitelist_hosts = dynamic(["DC01", "DC02", "WSUS01", "SCCM01"]);
let whitelist_users = dynamic(["svc_backup", "svc_patching", "svc_monitoring"]);
let whitelist_paths = dynamic([
@"C:\Program Files\SolarWinds\",
@"C:\Program Files\Qualys\",
@"C:\Windows\CCM\"
]);
DeviceProcessEvents
| where Timestamp > ago(1d)
// Apply whitelists early
| where not(DeviceName in~ (whitelist_hosts))
| where not(AccountName in~ (whitelist_users))
| where not(FolderPath has_any (whitelist_paths))
// ... rest of detection logic
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=1
| where NOT Computer IN ("DC01","DC02","WSUS01","SCCM01")
| where NOT User IN ("svc_backup","svc_patching","svc_monitoring")
| where NOT match(Image,
"(?i)C:\\\\Program Files\\\\SolarWinds|C:\\\\Program Files\\\\Qualys|C:\\\\Windows\\\\CCM")
`comment("Apply additional detection logic below")`
| lookup detection_whitelist Computer User Image OUTPUT is_whitelisted
| where is_whitelisted!="true" OR isnull(is_whitelisted)
Adding Severity Scoring¶
Extend queries with a calculated severity score to enable automated triage routing in your SOAR platform.
// Severity scoring pattern — extend any detection query with this block
// Substitute your detection logic in place of the placeholder below
DeviceProcessEvents
| where Timestamp > ago(1d)
// ... detection logic ...
| extend Score = case(
// Process running from temp/appdata (+3)
FolderPath has_any (@"C:\Temp\", @"\AppData\Local\Temp\", @"\AppData\Roaming\"), 3,
// Process spawned by Office (+2)
InitiatingProcessFileName in~ (
"winword.exe", "excel.exe", "powerpnt.exe"
), 2,
// Encoded commands (+2)
ProcessCommandLine has_any ("-enc", "-EncodedCommand"), 2,
// Default
0
)
| extend Severity = case(
Score >= 5, "Critical",
Score >= 3, "High",
Score >= 1, "Medium",
"Low"
)
| where Severity != "Low" // TUNE: remove to see all
| project Timestamp, DeviceName, AccountName, FileName,
ProcessCommandLine, Score, Severity
| order by Score desc
index=endpoint sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
EventCode=1
`comment("Paste your detection filter here, then append the scoring block below")`
| eval score=0
| eval score=score + if(match(Image,"(?i)\\\\Temp\\\\|\\\\AppData\\\\"), 3, 0)
| eval score=score + if(match(ParentImage,"(?i)winword|excel|powerpnt"), 2, 0)
| eval score=score + if(match(CommandLine,"(?i)-enc|-EncodedCommand"), 2, 0)
| eval score=score + if(match(CommandLine,"(?i)bypass|hidden|noprofile"), 1, 0)
| eval severity=case(
score>=5, "critical",
score>=3, "high",
score>=1, "medium",
true(), "low"
)
| where severity!="low" `comment("TUNE: remove filter to see all")`
| sort - score
| table _time Computer User Image CommandLine score severity
Maintaining This Library
- Review all
-- TUNE:thresholds quarterly or after any major infrastructure change. - Track false-positive rates per query in your SOAR platform. Target < 5% FP rate before escalating alert priority.
- Cross-reference new threat intelligence with these queries monthly — update IOC lists and patterns as TTPs evolve.
- All queries follow MITRE ATT&CK v14 tactic/technique numbering.