move open esc close Searches the full text of every issue.

SOC Weekly Brief The week in the Microsoft security stack, distilled

← Home

KQL library

62 queries featured across the weekly issues. Each one links to the original in its author's repo — run them read-only first and tune the thresholds to your estate.

Multiple-Uncommon loaded image connection to suspicious domain

Two related Defender for Endpoint hunts from the same author, both keyed on a shared list of domain patterns that show up in tooling and exfiltration rather than in normal traffic: Burp Collaborator (oastify.com, portswigger.net), the whatismyip family, and generated-looking hostnames under CloudFront, KeyCDN, Azure Front Door, Azure cloudapp and AWS API Gateway. The first correlates network events with image load events on the same process, so it surfaces the loaded module behind the connection and filters on FileProfile prevalence and signature state to drop the common and the signed.

let query_frequency = 1h;
let query_period = 14d;
let suspicious_domains = dynamic([
    @"d\d[a-z0-9]{12}\.cloudfront\.net",
    @"[\-\w]+\-[a-f0-9]{3,5}\.kxcdn\.com",
    @"[\-\w]+\-[a-z0-9]{16}\.\w\d\d\.azurefd\.net",
    @"[\-\w]+\.[a-z0-9]+\.cloudapp\.azure\.com",
    @"[a-z0-9]{10}\.execute\-api\.[a-z]{2}\-[a-z]+\-\d\.amazonaws\.com",
    @"portswigger\.net",
    @"oastify\.com",
    @"whatismyip\.com",
    @"whatismyip\.net",
    @"whatismyipaddress\.com"
]);
let excluded_urls = dynamic([
    "uhf-exp-fd-gbcrdgggfbggh0g3.b02.azurefd.net",
    "vs-exp-afd-prod-endpoint-e3b9a0c0h0e8d6fd.b02.azurefd.net"
]);
let excluded_company_names = dynamic([]);
let excluded_original_names = dynamic([]);
DeviceNetworkEvents
| where Timestamp > ago(query_period)
| where RemoteUrl matches regex strcat_array(suspicious_domains, "|") // and not(InitiatingProcessAccountSid in ("S-1-5-18", "S-1-5-20"))
//| where not(RemoteUrl has_any (excluded_urls))
| where not(InitiatingProcessUniqueId == 0)
| project DeviceId, DeviceName, LocalIP, ActionType, RemoteIP, RemotePort, RemoteUrl, Protocol, InitiatingProcessUniqueId
| as _AuxiliarEvents
| join kind=inner (
    DeviceImageLoadEvents
    | where Timestamp > ago(query_period)
    | where not(InitiatingProcessUniqueId == 0) and (isnotempty(SHA1) or isnotempty(SHA256) or isnotempty(MD5)) and DeviceId in (toscalar(_AuxiliarEvents | summarize make_set(DeviceId)))
    | project-away DeviceName, ActionType
    ) on DeviceId, InitiatingProcessUniqueId
| project-away DeviceId1, InitiatingProcessUniqueId1
| summarize
    StartTime = arg_min(Timestamp, *),
    EndTime = max(Timestamp),
    DeviceNamesSample = array_sort_asc(make_set(DeviceName, 100)),
    RemoteUrlsSample = array_sort_asc(make_set(RemoteUrl, 100))
    by SHA1, SHA256, MD5
| where StartTime > ago(query_frequency)
| invoke FileProfile("SHA1", 1000)
| where not(GlobalPrevalence > 10000)
| where not(GlobalPrevalence > 1000 and GlobalFirstSeen < ago(query_period))
| where not(GlobalPrevalence > 1000 and GlobalFirstSeen < ago(query_frequency) and SignatureState == "SignedValid")
| where not(GlobalPrevalence > 500 and InitiatingProcessVersionInfoCompanyName in (excluded_company_names) and InitiatingProcessVersionInfoOriginalFileName in (excluded_original_names))
| where not(GlobalFirstSeen < ago(5 * 365d))
| project
    StartTime,
    EndTime,
    DeviceNamesSample,
    RemoteUrlsSample,
    Timestamp = StartTime,
    DeviceId,
    DeviceName,
    LocalIP,
    ActionType,
    RemoteIP,
    RemotePort,
    RemoteUrl,
    Protocol,
    FileName,
    FolderPath,
    SHA1,
    SHA256,
    MD5,
    FileSize,
    GlobalPrevalence,
    GlobalFirstSeen,
    GlobalLastSeen,
    SignatureState,
    InitiatingProcessAccountName,
    InitiatingProcessAccountSid,
    InitiatingProcessAccountUpn,
    InitiatingProcessAccountObjectId,
    InitiatingProcessFileName,
    InitiatingProcessFolderPath,
    InitiatingProcessCommandLine,
    InitiatingProcessCreationTime,
    IsInitiatingProcessRemoteSession,
    InitiatingProcessParentFileName,
    InitiatingProcessVersionInfoCompanyName,
    InitiatingProcessVersionInfoProductName,
    InitiatingProcessVersionInfoOriginalFileName,
    InitiatingProcessVersionInfoInternalFileName,
    InitiatingProcessVersionInfoFileDescription,
    InitiatingProcessVersionInfoProductVersion,
    InitiatingProcessUniqueId,
    ReportId

DeviceNetworkEvents-Uncommon process connection to suspicious domain

The companion query drops the image load join and groups by the initiating process's version metadata instead, which makes it the lighter of the two and the better starting point if you want to see which uncommon binaries in your estate are reaching those domains at all. It was published on 24 August, a few days ahead of the one above.

let query_frequency = 1h;
let query_period = 14d;
let suspicious_domains = dynamic([
    @"d\d[a-z0-9]{12}\.cloudfront\.net",
    @"[\-\w]+\-[a-f0-9]{3,5}\.kxcdn\.com",
    @"[\-\w]+\-[a-z0-9]{16}\.\w\d\d\.azurefd\.net",
    @"[\-\w]+\.[a-z0-9]+\.cloudapp\.azure\.com",
    @"[a-z0-9]{10}\.execute\-api\.[a-z]{2}\-[a-z]+\-\d\.amazonaws\.com",
    @"portswigger\.net",
    @"oastify\.com",
    @"whatismyip\.com",
    @"whatismyip\.net",
    @"whatismyipaddress\.com"
]);
let excluded_company_names = dynamic([]);
let excluded_original_names = dynamic([]);
DeviceNetworkEvents
| where Timestamp > ago(query_period)
| where RemoteUrl matches regex strcat_array(suspicious_domains, "|") // and not(InitiatingProcessAccountSid in ("S-1-5-18", "S-1-5-20"))
| where isnotempty(InitiatingProcessFileName)
| summarize
    StartTime = arg_min(Timestamp, *),
    EndTime = max(Timestamp),
    DeviceNamesSample = array_sort_asc(make_set(DeviceName, 100)),
    RemoteUrlsSample = array_sort_asc(make_set(RemoteUrl, 100))
    by InitiatingProcessVersionInfoCompanyName, InitiatingProcessVersionInfoProductName, InitiatingProcessVersionInfoOriginalFileName, InitiatingProcessVersionInfoInternalFileName, InitiatingProcessVersionInfoFileDescription
| where StartTime > ago(query_frequency)
| invoke FileProfile("InitiatingProcessSHA1", 1000)
| where not(GlobalPrevalence > 10000)
| where not(GlobalPrevalence > 1000 and GlobalFirstSeen < ago(query_frequency) and SignatureState == "SignedValid")
| where not(GlobalPrevalence > 500 and InitiatingProcessVersionInfoCompanyName in (excluded_company_names) and InitiatingProcessVersionInfoOriginalFileName in (excluded_original_names))
| project
    StartTime,
    EndTime,
    DeviceNamesSample,
    RemoteUrlsSample,
    Timestamp = StartTime,
    DeviceId,
    DeviceName,
    LocalIP,
    ActionType,
    RemoteIP,
    RemotePort,
    RemoteUrl,
    Protocol,
    InitiatingProcessAccountName,
    InitiatingProcessAccountSid,
    InitiatingProcessAccountUpn,
    InitiatingProcessAccountObjectId,
    InitiatingProcessSHA1,
    InitiatingProcessSHA256,
    InitiatingProcessMD5,
    InitiatingProcessFileName,
    InitiatingProcessFolderPath,
    InitiatingProcessCommandLine,
    InitiatingProcessCreationTime,
    IsInitiatingProcessRemoteSession,
    InitiatingProcessParentFileName,
    InitiatingProcessVersionInfoCompanyName,
    InitiatingProcessVersionInfoProductName,
    InitiatingProcessVersionInfoOriginalFileName,
    InitiatingProcessVersionInfoInternalFileName,
    InitiatingProcessVersionInfoFileDescription,
    InitiatingProcessVersionInfoProductVersion,
    GlobalPrevalence,
    GlobalFirstSeen,
    GlobalLastSeen,
    SignatureState,
    ReportId

Hunting CVE-2026-65400 Exploitation on macOS

Hunts Defender XDR telemetry for the macOS Screen Sharing attack path behind CVE-2026-65400, covering privileged Screen Sharing file transfer helpers, SSH session persistence, LaunchDaemon writes, process masquerading, Packet Filter changes and XMRig artefacts — useful for answering whether any of it was already happening before you patched.

let Days = 30d;
let KnownSHA256 = "84006055916e267f7c2f9324f1848563e589e4526a296d4e9e9ce8e2112d357c";
union
( // 1. Initial access / execution: Root Screen Sharing activity, SSH persistence, masquerading and PF modification.
DeviceInfo| where OSPlatform has 'MacOs' | join kind=inner DeviceProcessEvents on  DeviceId
    | where Timestamp > ago(Days)
    | where
        FileName =~ "SSFileCopyReceiver" or ProcessCommandLine has "sshd-session -i -R" or ProcessCommandLine has "exec -a com.apple.airportd"
        or ProcessCommandLine has "pfctl" or ProcessCommandLine has "com.xmr.miner.plist" or ProcessCommandLine has "sysmond"
    | extend HuntingSignal = case(
        FileName =~ "SSFileCopyReceiver", "Screen Sharing file transfer", ProcessCommandLine has "sshd-session -i -R",
            "SSH remote session activity", ProcessCommandLine has "exec -a com.apple.airportd", "Process masquerading",
        ProcessCommandLine has "pfctl", "Packet Filter modification", ProcessCommandLine has "com.xmr.miner.plist",
            "LaunchDaemon persistence", ProcessCommandLine has "sysmond", "Hidden miner execution", "Suspicious process activity"
    )
    | project Timestamp,DeviceName, ActionType, HuntingSignal, FileName, FolderPath, SHA256, ProcessCommandLine, InitiatingProcessFileName
),(// 2. File / persistence activity: SSH keys, hidden files, privileged paths and LaunchDaemon persistence
DeviceInfo| where OSPlatform has 'MacOs' | join kind=inner DeviceFileEvents on DeviceId
    | where Timestamp > ago(Days)
    | where SHA256 == KnownSHA256
        or FolderPath has "/private/var/root/.config" or FolderPath has "/Library/LaunchDaemons" or FolderPath has "/private/var/root/.ssh"
    | extend HuntingSignal = case(
        SHA256 == KnownSHA256, "Known XMRig SHA256",
        FolderPath has "/private/var/root/.config", "Hidden root file activity",
        FolderPath has "/Library/LaunchDaemons", "LaunchDaemon activity",
        FolderPath has "/private/var/root/.ssh", "Root SSH activity", "Suspicious file activity"
    )
    | project Timestamp, DeviceName, ActionType, HuntingSignal, FileName, FolderPath, SHA256,
        ProcessCommandLine = InitiatingProcessCommandLine,InitiatingProcessFileName
),
(// 3. Network activity: Observed mining pool plus SSH and Screen Sharing traffic.Ports 22/5900 are context only, not malicious by themselves.
 DeviceInfo| where OSPlatform has 'MacOs' | join kind=inner DeviceNetworkEvents on DeviceId
    | where Timestamp > ago(Days)
    | where RemoteUrl =~ "auto.c3pool.org" or RemotePort in (22, 5900)
    | extend HuntingSignal = case(
        RemoteUrl =~ "auto.c3pool.org", "Observed mining pool",
        RemotePort == 5900, "Screen Sharing / VNC traffic",
        RemotePort == 22, "SSH traffic","Suspicious network activity"
    )
    | project Timestamp, DeviceName, ActionType, HuntingSignal, FileName = InitiatingProcessFileName, FolderPath = "", SHA256 = InitiatingProcessSHA256,
        ProcessCommandLine = InitiatingProcessCommandLine, InitiatingProcessFileName
)

Suspicious SSH Tunneling and Config Exposure

Scores SSH port forwarding and tunnelling against a 30-day per-device baseline, weighting listener scope and persistent-shell flags, and pairs it with unauthorised sshd_config writes and GatewayPorts or PermitTunnel appearing on a command line.

let Lookback    = 7d;
let BaselineWin = 30d;
// Set to true to filter out noisy local development tunnels on loopback and focus on higher-risk activity
let HighFidelityOnly = true;
let FwdRegex = @"(^|\s)-[LDR] ?(\d{1,5}(:|\s|$)|\*:|0\.0\.0\.0:|localhost:|\[)";
let SshTunnelEvents = (StartTime:datetime, EndTime:datetime) {
    DeviceProcessEvents
    | where Timestamp between (StartTime .. EndTime)
    // Ignore sshd here, as -D and -R are standard daemon flags there
    | where FileName in~ ("ssh", "ssh.exe", "plink.exe", "putty.exe")
    | where ProcessCommandLine matches regex FwdRegex
    | extend TunnelFlag   = extract(@"(^|\s)-([LDR]) ?(\d|\*:|0\.0\.0\.0:|localhost:|\[)", 2, ProcessCommandLine)
    | extend ForwardSpecs = extract_all(@"(?:^|\s)-[LDR] ?(\S+)", ProcessCommandLine)
    | extend AllSpecs     = strcat_array(ForwardSpecs, " ")
    | extend SshTarget    = extract(@"\s([\w\.\-]+@[\w\.\-]+)", 1, ProcessCommandLine)
    | extend CmdPattern   = replace_regex(ProcessCommandLine, @"\b\d{4,5}\b", "<port>")
};
let TunnelBaseline =
    SshTunnelEvents(ago(BaselineWin + Lookback), ago(Lookback))
    | distinct DeviceName, AccountName, CmdPattern;
let TunnelFindings =
    SshTunnelEvents(ago(Lookback), now())
    | extend TunnelType = case(
        TunnelFlag == "L", "Local Port Forwarding (-L)",
        TunnelFlag == "D", "Dynamic Port Forwarding / SOCKS (-D)",
        TunnelFlag == "R", "Remote Port Forwarding (-R)",
        "SSH Tunneling / Other")
    // Explicit binding requires all four fields (bind:port:host:hostport)
    | extend ListenerScope = case(
        AllSpecs matches regex @"(^|\s)(\*|0\.0\.0\.0|::):",           "Bind to all interfaces",
        AllSpecs matches regex @"[\w\.\-]+:\d{1,5}:[\w\.\-]+:\d{1,5}", "Bind to explicit address",
        "Loopback (Default)")
    | extend PersistentTunnel = ProcessCommandLine matches regex @"(^|\s)-(fN|Nf|N|f)(\s|$)"
    // Regular local dev tunnels on loopback are excluded in High-Fidelity mode
    | where HighFidelityOnly == false
        or ListenerScope != "Loopback (Default)"
        or TunnelType startswith "Remote"
    | summarize
        EventCount     = count(),
        FirstSeen      = min(Timestamp),
        LastSeen       = max(Timestamp),
        SampleCmd      = any(ProcessCommandLine),
        Parents        = make_set(InitiatingProcessFileName, 8),
        SshTargets     = make_set_if(SshTarget, isnotempty(SshTarget), 8),
        ForwardSpecSet = make_set(AllSpecs, 8),
        ReportId       = any(ReportId),
        DeviceId       = any(DeviceId)
        by DeviceName, AccountName, InitiatingProcessAccountName, FileName,
           TunnelType, ListenerScope, PersistentTunnel, CmdPattern
    | join kind=leftanti TunnelBaseline on DeviceName, AccountName, CmdPattern
    | extend RiskScore =
          iff(ListenerScope == "Bind to all interfaces",   40, 0)
        + iff(ListenerScope == "Bind to explicit address", 20, 0)
        + iff(TunnelType startswith "Remote",                30, 0)
        + iff(PersistentTunnel,                               15, 0)
        + iff(TunnelType startswith "Dynamic",               10, 0)
    | project
        Category  = "SSH Port Forwarding",
        RiskScore,
        Signal    = TunnelType,
        Evidence  = strcat(TunnelType, " | ", ListenerScope,
                        iff(PersistentTunnel, " | persistent tunnel without shell (-N/-f)", "")),
        DeviceName,
        Account   = coalesce(AccountName, InitiatingProcessAccountName),
        EventCount,
        FirstSeen,
        LastSeen,
        CLI       = SampleCmd,
        SampleCmd,
        Details   = strcat("Specs: ", strcat_array(ForwardSpecSet, ", "),
                        iff(array_length(SshTargets) > 0, strcat(" | Targets: ", strcat_array(SshTargets, ", ")), ""),
                        " | Parent: ", strcat_array(Parents, ", ")),
        ReportId,
        DeviceId;
let ConfigWrites =
    DeviceFileEvents
    | where Timestamp > ago(Lookback)
    | where FileName =~ "sshd_config" or FolderPath has "/etc/ssh/sshd_config.d"
    | where ActionType in ("FileCreated", "FileModified", "FileRenamed")
    | project Timestamp, DeviceName, Account = InitiatingProcessAccountName,
              Cmd = InitiatingProcessCommandLine, Path = FolderPath,
              Signal = "sshd_config modified", Score = 35, ReportId, DeviceId;
let GatewayPortsCmd =
    DeviceProcessEvents
    | where Timestamp > ago(Lookback)
    | where ProcessCommandLine has_cs "GatewayPorts" or ProcessCommandLine has_cs "PermitTunnel"
    | where ProcessCommandLine !contains "GatewayPorts=no" and ProcessCommandLine !contains "GatewayPorts no"
    | project Timestamp, DeviceName, Account = InitiatingProcessAccountName,
              Cmd = ProcessCommandLine, Path = FolderPath,
              Signal = "GatewayPorts/PermitTunnel in command line", Score = 45, ReportId, DeviceId;
let ConfigFindings =
    union ConfigWrites, GatewayPortsCmd
    // Adjust admin and deployment accounts based on your environment
    | where Account !in~ ("puppet", "ansible", "salt")
        and Cmd !has "cloud-init" and Cmd !has "unattended-upgrade"
    | summarize
        EventCount = count(),
        FirstSeen  = min(Timestamp),
        LastSeen   = max(Timestamp),
        SampleCmd  = any(Cmd),
        Paths      = make_set(Path, 4),
        RiskScore  = max(Score),
        ReportId   = any(ReportId),
        DeviceId   = any(DeviceId)
        by DeviceName, Account, Signal
    | project
        Category  = "sshd_config Exposure",
        RiskScore,
        Signal,
        Evidence  = strcat(Signal, " by ", Account),
        DeviceName,
        Account,
        EventCount,
        FirstSeen,
        LastSeen,
        CLI       = SampleCmd,
        SampleCmd,
        Details   = strcat("Paths: ", strcat_array(Paths, ", ")),
        ReportId,
        DeviceId;
union TunnelFindings, ConfigFindings
| extend Severity = case(RiskScore >= 40, "High", RiskScore >= 20, "Medium", "Low")
| project-reorder Severity, RiskScore, Category, Evidence, DeviceName, Account,
              EventCount, FirstSeen, LastSeen, CLI, SampleCmd, Details
| sort by RiskScore desc, LastSeen desc

Suspicious LDAP Reconnaissance from Non-Compliant Devices

Correlates non-compliant or health-flagged Intune devices with LDAP reconnaissance from the same host, pairing device posture with directory enumeration.

let Lookback		= 1d;
let BinSize		= 15m;
let RiskyDevices = materialize(
	IntuneDeviceComplianceOrg
	| where TimeGenerated > ago(7d)
	| summarize arg_max(TimeGenerated, DeviceHealthThreatLevel, ComplianceState) by DeviceName
	| where DeviceHealthThreatLevel in~ ("Low", "Medium", "High")
		or ComplianceState =~ "Noncompliant"
	| extend DeviceKey = tolower(tostring(split(DeviceName, ".")[0]))
	| distinct DeviceKey, DeviceHealthThreatLevel, ComplianceState
);
// Attributes split by signal strength instead of a flat has_any
let RxHigh	= @"ms-mcs-admpwd|mslaps-(encrypted)?password|ntsecuritydescriptor|msds-allowedtodelegateto|msds-keycredentiallink|sidhistory|unixuserpassword";
let RxMed	= @"serviceprincipalname|admincount|msds-managedpassword|gplink|scriptpath";
let RxLow	= @"useraccountcontrol|memberof|member|primarygroupid|grouptype";
// LDAP matching rules and bitmask filters, almost exclusively seen from offensive tooling
let RxTool	= @"1\.2\.840\.113556\.1\.4\.1941|1\.2\.840\.113556\.1\.4\.803:=(4194304|524288|16777216|8192)";
IdentityQueryEvents
| where TimeGenerated > ago(Lookback)
| where ActionType == "LDAP query"
| where isnotempty(Query) and isnotempty(DeviceName)
| where AccountName !endswith "$"				// Computer accounts need their own baseline
| extend DeviceKey = tolower(tostring(split(DeviceName, ".")[0]))
| lookup kind=inner RiskyDevices on DeviceKey
| extend q = tolower(Query)
| extend
	ToolHit	= extract(RxTool, 0, q),
	Score	= iff(q matches regex RxHigh, 5, 0)
		+ iff(q matches regex RxMed,  2, 0)
		+ iff(q matches regex RxLow,  1, 0)
		+ iff(q matches regex RxTool, 5, 0)
		+ iff(q contains "objectcategory=person" and q contains "(&", 1, 0)
| where Score > 0
| summarize
	Events		= count(),
	DistinctQueries	= dcount(Query),
	Targets		= dcount(QueryTarget),
	MaxScore	= max(Score),
	TotalScore	= sum(Score),
	ToolIndicators	= make_set_if(ToolHit, isnotempty(ToolHit), 10),
	SampleQueries	= make_set(substring(Query, 0, 200), 8),
	IPs		= make_set(IPAddress, 5),
	TargetDCs	= make_set(DestinationDeviceName, 5),
	FirstSeen	= min(TimeGenerated),
	LastSeen	= max(TimeGenerated)
	by bin(TimeGenerated, BinSize), DeviceKey, AccountUpn, AccountDisplayName, DeviceHealthThreatLevel, ComplianceState
// Two separate triggers: single high confidence hit OR bulk enumeration
| where MaxScore >= 5 or (TotalScore >= 15 and DistinctQueries >= 20)
| extend Verdict = case(
	array_length(ToolIndicators) > 0,	"High: LDAP matching rule or bitmask filter, typical for offensive tooling",
	MaxScore >= 5,				        "High: access to highly sensitive AD attributes (LAPS/ACL/delegation)",
	DistinctQueries >= 50,			    "Medium: broad AD enumeration in a short time window",
						                "Low: elevated LDAP activity, review context")
| order by MaxScore desc, DistinctQueries desc

Azure Log Analytic Table Operation Audit

Audits write and delete operations against Log Analytics workspace tables, covering the tamper path that removes the evidence rather than the alert.

//For auditing Write/Delete actions upon LAW Tables
AzureActivity
| where TimeGenerated > ago(90d)
| where OperationNameValue contains "MICROSOFT.OPERATIONALINSIGHTS/WORKSPACES/TABLES/"
| extend TableName = Properties_d.resource
| project-reorder TimeGenerated, TableName, Caller

CloudAppEvents-No threats found by AIR investigation in phishing submission

Lists user-reported phishing submissions where automated investigation returned no threats found, the queue where genuine phishing quietly ends up.

CloudAppEvents
| where ActionType == "AirInvestigationData"
| extend
    Status = tostring(RawEventData["Status"]),
    InvestigationId = tostring(RawEventData["InvestigationId"]),
    InvestigationType = tostring(RawEventData["InvestigationType"]),
    InvestigationName = tostring(RawEventData["InvestigationName"]),
    Data = todynamic(tostring(RawEventData["Data"]))
| where InvestigationType == "SubmissionInvestigation" and InvestigationName has "User reported message as malicious" and Status == "No threats found"
| extend
    AlertType = tostring(Data["AlertType"]),
    AlertId = strcat("fa", tostring(Data["ProviderAlertId"])),
    InternetMessageId = tostring(Data["Entities"][0]["InternetMessageId"]),
    NetworkMessageId = tostring(Data["Entities"][0]["NetworkMessageId"]),
    SenderFromAddress = tostring(Data["Entities"][0]["Sender"]),
    RecipientEmailAddress = tostring(Data["Entities"][0]["Recipient"]),
    Subject = tostring(Data["Entities"][0]["Subject"])
| where AlertType == "b26a5770-0c38-434a-9380-3a3c2c27bbb3" and  isnotempty(NetworkMessageId) and isnotempty(SenderFromAddress) and isnotempty(RecipientEmailAddress) and isnotempty(Subject)

Catching emojis on email Subjects

Flags emoji in email subjects joined to URL click events, a filter-evasion trick that shows up in phishing waves.

// Sergio Albea 17-03-2026 ©️
EmailEvents
| where Timestamp > ago(7d)
| where isnotempty(Subject)
| extend Icons = extract_all(@"([\x{1F300}-\x{1FAFF}\x{2600}-\x{27BF}])", Subject)
| where isnotempty(Icons)
| join kind=inner UrlClickEvents on NetworkMessageId
| where UserLevelPolicy !has 'Allow' 
| where OrgLevelPolicy !has 'Allow'
| extend SenderIP = iff(isnotempty( SenderIPv4),SenderIPv4,SenderIPv6)
| extend geo_ip = tostring(geo_info_from_ip_address(SenderIP).country)
//| where Subject contains "⚠️" 
| summarize Distinct_Recipients=dcount(RecipientEmailAddress),make_set(RecipientEmailAddress),Emails=count() by Subject,SenderIP,geo_ip,ActionType, Workload, Url, ThreatTypes, LatestDeliveryLocation
| order by Emails, Distinct_Recipients

HUNT-09_New-SP-KeyVault-Access

Surfaces service principals that accessed a Key Vault for the first time in seven days, the probing step after a service principal is compromised.

// Hunt     : Hunt - Service Principals Accessing Key Vault for the First Time Within 7 Days
// Tactics  : CredentialAccess
// MITRE    : T1552.001
// Purpose  : Surfaces identities that accessed a Key Vault for the first time in the last 7 days. Compromised service principals and attacker-created SPs often probe Key Vaults for secrets/certs after initial access. Investigate any SP not expected to require KV access.
//==========================================================================================

let AllKVAccess = AzureActivity
    | where TimeGenerated > ago(90d)
    | where OperationNameValue startswith "MICROSOFT.KEYVAULT/VAULTS/"
    | where ActivityStatusValue =~ "Success"
    | extend VaultPath = tostring(strcat(SubscriptionId, "/", ResourceGroup, "/", tostring(split(ResourceId, "/")[8])))
    | summarize FirstAccessEver = min(TimeGenerated) by Caller, VaultPath;
AzureActivity
| where TimeGenerated > ago(7d)
| where OperationNameValue startswith "MICROSOFT.KEYVAULT/VAULTS/"
| where ActivityStatusValue =~ "Success"
| extend VaultPath = tostring(strcat(SubscriptionId, "/", ResourceGroup, "/", tostring(split(ResourceId, "/")[8])))
// FIX: join on BOTH Caller and VaultPath. The previous version joined on Caller only, so a
// caller newly accessing vault A would also surface all of their long-standing vault B access.
| join kind=inner AllKVAccess on Caller, VaultPath
| where FirstAccessEver > ago(7d)
| project TimeGenerated, Caller, Operation = OperationNameValue, ResourceId, CallerIpAddress, SubscriptionId, ResourceGroup, VaultPath, FirstAccessEver
| order by FirstAccessEver desc

Multiple-Ingestion delays

Measures the gap between ingestion times per table across the whole workspace, which is how you spot a connector that has silently gone quiet.

let query_period = 7d;
union *
| where TimeGenerated > ago(query_period)
| distinct Type, bin(IngestionTime = ingestion_time(), 30m)
| sort by Type asc, IngestionTime asc
| extend Difference = iff(Type == prev(Type), IngestionTime - prev(IngestionTime), 0s)
| summarize Frequency = max(Difference) by Type
| extend Frequency = iff(Frequency == 0s, query_period, Frequency)
| lookup kind=leftouter (
    union *
    | where TimeGenerated > ago(query_period)
    | summarize percentiles(IngestionDelay = ingestion_time() - TimeGenerated, 50, 80, 95, 99) by Type
    ) on Type

DeviceNetworkEvents-Suspicious process connection to cloudfront domain

Matches the CloudFront domain pattern used by several loader families where msiexec or an installer process is the parent.

DeviceNetworkEvents
| where RemoteUrl matches regex @"d\d[a-z0-9]{12}\.cloudfront\.net" and (InitiatingProcessParentFileName has "msiexec.exe" or InitiatingProcessCommandLine has "/Install")
| project
    TimeGenerated,
    DeviceName,
    LocalIP,
    ActionType,
    InitiatingProcessParentCreationTime,
    InitiatingProcessParentFileName,
    InitiatingProcessCreationTime,
    InitiatingProcessAccountUpn,
    InitiatingProcessFolderPath,
    InitiatingProcessCommandLine,
    Protocol,
    RemoteUrl,
    RemoteIP,
    RemotePort

Microsoft Entra Conditional Access Policy Exclusion Modification

Extracts the exclusion changes from Conditional Access policy create and update events, so a quietly added exempt user or group does not go unnoticed.

// CAP Exludes
AuditLogs
| where OperationName in ("Update conditional access policy", "Create conditional access policy")
| where Result == "success"
| mv-expand TargetResource = TargetResources
| mv-expand ModifiedProperty = TargetResource.modifiedProperties
| where ModifiedProperty.displayName == "PolicyDetail"
| extend OldPolicy = todynamic(tostring(ModifiedProperty.oldValue))
| extend NewPolicy = todynamic(tostring(ModifiedProperty.newValue))
// Extract Excludes
| extend OldExclusions = OldPolicy.conditions.users.excludeGroups
| extend NewExclusions = NewPolicy.conditions.users.excludeGroups
| extend OldExcludedUsers = OldPolicy.conditions.users.excludeUsers
| extend NewExcludedUsers = NewPolicy.conditions.users.excludeUsers
// check for added Excludes
| where array_length(NewExclusions) > array_length(OldExclusions) 
     or array_length(NewExcludedUsers) > array_length(OldExcludedUsers)
| project TimeGenerated, InitiatedBy = Identity, PolicyName = TargetResource.displayName, OldExclusions, NewExclusions, OldExcludedUsers, NewExcludedUsers

adminsignsinfronnewnetwork

Builds a list of admin accounts, learns the ASNs they normally sign in from, and flags admin sign-ins arriving from a new network.

let admins=(IdentityInfo
| where AssignedRoles contains "admin" or GroupMembership has "Admin"
| summarize by tolower(AccountUPN));
//admins
let known_asns = (
SigninLogs
| where TimeGenerated between(ago(14d)..ago(1d))
| where ResultType == 0
| summarize by AutonomousSystemNumber);
//known_asns
SigninLogs
| where TimeGenerated > ago(1d)
| where ResultType == 0
| where tolower(UserPrincipalName) in (admins)
| where AutonomousSystemNumber !in (known_asns)
| project-reorder TimeGenerated, UserPrincipalName, UserAgent, IPAddress, AutonomousSystemNumber
| extend AccountName = tostring(split(UserPrincipalName, "@")[0]), AccountUPNSuffix = tostring(split(UserPrincipalName, "@")[1])

Potential Entra Admin Synced back On-premise

Finds identities holding Entra admin roles that are synchronised from on-premises Active Directory, the hybrid dependency Microsoft has been advising teams to break.

IdentityInfo //Advanced Hunting table but can be ingested in sentinel
| where TimeGenerated > ago(30d) //Will capture user if any change occured in last 30 days to user
| where (isnotempty(AccountDomain))
| where (isnotempty(tostring(AssignedRoles)))
| where tostring(AssignedRoles) contains "admin"
| where IdentityEnvironment == @"Hybrid"
| sort by TimeGenerated desc
| summarize by AccountUpn, OnPremObjectId, tostring(AssignedRoles), AccountDomain

MDI Service Accounts without Service Principals and MSAs

Lists accounts Defender for Identity classifies as service accounts but that are neither service principals nor managed service accounts, the ones running on a password somebody set years ago.

IdentityInfo
| where Timestamp > ago(30d)
| where Type == @"ServiceAccount"
| extend ["Service On-Prem Sid"] = OnPremObjectId
| extend ["Service Principal Name"] = iff(IdentityEnvironment == "OnPremises",replace_string(strcat(AccountName, "@",AccountDomain),"$",""), AccountUpn )
| where parse_json(UserAccountControl)[0] != 'WorkstationTrustAccount' //Exclude gMSA/dMSA
| where not (ChangeSource == @"System-UserPersistence" and isempty(CloudSid)) //Exclude Service Principals
| summarize arg_max(Timestamp,*) by AccountObjectId,CloudSid,['Service On-Prem Sid']

Microsoft Dynamics 365 Privilege Escalation via Role or Team Modification

Correlates Dynamics 365 role and team modifications with off-network access inside a short window, aimed at privilege escalation in the business application layer.

let CorporateIPRange = "147.86.0.0/16";
let ThreatWindow = 10m;
let SuspiciousInquiries = 
    CloudAppEvents
    | where TimeGenerated > ago(1d)
    | where Application == "Microsoft Dynamics 365"
    | where not(ipv4_is_in_range(IPAddress, CorporateIPRange ))
    | where IsAdminOperation == 0
    | where ActionType in ("RetrieveUserPrivileges", "RetrieveUserPrivilegeByPrivilegeName", "RetrievePrivilegeMaxDepthFromTeamRoles")
    | project TargetTime = TimeGenerated, AccountId, IPAddress, CorrelationId = tostring(parse_json(RawEventData).CorrelationId);
CloudAppEvents
| where TimeGenerated > ago(1d)
| where Application == "Microsoft Dynamics 365"
| where not(ipv4_is_in_range(IPAddress, CorporateIPRange ))
| where ActionType has_any ("Update", "Create") and (ObjectName has "role" or ObjectName has "team" or parse_json(RawEventData).EntityName has_any ("role", "systemuserroles", "teamroles"))
| project ModificationTime = TimeGenerated, AccountId, ActionType, ObjectName, RawEventData
| join kind=inner SuspiciousInquiries on AccountId
| where ModificationTime between (TargetTime .. (TargetTime + ThreatWindow))
| project ModificationTime, AccountId, ActionType, ObjectName, IPAddress, TargetTime

ExecutableFilesPublicFolder

Watches for executable and script file types written into C:\Users\Public, a staging directory that almost nothing legitimate writes binaries to.

// The start of the folderpath in the Public directory.
let PublicFolder = @'C:\Users\Public';
// List with Executable File Extensions, can be adjusted or changed.
let ExecutableFileExtensions = dynamic(['bat', 'cmd', 'com', 'cpl', 'ex', 'exe', 'jse', 'msc','ps1', 'reg', 'vb', 'vbe', 'ws', 'wsf', 'hta', 'js']);
// Prevalence Threshold, if the file exceeds this threshold it is likely to be benign.
let FilePrevalenceThreshold = 250;
DeviceFileEvents
| where FolderPath contains PublicFolder
// Extract File Extension from the filename.
| extend FileExtension = tostring(extract(@'.*\.(.*)', 1, FileName))
// Only list Files that are executable
| where FileExtension in~ (ExecutableFileExtensions)
| invoke FileProfile('SHA256', 10000)
// Filter based on FilePrevalenceThreshold
| where GlobalPrevalence <= FilePrevalenceThreshold
| project Timestamp, DeviceName, FileExtension, FolderPath, GlobalPrevalence, Signer, Publisher, ReportId, DeviceId

MDE-LocalAIAgents

Lists local AI agents and their MCP servers from the AgentsInfo table, an inventory most estates do not have yet.

AgentsInfo
| where Platform == @"LocalAgents"
| extend AgentInfo = parse_json(RawAgentInfo).localAgentMetadata
| where isnotempty( AgentInfo)
| extend DeviceName = tostring(AgentInfo.deviceName)
| where isnotempty( column_ifexists("McpServers",""))
| mv-expand McpServers
| extend MCP_Name = tostring( McpServers.name)
| extend MCP_Type = tostring(McpServers.type)
| extend MCP_Endpoint = tostring(McpServers.endpoint)
| project MCP_Name, MCP_Type, MCP_Endpoint, Name, DeviceName
| summarize Devices = make_set(DeviceName), TotalDevices = dcount(DeviceName,4) by MCP_Name, MCP_Type, MCP_Endpoint

Potential Azure VM Admin Password reset using VMAccess extension

Catches the VMAccess extension resetting a local administrator password on an Azure VM, a supported feature that is also a clean path onto a machine you do not have credentials for.

//JsonVMAccessExtension.exe refers to the VMAccess Extension that can reset the Built-in administrator account/add new accounts. This applies to any Azure VM/AVD
//Ref: https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/reset-rdp#reset-the-local-admin-account-password
DeviceEvents
| where TimeGenerated >ago(90d)
//| where AdditionalFields.PipeName == "\\Device\\NamedPipe\\wkssvc" or AdditionalFields.PipeName == "\\Device\\NamedPipe\\srvsvc"
| where AdditionalFields.RemoteClientsAccess == "AcceptRemote" 
| where InitiatingProcessCommandLine == "JsonVMAccessExtension.exe  \"enable\"" //Azure VM account Extension
| where InitiatingProcessAccountName == "system" //Extension runs as system

Identify Windows Devices Missing Defender for Endpoint WSL Plugin

Finds Windows devices running WSL without the Defender for Endpoint WSL plugin installed, a coverage gap that hides Linux activity on Windows endpoints.

let ActiveWindowsDevices = 
    DeviceInfo
    | where Timestamp > ago(30d)
    | where OSPlatform startswith "Windows"
    | summarize arg_max(Timestamp, *) by DeviceId
    | where isnotempty(DeviceName)
    | where OnboardingStatus == "Onboarded"
    | project DeviceId, DeviceName, OSPlatform, OSVersion;
// Identify all devices that have the WSL plugin installed
let DevicesWithWslPlugin = 
    DeviceTvmSoftwareInventory
    | where SoftwareName has "Defender for Endpoint plug-in for WSL" or SoftwareName has "DefenderPluginForWSL"
    | summarize by DeviceId;
// Combine the tables and display "yes" or "no"
ActiveWindowsDevices
| extend WslPluginInstalled = iif(DeviceId in (DevicesWithWslPlugin), "yes", "no")
| sort by DeviceName asc
// Optional Filter for Devices with missing WSL Plugin
//| where WslPluginInstalled == "no"
// Microsoft Defender for Endpoint plug-in for Windows Subsystem for Linux (WSL) https://learn.microsoft.com/en-us/defender-endpoint/mde-plugin-wsl

GraphActivityFromFirstPartyApps

Enriches Microsoft Graph activity from first-party apps with a sensitive-permission classification pulled in at query time.

// Get a list of first party apps with activity to Microsoft Graph and enriched classification

let SensitiveMsGraphPermissions = externaldata(AppId: guid, AppRoleId: guid, AppRoleDisplayName: string, Category: string, EAMTierLevelName: string, EAMTierLevelTagValue: string)["https://raw.githubusercontent.com/Cloud-Architekt/AzurePrivilegedIAM/main/Classification/Classification_MsGraphAppRoles.json"] with (format='multijson')
    | where isnotempty(EAMTierLevelName)
    // Optional: Filtering for Control Plane only

    //| where EAMTierLevelName == "ControlPlane"    

    | distinct AppRoleDisplayName;
_GetWatchlist('WorkloadIdentityInfo')
| where IsFirstPartyApp == "true"
| extend Identity = tostring(ServicePrincipalObjectId)
| join kind=inner (
    MicrosoftGraphActivityLogs
    | where TimeGenerated >ago(365d)
    | where Roles has_any (SensitiveMsGraphPermissions)
    | extend Roles = split(Roles, ' ')
    | extend Identity = ServicePrincipalId
) on Identity
| summarize AppRoleScope = make_set( Roles ), TotalResponseSizeBytes = sum(ResponseSizeBytes) by AppDisplayName, ServicePrincipalObjectId, AppId, AppOwnerTenantId, ServicePrincipalType, SignInAudience

VM Creation using Azure Activity

Lists VM and deployment write operations across 90 days of AzureActivity, the starting point for rogue or unbudgeted VM hunting.

//Could be useful as part of rogue VM creation hunting, could add queries to check the tags and ensure is compliant with Org Tagging
let operationList = dynamic(["microsoft.compute/virtualmachines/write", "microsoft.resources/deployments/write"]);
//
AzureActivity
| where TimeGenerated > ago(90d)
| where OperationNameValue in~ (operationList)
| where ActivitySubstatusValue == "Created"
| extend ProvisioningState = parse_json(tostring(parse_json(tostring(parse_json(Properties).responseBody)).properties)).provisioningState,
VM_ID = parse_json(tostring(parse_json(tostring(parse_json(Properties).responseBody)).properties)).vmId,
ImageReference_Offer = parse_json(tostring(parse_json(tostring(parse_json(tostring(parse_json(tostring(Properties_d.responseBody)).properties)).storageProfile)).imageReference)).offer,
ImageReference_Exact_version = parse_json(tostring(parse_json(tostring(parse_json(tostring(parse_json(tostring(Properties_d.responseBody)).properties)).storageProfile)).imageReference)).exactVersion,
ImageReference_SKU = parse_json(tostring(parse_json(tostring(parse_json(tostring(parse_json(tostring(Properties_d.responseBody)).properties)).storageProfile)).imageReference)).sku
//Only search for Windows Server
| where ImageReference_Offer == "WindowsServer"

Teams-AllowedDomains

Tracks changes to the Teams external access allowed-domains list, a federation setting that quietly widens who can message your users.

CloudAppEvents
| where ActionType == "TeamsAdminAction"
| where RawEventData has "AllowedDomains"
| extend ModifiedProperties = parse_json(RawEventData).ModifiedProperties
| mv-apply ModifiedProperties on
(
    where ModifiedProperties.Name == "AllowedDomains"
    | project 
        PropertyName = tostring(ModifiedProperties.Name),
        NewValue = tostring(ModifiedProperties.NewValue),
        OldValue = tostring(ModifiedProperties.OldValue)
)
| project 
    TimeGenerated,
    ActionType,
    AccountDisplayName,
    PropertyName,
    OldValue,
    NewValue,
    RawEventData

NTLM Network Logon to Critical Device

Filters NTLM network logons down to devices the exposure graph marks as critical, which is where an NTLM relay actually hurts.

let NetworkLogons = DeviceLogonEvents
	| where Timestamp > ago(4h)
	| where LogonType == "Network"
	| where Protocol == "NTLM"
	| extend ShortDeviceName = toupper(split(DeviceName, ".")[0]);
NetworkLogons
| join kind=inner (	ExposureGraphNodes
	| where Categories has "device"
	| where isnotnull(NodeProperties.rawData.criticalityLevel)
	| extend ShortNodeName = toupper(split(NodeName, ".")[0])
	| extend TargetCriticalityScore = toint(NodeProperties.rawData.criticalityLevel.criticalityLevel)
	| extend TargetCriticalityRule = tostring(NodeProperties.rawData.criticalityLevel.ruleName)
	| project ShortNodeName, TargetCriticalityScore, TargetCriticalityRule
) on $left.ShortDeviceName == $right.ShortNodeName
| where TargetCriticalityScore >= 3

XDR-4_Behavior_To_Event_DrillThrough

Joins anomalous Insider Risk behaviours to the concrete file events behind them for the same user and window, turning a behaviour score into evidence.

// XDR-4 - Behavior -> Event drill-through

// Run in: security.microsoft.com -> Hunting -> Advanced hunting

// Joins anomalous Insider Risk behaviors with concrete file events on same user, same window

DataSecurityBehaviors
| where Timestamp > ago(7d) and IsAnomalous == 1
| project BehaviorId, AccountUpn, ActionTypeBehavior=ActionType, Categories, StartTime, EndTime
| join kind=inner (
    DataSecurityEvents
    | where Timestamp > ago(7d)
    | project Timestamp, AccountUpn, ActionType, ObjectId, ApplicationNames, Workload, ActivityId
) on AccountUpn
| where Timestamp between (StartTime .. EndTime)
| project ActionTypeBehavior, Categories, AccountUpn, Timestamp, ActionType, ObjectId, ApplicationNames, Workload
| order by AccountUpn, Timestamp asc

PIMAlerts

Parses triggered PIM alerts out of AuditLogs and assigns severity, covering standing role assignments made outside PIM.

AuditLogs
| where OperationName =~ "Triggered PIM alert"
| where Category =~ "RoleManagement"
| extend AlertDescription = TargetResources[0].displayName
| extend Severity = case(
	AlertDescription =~ "Roles are being assigned outside of Privileged Identity Management" or AlertDescription =~ "Role assigned outside of PIM", "High",
	AlertDescription =~ "Potential stale accounts in a privileged role", "Medium",
	AlertDescription =~ "Administrators aren't using their privileged roles", "Low",
	AlertDescription =~ "Roles don't require multifactor authentication for activation", "Low",
	AlertDescription =~ "The organization doesn't have Microsoft Entra ID P2 or Microsoft Entra ID Governance", "Low",
	AlertDescription =~ "There are too many Global Administrators", "Low",
	AlertDescription =~ "Roles are being activated too frequently", "Low",
	"Unknown"
)
| project-reorder TimeGenerated, OperationName, Category, AlertDescription, Severity

estimate-potential-savings-by-moving-tables-to-data-lake

Estimates what each table would cost in the data lake tier versus analytics, with the per-GB prices exposed as variables you set to your own contract.

let AnalyticsPricePerGB = 2.48; // Example price per GB for Analytics
let DataLakePricePerGB = 0.05; // Example price per GB for Data Lake
Usage
| where TimeGenerated >= startofday(ago(30d))
| where IsBillable == true
| summarize IngestedGB = round(sum(Quantity) / 1000.0, 2) by DataType, Plan
| where Plan == "Analytics"
| extend CurrentCost = round(IngestedGB * AnalyticsPricePerGB, 2)
| extend IfDataLakeCost = round(IngestedGB * DataLakePricePerGB, 2)
| extend PotentialSavings = round(CurrentCost - IfDataLakeCost, 2)
| where PotentialSavings > 0
| project DataType, IngestedGB, CurrentCost, IfDataLakeCost, PotentialSavings
| order by PotentialSavings desc

Copilot-DefenderThreatProtection

Expands Security Copilot activity into the individual resources each session touched, which is the audit trail question that comes up as soon as Copilot is rolled out.

CopilotActivity
| extend Parsed = parse_json(LLMEventData)
| mv-expand Resource = Parsed.AccessedResources
| extend Action = tostring(Resource.Action)
| extend Id = tostring(Resource.id)
| extend Name = tostring(Resource.Name)
| extend Type = tostring(Resource.Type)
| where Name == "Block"
| extend DetectionName = extract(@"blocked by ['""]([^'""]+)['""] detection", 1, Action)
| project TimeGenerated, DetectionName, Action, Id, Name, Type, SrcIpAddr, Workload, AppHost, AppIdentity, LLMEventData
| sort by TimeGenerated

OutboundMSHTA

Four lines for mshta.exe talking to a public IP, which on most estates is either a red team or a real intrusion.

DeviceNetworkEvents
| where InitiatingProcessFileName =~ "mshta.exe"
| where RemoteIPType == "Public" or not(ipv4_is_private(RemoteIP))
| project-reorder TimeGenerated, InitiatingProcessCommandLine, RemoteUrl, RemoteIP, DeviceName, InitiatingProcessAccountUpn

Audit B2B Guest Devices Trust Type

Reports the device trust type behind inbound B2B guest sign-ins, showing how much of your guest access arrives from unmanaged devices.

//Shouthout johannesblog.com for the idea
SigninLogs
//| where AppDisplayName =~ "Microsoft Teams"
| extend TrustType = tostring(DeviceDetail.trustType)
| where CrossTenantAccessType == @"b2bCollaboration"
| where AADTenantId != HomeTenantId //exclude B2b outbound
| where UserType == "Guest"
| project TimeGenerated, UserPrincipalName, AppDisplayName, IPAddress, TrustType,
          DeviceId = tostring(DeviceDetail.deviceId),
          DeviceName = tostring(DeviceDetail.displayName),
          OperatingSystem = tostring(DeviceDetail.operatingSystem),
          Browser = tostring(DeviceDetail.browser),
          ConditionalAccessStatus, ResultType, ResultDescription
| order by TimeGenerated desc

APT28 Kill Chain - LNK to UNC to SMB for Credential Exfiltration

Chains LNK execution to UNC path access to outbound SMB in one query, modelling the NTLM credential leak path used against CVE-2026-32202.

// APT28 Kill Chain for CVE-2026-32202
// https://thehackernews.com/2026/04/microsoft-confirms-active-exploitation.html
// LNK → CPL/UNC → SMB → NTLM Exfil
let timeframe = 7d;
let InitialAccess_LNK = DeviceProcessEvents
| where TimeGenerated > ago(timeframe)
| where InitiatingProcessCommandLine has ".lnk"
| project DeviceId, LNK_TriggerTime = TimeGenerated, LNK_CommandLine = InitiatingProcessCommandLine;
let Execution_UNC = DeviceProcessEvents
| where TimeGenerated > ago(timeframe)
| where ProcessCommandLine matches regex @"\\\\[a-zA-Z0-9\-\.]{4,}\\"
| where ProcessCommandLine has_any (".cpl", ".dll", ".exe")
| project DeviceId, UNC_LoadTime = TimeGenerated, UNC_CommandLine = ProcessCommandLine;
let Exfiltration_SMB = DeviceNetworkEvents
| where TimeGenerated > ago(timeframe)
| where RemotePort == 445
| where not(ipv4_is_private(RemoteIP))
| project DeviceId, SMB_ConnectTime = TimeGenerated, RemoteIP;
InitialAccess_LNK
| join kind=inner Execution_UNC on DeviceId
| where UNC_LoadTime between (LNK_TriggerTime .. (LNK_TriggerTime + 2m))
| join kind=inner Exfiltration_SMB on DeviceId
| where SMB_ConnectTime between (UNC_LoadTime .. (UNC_LoadTime + 2m))
| project
    DeviceId,
    LNK_Time = LNK_TriggerTime,
    UNC_Time = UNC_LoadTime,
    SMB_Time = SMB_ConnectTime,
    LNK_CommandLine,
    UNC_CommandLine,
    RemoteIP
| extend
    AlertTitle = "APT28 Kill Chain: LNK→UNC→SMB (CVE-2026-32202)",
    Severity = "Critical",
    MITRE = "T1566.001 → T1187 → T1557.001"

MDI-AD-GroupPolicy-PasswordPolicy

Surfaces Group Policy password policy changes from Defender for Identity directory events, including which policy and which domain was touched.

IdentityDirectoryEvents
| where ActionType == @"Group Policy settings were changed"
| extend Info = parse_json(AdditionalFields)
| extend MachinePolicies = tostring(Info.MachinePolicies),
         GroupPolicyName = tostring(Info.GroupPolicyName),
         GroupPolicyId   = tostring(Info.GroupPolicyId),
         DomainName      = tostring(Info.DomainName),
         Category        = tostring(Info.Category),
         AttackTechniques = tostring(Info.AttackTechniques)
| project TimeGenerated, DomainName, GroupPolicyName, GroupPolicyId, MachinePolicies, Category, AttackTechniques
| mv-expand PolicyEntry = split(MachinePolicies, ",") to typeof(string)
| extend FullPath    = tostring(split(PolicyEntry, "=")[0]),
         PolicyValue = tostring(split(PolicyEntry, "=")[1])
| extend PathParts   = split(FullPath, @"\"),
         PolicyName  = tostring(split(FullPath, @"\")[-1])
| extend PolicyPath  = strcat_array(array_slice(PathParts, 0, array_length(PathParts) - 2), @"\")
| where PolicyPath == @"Account Policies\PasswordPolicy"
| summarize Settings = make_bag(pack(PolicyName, PolicyValue)) 
    by TimeGenerated, GroupPolicyId, GroupPolicyName, DomainName, AttackTechniques, Category, PolicyPath

New TenantAllowBlockList (TABL) entry

Extracts new Tenant Allow Block List entries with their notes, expiry and defanged URL, so allow-list additions get reviewed rather than forgotten.

CloudAppEvents 
| where ActionType == "New-TenantAllowBlockListItems"
| extend Notes = extract(@'"Notes","Value":"(.*?)"', 1, tostring(ActivityObjects))
| extend Url = replace_string(extract(@'Name":"Entries","Value":"(.*?)"', 1, tostring(ActivityObjects)), ".", "[.]")
| extend Expiration = replace_string(extract(@'"Name":"ExpirationDate","Value":"(.*?)"', 1, tostring(ActivityObjects)), ".", "[.]")
//| project-reorder Notes, Url, Expiration
//| project TimeGenerated, ObjectName, Notes, Url, Expiration, IPAddress

AutomationAccount-RunbookStatus

Turns Automation Account job logs into per-runbook start, end and status rows, so a runbook that suddenly starts failing or running at odd hours is visible.

AzureDiagnostics
| where Category == 'JobLogs'
| extend RunbookName = RunbookName_s
| project TimeGenerated,RunbookName,ResultType,CorrelationId,JobId_g
| summarize StartTime = minif(TimeGenerated,ResultType == 'Started'),EndTime = minif(TimeGenerated,ResultType in ('Completed','Failed','Failed')),
Status = tostring(parse_json(make_list_if(ResultType,ResultType in ('Completed','Failed','Stopped')))[0]) by JobId_g,RunbookName
| extend DurationSec = datetime_diff('second', EndTime,StartTime)
| join kind=leftouter (AzureDiagnostics
| where Category == "JobStreams"
| where StreamType_s == "Error"
| summarize TotalErrors = dcount(StreamType_s) by JobId_g, StreamType_s)
on $left. JobId_g == $right. JobId_g
| extend HasErrors = iff(StreamType_s == 'Error',true,false)
| project StartTime, EndTime, DurationSec,RunbookName,Status,HasErrors,TotalErrors,JobId_g

Defender -RedSun Detection - NamedPipe Detection

Hunts the named pipe pattern behind the RedSun technique, which abuses a Defender detection flow to escalate to SYSTEM.

//https://github.com/Nightmare-Eclipse/RedSun
//Uses a Defender detection to escalate to system. Note I couldn't get this exploit in MDE. It will fail to request a batch oplock on the update file.
// The binary/code supplied in repo uses an EICAR signature but a threatactor could subs this for anything so long as defender is triggered.
//Code will back out if real time monitoring is not enabled.
DeviceEvents
| where ActionType contains "NamedPipeEvent"
| where parse_json(AdditionalFields)["RemoteClientsAccess"] == 'AcceptRemote'
//| where parse_json(AdditionalFields)["PipeName"] == @'\Device\NamedPipe\REDSUN' //Low Fidelity Named Pipe (Line 518 in Code) Name can be changed but included here
| where parse_json(AdditionalFields)["DesiredAccess"] == '1704351' //Technically Access Mask can be modified
| where InitiatingProcessAccountName != @"system" and InitiatingProcessAccountName != "network service" and AccountSid != @"S-1-5-18"//PoC is from unprivileged User
| where parse_json(AdditionalFields)["FileOperation"] == 'File created'
| where InitiatingProcessVersionInfoCompanyName != @"Microsoft Corporation" //File created that created the namedpipe is the RedSun.exe binary (filename can be renamed)

per-solution-breakdown

Breaks 30 days of workspace ingestion down per solution and separates auxiliary tables, the query to run before any Sentinel cost conversation.

let _AuxiliaryTables = dynamic([
    "OfficeActivity", "AzureActivity", "Heartbeat",
    "SentinelHealth", "SecurityAlert", "SecurityIncident", "Operation"
]);
Usage
| where TimeGenerated > ago(30d)
| where IsBillable == true
| where DataType !in (_AuxiliaryTables)
| summarize 
    TotalGB = round(sum(Quantity) / 1024.0, 2),
    TableCount = dcount(DataType),
    DailyAvgGB = round(avg(Quantity) / 1024.0, 4)
    by Solution
| extend EstMonthlyCostUSD = round(DailyAvgGB * 30 * 2.76, 2)
| order by TotalGB desc

Overview of all MFA methods in use

Charts which MFA methods your tenant actually uses, which is how you find out how much of the estate is still on SMS before you start enforcing.

// Bar chart showing all MFA types used
SigninLogs
| where AuthenticationRequirement == "multiFactorAuthentication"
| where ResultType == 0
| project AuthenticationDetails
| extend ['MFA Method'] = tostring(parse_json(AuthenticationDetails)[1].authenticationMethod)
| summarize Count=count() by ['MFA Method']
| where ['MFA Method'] != "Previously satisfied" and isnotempty(['MFA Method'])
| sort by Count desc
| render barchart with (title="Types of MFA Methods used")

Correlation_Git_And_VSCode_Task_Abuse

Correlates history-rewriting git commands with suspicious node.exe activity on the same device, aimed at developer workstation supply chain tampering.

let GitAbuse = DeviceProcessEvents
| where ProcessCommandLine has_any ("git commit --amend", "--no-verify", "git push -f", "git push --force", "git config --local")
| project DeviceId, GitTime=Timestamp, DeviceName, AccountName, GitCmd=ProcessCommandLine;
let SuspiciousNode = DeviceProcessEvents
| where FileName in~ ("node.exe", "node")
| where ProcessCommandLine has_any (".woff2", ".woff", ".ttf", ".otf", ".eot")
| where InitiatingProcessFileName in~ ("Code.exe", "code", "cmd.exe", "powershell.exe", "bash", "sh", "zsh")
   or InitiatingProcessCommandLine has_any ("Code.exe", "code", ".vscode", "tasks.json", "folderOpen")
| project DeviceId, NodeTime=Timestamp, NodeCmd=ProcessCommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine;
GitAbuse
| join kind=inner SuspiciousNode on DeviceId
| where NodeTime between (GitTime - 7d .. GitTime + 7d)
| project DeviceName, AccountName, GitTime, GitCmd, NodeTime, NodeCmd, InitiatingProcessFileName, InitiatingProcessCommandLine
| order by NodeTime desc

NodeC2Polinder

Three lines that flag node.exe reaching out to blockchain infrastructure domains, a command and control channel that hides inside ordinary developer traffic.

DeviceNetworkEvents
| where InitiatingProcessFileName in~ ("node.exe", "node")
| where RemoteUrl has_any ("trongrid.io", "aptoslabs.com")

Device TVM Secure Configuration Assessment Summary

Summarises the TVM secure configuration assessment per device against the configuration knowledge base, giving a readable posture gap list rather than raw IDs.

DeviceTvmSecureConfigurationAssessment
| join kind=leftouter (
    DeviceTvmSecureConfigurationAssessmentKB
    | project ConfigurationId, ConfigurationName
) on ConfigurationId
| summarize 
    Total = count(),
    Compliant = countif(IsCompliant == 1),
    NonCompliant = countif(IsCompliant == 0),
    NotApplicable = countif(IsApplicable == 0)
  by ConfigurationId, ConfigurationName
| order by NonCompliant desc

SecurityEvent-Unusual user account authentication

Compares each account's authentication over a 14-day period against a watchlist of domain controllers to surface first-time or unusual authentication paths.

let query_frequency = 1h;
let query_period = 14d;
let _DomainControllers = toscalar(
    _GetWatchlist("Service-PrivateCorporateServices")
    | where Service == "DomainController"
    | summarize make_list(HostName)
);
SecurityEvent
| where TimeGenerated > ago (query_period)
| where EventID == 4624 and AccountType == "User" and Computer has_any (_DomainControllers)
| summarize arg_min(TimeGenerated, *) by
    LogonTypeName,
    AuthenticationPackageName,
    LmPackageName,
    EmptyIpAddress = IpAddress in ("-", ""),
    EmptyWorkstationName = WorkstationName in ("-", ""),
    ElevatedToken,
    IsAnonymousLogon = TargetAccount ==  @"NT AUTHORITY\ANONYMOUS LOGON"
| where TimeGenerated > ago(query_frequency)
| project
    TimeGenerated,
    Computer,
    Account,
    AccountType,
    Activity,
    LogonTypeName,
    AuthenticationPackageName,
    LmPackageName,
    KeyLength,
    EmptyIpAddress,
    EmptyWorkstationName,
    ElevatedToken,
    IsAnonymousLogon

HUNT-02_M365-Exchange-InboxTransportRule-Audit-30d

Audits 30 days of Exchange inbox and transport rule changes with forwarding, redirect and hiding indicators pre-calculated, the standard opening move on a suspected BEC.

// Hunt    : M365 - Inbox Rule and Transport Rule Audit (30d)

// Purpose : Full audit of all Exchange inbox and transport rules created or modified

//           in the past 30 days, pre-calculated for forwarding, redirect and hiding

//           indicators to support BEC and exfiltration investigations.

// Tables  : OfficeActivity

// Period  : P30D

//==========================================================================================


let LookbackDays = 30d;

OfficeActivity
| where TimeGenerated > ago(LookbackDays)
| where RecordType == "ExchangeAdmin"
| where Operation in (
    "New-InboxRule", "Set-InboxRule", "Remove-InboxRule",
    "New-TransportRule", "Set-TransportRule",
    "Enable-TransportRule", "Disable-TransportRule",
    "Remove-TransportRule", "New-JournalRule")
| extend Params = tostring(Parameters)
| extend
    HasForward      = Params has_any ("ForwardTo", "ForwardAsAttachmentTo", "RedirectTo", "RedirectMessageTo"),
    HasHide         = Params has_any ("DeleteMessage", "MoveToFolder", "MarkAsRead"),
    HasBypass       = Params has_any ("ExceptIfSenderDomainIs", "SetHeaderName"),
    ExternalAddress = extract(@"(ForwardTo|RedirectTo|RedirectMessageTo).*?([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})", 2, Params),
    FolderTarget    = extract(@"(MoveToFolder).*?:([^\s,]+)", 2, Params)
| project
    TimeGenerated,
    UserId,
    ClientIP,
    Operation,
    HasForward,
    HasHide,
    HasBypass,
    ExternalAddress,
    FolderTarget,
    Params
| sort by TimeGenerated desc

HUNT-01_GPU-VM-Deployment-History

Inventories 90 days of successful GPU and high-compute VM deployments from AzureActivity, the baseline you need before cryptojacking detections mean anything.

// Hunt     : Hunt - GPU and High-Compute VM Deployment History (90d)
// Tactics  : Impact
// MITRE    : T1496
// Purpose  : Inventory all successful GPU/high-compute VM deployments over the past 90 days. Use to baseline legitimate deployers before tuning Rule-01, and to investigate potential cryptojacking campaigns.
//==========================================================================================

let GPUSKUs = dynamic(["Standard_NC", "Standard_NV", "Standard_ND", "Standard_NP", "Standard_HB", "Standard_HC"]);
AzureActivity
| where TimeGenerated > ago(90d)
| where OperationNameValue has_any ("VIRTUALMACHINES/WRITE", "VIRTUALMACHINESCALESETS/WRITE")
| where ActivityStatusValue =~ "Success"
| where Properties has_any (GPUSKUs)
| project TimeGenerated, Caller, CallerIpAddress, SubscriptionId, ResourceGroup, ResourceId, Properties
| extend SKUHint = extract(@'Standard_(?:NC|NV|ND|NP|HB|HC)[^"]*', 0, Properties)
| order by TimeGenerated desc

MDO-AutoForwardingMode

Tracks changes to the outbound spam policy AutoForwardingMode setting, the tenant-level switch that decides whether external auto-forwarding is possible at all.

CloudAppEvents
| where ObjectName == "Set-HostedOutboundSpamFilterPolicy"
| mv-expand parse_json(ActivityObjects)
| where ActivityObjects.Name == 'AutoForwardingMode'
| extend Setting = tostring(ActivityObjects.Name)
| extend Configuration = tostring(ActivityObjects.Value)
| extend Description = case(
        Configuration == "Automatic", "System-controlled: Default value. Same as Off — forwarding is disabled.",
        Configuration == "On", "Forwarding is enabled: Automatic external forwarding is allowed and not restricted.",
        Configuration == "Off", "Forwarding is disabled: Automatic external forwarding is blocked and results in an NDR to the sender.",
        "Unknown"
    )
| project TimeGenerated, Setting,Configuration,Description

PotentialBeaconingActivity

Scores outbound connections for beaconing using device count, connection count and global prevalence thresholds you can tune to your own noise floor.

let DeviceThreshold = 5;
let ConnectionThreshold = 25;
let GlobalPrevalanceThreshold = 250;
DeviceNetworkEvents
| where TimeGenerated > ago(7d)
| where not(ipv4_is_private(RemoteIP))
| where ActionType == 'ConnectionSuccessAggregatedReport'
| extend Connections = toint(parse_json(AdditionalFields).uniqueEventsAggregated)
| summarize Total = count(), Devices = dcount(DeviceId), Domains = make_set(RemoteUrl), AvgConnections = avg(Connections) by RemoteIP, bin(TimeGenerated, 1d)
| where AvgConnections >= ConnectionThreshold and Devices <= DeviceThreshold
| join kind=inner (DeviceNetworkEvents
    | where ActionType == 'ConnectionSuccess'
    | distinct RemoteIP, InitiatingProcessSHA256) on RemoteIP
    | invoke FileProfile(InitiatingProcessSHA256)
    | where GlobalPrevalence <= GlobalPrevalanceThreshold

SignInsByOS

Parses the sign-in user agent into an operating system breakdown, useful for finding stale or unmanaged client platforms before you tighten Conditional Access.

EntraIdSignInEvents
| where isnotempty(UserAgent)
// Filter for successful sign ins only
| where ErrorCode == 0
| extend ParsedAgent = parse_json(parse_user_agent(UserAgent, "os"))
| extend OperatingSystem = strcat(tostring(ParsedAgent.OperatingSystem.Family), " ", tostring(ParsedAgent.OperatingSystem.MajorVersion))
| summarize Total = count() by OperatingSystem
| sort by Total

Attempt to Disable Syslog Service

Detects systemctl and service commands aimed at syslog and rsyslog on Linux endpoints, an anti-forensics step that usually precedes something worse.

// Attempt to Disable Syslog Service
DeviceProcessEvents
| where ProcessCommandLine has_any ("syslog", "rsyslog", "syslog-ng", "syslog.service", "rsyslog.service", "syslog-ng.service")
| where FileName in~ ("systemctl", "service", "chkconfig", "update-rc.d")
| where (
    (FileName =~ "systemctl" and ProcessCommandLine has_any ("disable", "stop", "kill", "mask")) or
    (FileName =~ "service" and ProcessCommandLine has "stop") or
    (FileName =~ "chkconfig" and ProcessCommandLine has "off") or
    (FileName =~ "update-rc.d" and ProcessCommandLine has_any ("remove", "disable"))
)
// Exclude known log rotation or HUP signals
| where InitiatingProcessFileName !~ "rsyslog-rotate"
| where ProcessCommandLine !has "HUP"

MDE-AsrVulnerableSignedDriverBlocked

Pulls the LOLDrivers catalogue at query time and checks it against ASR vulnerable-driver block events, showing which known-bad drivers actually got blocked in your estate.

let LOLDrivers = externaldata (Category:string, KnownVulnerableSamples:dynamic, Verified:string ) [h@"https://www.loldrivers.io/api/drivers.json"]
    with (format=multijson, ingestionMapping='[{"Column":"Category","Properties":{"Path":"$.Category"}},{"Column":"KnownVulnerableSamples","Properties":{"Path":"$.KnownVulnerableSamples"}},{"Column":"Verified","Properties":{"Path":"$.Verified"}}]')
| mv-expand KnownVulnerableSamples
| extend SHA1 = tostring(KnownVulnerableSamples.SHA1), SHA256 = tostring(KnownVulnerableSamples.SHA256)
;
DeviceEvents
| where ActionType == @"AsrVulnerableSignedDriverBlocked"
| project Timestamp, DeviceName, FileName, SHA256,SHA1, FolderPath
| join kind=inner   (LOLDrivers | where isnotempty(SHA256)) on SHA256
| union (
  DeviceEvents
| where ActionType == @"AsrVulnerableSignedDriverBlocked"
  | join kind=inner (LOLDrivers | where isnotempty(SHA1)) on SHA1
)

ClickFix LoLBin Abuse

Targets the ClickFix living-off-the-land chain, wscript and cscript running SyncAppvPublishingServer.vbs and its siblings, from process telemetry.

//THX to Maurice Fielenbach https://www.linkedin.com/posts/mauricefielenbach_threathunting-dfir-cybersecurity-activity-7429953431588659200-c0zE/
DeviceProcessEvents
| where Timestamp >= ago(7d)
| where (
    (FileName in~ ("wscript.exe", "cscript.exe") and ProcessCommandLine has "SyncAppvPublishingServer.vbs")
    or (FileName =~ "wmic.exe" and ProcessCommandLine has_any ("process", "call", "create"))
    or (FileName =~ "ssh.exe" and ProcessCommandLine has "ProxyCommand")
)
| where ProcessCommandLine has_any (
    "gal", "i*x",         
    "gcm", "*stM*",       
    "jsdelivr.net",       
    "github",             
    "powershell"          
)

DeviceIPHistory

Reconstructs the IP history of a single device from network telemetry, the lookup you want when an alert gives you an address and a timestamp but no hostname.

let QueryDevice = "devicename";
DeviceNetworkEvents
| where DeviceName startswith QueryDevice
| where LocalIP !in ("", "::", "::1", "127.0.0.1", "0.0.0.0")
| where LocalIP !startswith "::ffff:"
| where LocalIP != RemoteIP
| where ActionType !in ("ListeningConnectionCreated", "ConnectionFailed")  // only successful peer connections are evidence

| where ActionType !endswith "Inspected"  // bad data -- Zeek flips RemoteIP and LocalIP sometimes

| extend AccurateDirection = tostring(parse_json(AdditionalFields).direction)
| extend EstimatedDirection = iff(LocalPort > RemotePort, "Out", "In")  // rarely wrong; needed when sensor isn't logging directionality

| extend Direction = iff(AccurateDirection != "", AccurateDirection, EstimatedDirection)
| summarize
    Start=min(Timestamp),
    End=max(Timestamp),
    ConnectionCount=count(),
    InboundConnections=countif(Direction=="In"),
    OutboundConnections=countif(Direction!="In"),
    Peers=make_set(RemoteIP)
    by LocalIP
| where OutboundConnections > 0  // wrong directionality captured by EstimatedDirection

| sort by Start desc

Windows - Recent Devices Missing Full Scan

Lists recently onboarded devices that never ran the initial full antivirus scan Microsoft recommends after MDE onboarding.

// All credit goes to Felix Brand - Taken from his post (https://www.linkedin.com/posts/felix-brand_defenderxdr-kql-microsoft-activity-7228749401849040896-0sNk)
// Microsoft now recommends (https://lnkd.in/ewBryvhF) to have one initial full scan when you onboard your system into MDE. But MDE does not trigger a full scan after onboarding, this KQL query combined with a custom detection rule can trigger a scan. 
// Once a scan is completed on an endpoint, it will not appear in this query again.

let AvModeDescription = dynamic({"0":"Normal", "1":"Passive", "4":"EDR Block"});
let TimeRange = ago(1d);
DeviceTvmInfoGathering
| where Timestamp > TimeRange
| extend AdditionalFields = parse_json(AdditionalFields)
| extend AvMode =  tostring(AvModeDescription[tostring(AdditionalFields.["AvMode"])])
| extend FullScanStatus = coalesce(extractjson("$.Full.ScanStatus", tostring(AdditionalFields.AvScanResults)),"Not available")
| where isnotempty( AvMode ) and AvMode has "Normal"
| where FullScanStatus !has "Completed"
| join DeviceEvents on DeviceName
| summarize arg_max(Timestamp, *) by DeviceName, DeviceId
| project DeviceName, DeviceId, FullScanStatus, ReportId, Timestamp
| take 50

Malicious Browser Extension Downloads using DeviceFileEvents

Matches browser extension files written to disk against a public list of malicious extension IDs, covering an install path that rarely gets monitored.

//Credit https://github.com/toborrm9/malicious_extension_sentry
let MaliciousExtensions = externaldata (ExtensionID: string) [@'https://raw.githubusercontent.com/toborrm9/malicious_extension_sentry/refs/heads/main/Malicious-Extensions.csv'] with (format=txt, ignoreFirstRecord = true)
| extend ExtensionID = split(ExtensionID,",")
| mv-expand ExtensionID
| extend ExtensionID = tostring(ExtensionID);
DeviceFileEvents
| where TimeGenerated > ago(90d)
| where ActionType == "FileCreated"
| where FileName endswith ".crx"
//| where InitiatingProcessFileName == "chrome.exe" //if you need to filter down to chrome vs edge
| where FolderPath contains "Webstore Downloads"
| extend ExtensionID = trim_end(@"_\d{2,6}.crx", FileName)
| extend ExtensionURL = strcat("https://chrome.google.com/webstore/detail/",ExtensionID)
| extend EdgeExtensionURL = strcat("https://microsoftedge.microsoft.com/addons/detail/",ExtensionID)
| summarize count() by ExtensionID,ExtensionURL, EdgeExtensionURL
| join kind=leftouter MaliciousExtensions on ExtensionID //if name is present in the risky list present it

Image File Execution Options (IFEO) or SilentProcessExit Registry Modification

Watches Image File Execution Options and SilentProcessExit registry keys, a debugger-hijack persistence technique that also doubles as a way to kill security tooling.

let ExludedInitiatingProcessFileNames = ("ExludedProcessFilesName.exe");
DeviceRegistryEvents
| where InitiatingProcessFileName !in (ExludedInitiatingProcessFileNames)
| where RegistryKey has_any (
    @"Microsoft\Windows NT\CurrentVersion\Image File Execution Options", 
    @"Microsoft\Windows NT\CurrentVersion\SilentProcessExit"
)
| where RegistryValueName has_any ("Debugger", "MonitorProcess", "ReportingMode", "GlobalFlag")
| extend TargetProcess = tostring(split(RegistryKey, @"\")[8])

Azure RBAC Elevation via User Access Admin toggle

Catches the Azure RBAC elevate-access toggle, the one-click path from Global Administrator to User Access Administrator over every subscription in the tenant.

AuditLogs
| where Category == "AzureRBACRoleManagementElevateAccess"
| where ActivityDisplayName == "User has elevated their access to User Access Administrator for their Azure Resources"
| extend Actor = tostring(InitiatedBy.user.displayName)
| extend ActorUPN = tostring(InitiatedBy.user.userPrincipalName)
| extend ActorId = tostring(InitiatedBy.user.id)
| extend Operation = tostring(OperationName)
| extend IPAddress = tostring(InitiatedBy.user.ipAddress)
| extend AppId = tostring(InitiatedBy.app.appId)
| project TimeGenerated, ActivityDisplayName, Category, Operation, Actor, ActorUPN, ActorId, IPAddress, AppId, Result, ResultReason, CorrelationId
| order by TimeGenerated desc

HuntAccountsWithLeakedCredentials

Joins IdentityInfo against the exposure graph to list accounts flagged with leaked credentials, sorted so critical identities surface first.

IdentityInfo
| summarize arg_max(TimeGenerated, AccountUpn, AccountDisplayName, AccountDomain, CriticalityLevel, DistinguishedName) by AccountObjectId
| join kind=inner (
    ExposureGraphNodes
    // Get accounts with Leaked Credentials
    | where NodeProperties.rawData.hasAdLeakedCredentials == "true" or NodeProperties.rawData.hasLeakedCredentials == "true"
    // Get the AAD Object ID
    | mv-expand EntityIds
    | where EntityIds.type == "AadObjectId"
    | extend AccountObjectId = extract('objectid=(.*)', 1, tostring(EntityIds.id))
    | extend HasAdLeakedCredentials = tostring(NodeProperties.rawData.hasAdLeakedCredentials),
        HasLeakedCredentials = tostring(NodeProperties.rawData.hasLeakedCredentials)
    | distinct NodeLabel, AccountObjectId, HasAdLeakedCredentials, HasLeakedCredentials
) on AccountObjectId

authenticator_device_enrollment_country_risk_baseline

Baselines the countries each user normally signs in from over 90 days, then surfaces authenticator enrollments that happen outside that baseline.

let Lookback=90d;
let CountryBaseline=EntraIdSignInEvents
| where Timestamp > ago(Lookback)
| where isnotempty(AccountUpn)
| where isnotempty(IPAddress)
| extend Geo=geo_info_from_ip_address(IPAddress)
| extend CountryName=tostring(Geo["country"])
| where isnotempty(CountryName)
| summarize BaselineCountries=make_set(CountryName,256),BaselineCountryCount=dcount(CountryName) by AccountUpn;
AuditLogs
| where TimeGenerated > ago(Lookback)
| extend Details=tostring(AdditionalDetails)
| where Details has_any ("Microsoft Authenticator","PhoneAppNotification","PhoneAppOTP","Authenticator")
| extend Timestamp=TimeGenerated,
         AccountUpn=tostring(TargetResources[0].userPrincipalName),
         InitiatorUpn=coalesce(tostring(InitiatedBy.user.userPrincipalName),tostring(InitiatedBy.app.displayName)),
         InitiatorIP=coalesce(tostring(InitiatedBy.user.ipAddress),tostring(InitiatedBy.app.ipAddress))
| extend Geo=iff(isempty(InitiatorIP),dynamic(null),geo_info_from_ip_address(InitiatorIP))
| extend Country=tostring(Geo["country"]),
         Latitude=todouble(Geo["latitude"]),
         Longitude=todouble(Geo["longitude"])
| join kind=leftouter (CountryBaseline) on $left.AccountUpn==$right.AccountUpn
| extend BaselineCountries=coalesce(BaselineCountries,dynamic([]))
| extend HasBaseline=array_length(BaselineCountries)>0
| extend HasGeo=isnotempty(Country)
| extend IsNewCountry=iff(HasGeo==false,bool(null),not(set_has_element(BaselineCountries,Country)))
| extend RiskLevelAggregated=case(Result !~ "success",100,HasGeo==false,50,HasBaseline==false,50,IsNewCountry==true,50,0)
| extend RiskLevel=case(RiskLevelAggregated==100,"High",RiskLevelAggregated==50,"Medium",RiskLevelAggregated==10,"Low","None/Unknown")
| extend RiskReason=case(Result !~ "success","Audit operation not success",HasGeo==false,"No geo data for initiator IP",HasBaseline==false,"No 90d country baseline for user",IsNewCountry==true,"New country vs 90d sign-in baseline","Within baseline")
| project Timestamp,AccountUpn,InitiatorUpn,IPAddress=InitiatorIP,Country,Latitude,Longitude,BaselineCountryCount,BaselineCountries,IsNewCountry,RiskLevelAggregated,RiskLevel,RiskReason,OperationName,Result,CorrelationId,AdditionalDetails
| sort by Timestamp desc

Monitor DLLs by Signer

Summarises loaded DLLs by certificate signer so unsigned or oddly signed modules stand out against the normal signer set in your estate.

DeviceImageLoadEvents
| where TimeGenerated > ago(90d) //change to 30d if using advanced hunting with no sentinel
| where FileName contains ".dll"
| join kind=leftouter DeviceFileCertificateInfo on $left.SHA1 == $right.SHA1
| where FileName contains ".dll"
| summarize make_set(FileName) by Signer

TI Feed - C2URLFeed

Pulls an external C2 domain feed with externaldata and matches it against DeviceNetworkEvents, a cheap way to add threat intel without a connector.

// Collect Remote data
let C2IntelFeeds = externaldata(Domain: string, ioc:string, path:string, IP:string)[@"https://raw.githubusercontent.com/drb-ra/C2IntelFeeds/master/feeds/domainC2swithURLwithIP.csv"] with (format="csv", ignoreFirstRecord=True);
// Generate list that can be used to filter DeviceNetworkEvents
let DomainList = C2IntelFeeds
| distinct Domain;
DeviceNetworkEvents
// Filter only on C2 Domains
| extend ToLowerUrl = tolower(RemoteUrl)
| where RemoteUrl has_any (DomainList)
// Join the C2IntelFeed information for enrichment
| join kind=leftouter C2IntelFeeds on $left.RemoteIP == $right.IP
| extend GeoIPInfo = geo_info_from_ip_address(RemoteIP)
| extend country = tostring(parse_json(GeoIPInfo).country), state = tostring(parse_json(GeoIPInfo).state), city = tostring(parse_json(GeoIPInfo).city), latitude = tostring(parse_json(GeoIPInfo).latitude), longitude = tostring(parse_json(GeoIPInfo).longitude)
| project-reorder TimeGenerated, DeviceName, RemoteIP, RemotePort, RemoteUrl

DetectMsiexecExecutingDllNetworkConnections

Joins network connections back to their initiating process to catch msiexec spawning a child that then calls out, the classic MSI-delivered loader pattern.

DeviceNetworkEvents
| where TimeGenerated > ago(1h)
| where InitiatingProcessParentFileName =~ "msiexec.exe"
| join kind=inner (
    DeviceProcessEvents
    | where Timestamp > ago(3d)
    | where InitiatingProcessFileName =~ "msiexec.exe"
) on DeviceId, 
    $left.InitiatingProcessParentId == $right.InitiatingProcessId,
    $left.InitiatingProcessParentCreationTime == $right.InitiatingProcessCreationTime
| where InitiatingProcessCommandLine1 has_any ("/y", "-y", "/z", "-z")

SuspiciousRUNMRUEntry

Reads the RunMRU registry key for encoded PowerShell, curl and mshta invocations, which is where ClickFix-style paste-and-run lures leave their fingerprint.

let Parameters = dynamic(['http', 'https', 'Encoded', 'EncodedCommand', '-e', '-eC', '-enc', "-w", '-i', '/i','/e', '/eC', '/enc', "/w", 'wind', 'nop', 'DownloadString', 'FromBase64String', 'iwr', '$env']);
let Executables = dynamic(["cmd", "powershell", "curl", "mshta", "msiexec", 'SyncAppvPublishingServer']);
DeviceRegistryEvents
| where ActionType == "RegistryValueSet"
| where RegistryKey has "RunMRU"
| where RegistryValueData has_any (Parameters) and RegistryValueData has_any (Executables)
| project-reorder TimeGenerated, DeviceId, DeviceName, RegistryValueData, RegistryKey

macOS LoginWindow Hooks & Authorization Plugins

Watches DeviceFileEvents for writes into the macOS SecurityAgentPlugins and LoginHook paths, a persistence spot that survives reboots and rarely changes on a managed Mac fleet.

let ExcludedBinaries = dynamic(["acrappyAPP"]);
DeviceFileEvents
| where Timestamp > ago(24h)
| where FolderPath has_any (
    "/Library/Security/SecurityAgentPlugins/",
    "/System/Library/CoreServices/SecurityAgentPlugins/",
    "/Library/Security/SecurityAgentPlugins.bundle"
    )
    or (FolderPath contains "/loginwindow" and ActionType in ("FileCreated", "FileModified"))
| where InitiatingProcessFileName !in ("installer", "softwareupdated", "installd")
| invoke FileProfile(SHA1)
| where GlobalPrevalence < 10000

Mass Wipe or Retire Device Action

Flags an operator wiping or retiring more than five Intune devices in an hour, the shape a compromised endpoint admin account leaves behind.

let WipeThreashold = 5; // A normal engineer is not wiping more than 5 devices per hour. Can be adjusted for the environment.
IntuneAuditLogs 
| where OperationName in('wipe ManagedDevice','retire ManagedDevice')
| where  ResultType == 'Success'
| where isnotempty(Properties)
| extend Targets = extract_json("$.Targets", Properties, typeof(dynamic))
| extend TargetCount= array_length(Targets)
| extend Actor = extract_json("$.Actor", Properties, typeof(string))
| extend AccountObjectId = extract_json("$.ObjectId", Actor, typeof(guid))
| summarize TotalWipeOrRetireTargetCount=sum(TargetCount), TimeGenerated=min(TimeGenerated), Identity=min(Identity) by AccountObjectId
| where TotalWipeOrRetireTargetCount > WipeThreashold