Detection Queries for Cloud Threats
Knowing what a password spray or a suspicious OAuth grant looks like in the abstract only gets you halfway there. Someone has to turn that knowledge into a query that actually runs against a log platform, returns in a reasonable time, and doesn't page the on-call analyst every time a CI/CD pipeline does its job. This chapter covers how detection logic gets written for cloud threat scenarios across the three formats you'll encounter most: KQL against Microsoft Sentinel, SPL against Splunk, and Sigma as the format that lets you write the logic once and target either backend.
Choosing the Right Query Language for the Data Source
Detection engineers don't usually pick a query language the way they'd pick a programming language, by preference or prior familiarity. The choice almost always follows the log platform the data already lives in, and the log platform is usually decided by whichever team stood up the SIEM long before detection content was on anyone's roadmap. Writing detections means learning to work fluently in whatever the organization already has, and most cloud-focused analysts end up conversant in more than one.
KQL, Kusto Query Language, is the language of Microsoft Sentinel and any Log Analytics workspace underneath it. If an organization is running Entra ID (formerly Azure AD) and forwarding sign-in and audit telemetry into Sentinel, which is the most common destination for that data, detections against it get written in KQL. The language reads closer to a data pipeline than a traditional search syntax: a table name, piped through a sequence of filtering, projection, and aggregation operators, each stage narrowing or reshaping the result set that flows into the next.
SPL, Search Processing Language, is Splunk's native query language, and it's the one you'll reach for most often when AWS CloudTrail is the data source, since CloudTrail logs shipped through a forwarder or an add-on into Splunk are extremely common in AWS-heavy environments that adopted Splunk before cloud-native SIEM options matured. SPL has a similar pipe-based feel to KQL at a conceptual level, a base search followed by a chain of commands, but the syntax, function names, and idioms are different enough that fluency in one doesn't transfer automatically to the other.
Sigma sits apart from both because it isn't tied to a backend at all. It's a vendor-neutral, YAML-based format for describing detection logic in the abstract: what log source, what field values, what condition. A Sigma rule on its own doesn't execute anywhere. It gets converted, typically with a tool like sigma-cli or the pySigma conversion backends, into the native query syntax of whatever platform is going to run it, KQL, SPL, or otherwise. That indirection is the entire point: an analyst writes the detection logic once and doesn't have to hand-translate it every time it needs to run somewhere new.
| Attribute | KQL | SPL | Sigma |
|---|---|---|---|
| Typical backend | Microsoft Sentinel / Log Analytics | Splunk | None natively (converts to a backend) |
| Typical log source in this module | Entra SigninLogs / AuditLogs | AWS CloudTrail (aws:cloudtrail sourcetype) | Any (logsource defines the mapping) |
| Primary use case | Native, platform-tuned identity detections | Native, platform-tuned CloudTrail detections | Portable, shareable detection logic across backends |
KQL for Entra Sign-In and Audit Logs
Two Entra tables carry most of the identity detection weight in a Sentinel workspace: SigninLogs, which records every interactive and non-interactive authentication attempt, and AuditLogs, which records administrative and configuration actions taken within the tenant. Chapter 2 covered what password spray and consent phishing look like from an attacker's perspective. Here's how each shows up in the shape of a KQL query built against those two tables.
A password spray detection against SigninLogs starts by filtering on ResultType, the field that carries the sign-in error code. A ResultType of 50126 indicates an invalid username or password, and a small cluster of related codes covers other authentication failures. The query filters down to failed sign-ins within a rolling time window, then aggregates by UserPrincipalName and IPAddress using a summarize operator, counting distinct usernames touched from each source alongside the failure count per username. Password spray has a distinctive statistical signature once you see it this way: a small number of source IP addresses, each attempting authentication against a large number of distinct usernames, each username tried only once or twice. That's structurally different from credential stuffing (many attempts against the same account) or simple brute force (many attempts against one account from one source), and the aggregation is what separates the three.
A consent-grant detection against AuditLogs works differently because the goal isn't statistical, it's a review-worthy event that should surface every time it happens rather than only when it crosses a threshold. The query filters OperationName for values like "Consent to application", which fires whenever a user or admin grants an OAuth application delegated or application permissions into the tenant. Because most such events are benign (a user connecting a calendar app, a team approving a legitimate integration), the query typically projects out the granted permission scopes from the nested TargetResources field and flags anything requesting high-privilege scopes such as mail read/send, full directory access, or offline access, so an analyst reviews the request rather than the noise floor of routine consent activity.
SigninLogs
| where TimeGenerated > ago(1h)
| where ResultType in ("50126", "50053")
| summarize FailedUsers = dcount(UserPrincipalName),
Attempts = count()
by IPAddress, bin(TimeGenerated, 15m)
| where FailedUsers > 10 and (Attempts * 1.0 / FailedUsers) < 2
| order by FailedUsers desc
Grouping by a 15-minute bin keeps the spray window tight. The Attempts / FailedUsers ratio is the part that actually separates spray (low ratio, wide user fan-out) from brute force or credential stuffing (high ratio, narrow user fan-out) once a single IP clears the distinct-user threshold.
AuditLogs
| where OperationName == "Consent to application"
| extend GrantedScopes = tostring(TargetResources[0].modifiedProperties[0].newValue)
| where GrantedScopes has_any ("Mail.Read", "Mail.Send", "Directory.ReadWrite.All", "offline_access")
| project TimeGenerated, InitiatedBy, TargetResources, GrantedScopes
This surfaces only the consent grants worth a human look, high-privilege or offline-capable scopes, rather than every calendar-app approval that happens in a normal week.
| Field | Table | Purpose in detection |
|---|---|---|
| ResultType | SigninLogs | Identifies authentication failure vs. success |
| UserPrincipalName | SigninLogs | Grouping key for per-user and per-spray analysis |
| IPAddress | SigninLogs | Grouping key to isolate a small source-IP set |
| OperationName | AuditLogs | Isolates consent-grant and other admin actions |
| TargetResources | AuditLogs | Carries the granted permission scopes for review |
Sigma Rules for Cloud Log Sources
Sigma is best understood as a generic signature format for log-based detections, the log equivalent of what YARA does for files or Snort does for network traffic. A Sigma rule is a YAML document with a small, consistent set of top-level fields: a title and description, a logsource block identifying what kind of data the rule applies to, a detection block defining the actual matching logic, and metadata like severity level and references. None of that YAML executes directly against a log platform. A conversion tool reads it and emits the equivalent native query for whatever backend you point it at.
A Sigma rule targeting AWS CloudTrail sets its logsource category to cloudtrail (with product: aws), which tells the conversion tooling which field mappings to apply, since the same logical field name in Sigma might map to a different literal field name depending on how CloudTrail data lands in a given SIEM. The detection block then defines one or more named selections, each a set of field-value pairs to match, and a condition line that says how those selections combine. A rule watching for logging tampering, for instance, would define a selection matching eventName values of StopLogging or DeleteTrail, both of which are actions an attacker takes to blind CloudTrail after gaining a foothold, and a condition of simply selection to alert whenever that selection matches.
The value of writing that logic in Sigma instead of directly in KQL or SPL isn't stylistic, it's operational. An organization running Sentinel in one business unit and Splunk in another, or a detection engineer publishing rules for a community that spans multiple SIEM vendors, would otherwise have to hand-maintain parallel copies of the same logic in two syntaxes and keep them in sync by hand every time the rule gets tuned. Writing it once in Sigma and converting it per backend removes that duplication and the drift that comes with it. It's also why Sigma has become the de facto exchange format for publicly shared detection content: a rule published against CloudTrail logging-tampering behavior is useful to any analyst regardless of which SIEM they run, because the conversion step is the only thing standing between the rule and their platform.
title: AWS CloudTrail Logging Disabled or Deleted
id: 8f2b1c3a-5e6d-4a2b-9c3d-1a2b3c4d5e6f
status: experimental
description: Detects StopLogging or DeleteTrail, a common defense-evasion step after cloud initial access.
logsource:
product: aws
service: cloudtrail
detection:
selection:
eventName:
- StopLogging
- DeleteTrail
condition: selection
level: high
tags:
- attack.defense-evasion
- attack.t1562.008
One logsource, one detection block, one condition. The tag maps to ATT&CK T1562.008 (Impair Defenses: Disable or Modify Cloud Logs), which is what makes this rule useful for the coverage-mapping exercise Chapter 7 covers.
SPL for AWS CloudTrail in Splunk
CloudTrail data ingested into Splunk, typically through the AWS Add-on or a forwarder writing to an S3-backed input, lands under a sourcetype conventionally named something like aws:cloudtrail. An SPL search against it starts the same way most Splunk searches do: a base search scoped to that sourcetype and a relevant time range, followed by a chain of piped commands that filter and reshape the results.
A detection for suspicious IAM activity, for example watching for eventName values like CreateAccessKey or AttachUserPolicy, filters the base search down to those event names and then pipes into a stats command counting occurrences grouped by the identity that performed the action, typically fields nested under userIdentity such as the ARN or principal ID. That count-by-identity step is what turns a raw event stream into something an analyst can reason about: one AttachUserPolicy call from a principal that does this routinely as part of its job is unremarkable, ten from a principal that has never done it before is a different story entirely.
Where SPL detections often go further than a simple count is by joining against a lookup table of known-good identities or expected behavior. A lookup mapping IAM principal names or role ARNs to their normal function (a CI/CD deployment role, a backup service account) lets the search filter out or de-prioritize activity from identities that are expected to perform sensitive actions as part of their routine job, and surface activity from identities that aren't on that list at all, or that suddenly start performing an action their history and the lookup say they shouldn't. A threshold comparison, flagging counts that exceed a static number or a rolling average, layered on top of that lookup-based filtering is what turns a search into something closer to a production alert rather than a query an analyst has to run and eyeball manually.
sourcetype=aws:cloudtrail eventName IN ("CreateAccessKey", "AttachUserPolicy")
| stats count by userIdentity.arn, eventName
| lookup known_automation_identities.csv arn AS "userIdentity.arn" OUTPUT is_automation
| where isnull(is_automation) AND count > 3
| sort - count
The lookup is what separates "unremarkable, this role does this daily" from "this principal has never touched IAM before." Without it, the query flags the same known-good automation every single day.
Tuning Cloud Detections to Reduce False Positives
The single hardest problem in cloud detection engineering isn't writing a query that catches the threat, it's writing one that doesn't also catch everything else. A huge share of API activity in any real cloud environment is legitimate automation: CI/CD pipelines creating and tearing down infrastructure, infrastructure-as-code tools like Terraform applying changes on a schedule, Lambda functions or Azure Functions firing on triggers, service principals performing the same routine task hundreds of times a day. A naive detection written directly against a sensitive event name, alert whenever AttachUserPolicy or CreateAccessKey fires, will bury an analyst in noise from that automation within the first day it's live, and an analyst drowning in noise stops trusting the detection entirely, which is worse than not having it.
The first practical tuning lever is allowlisting known automation identities. If a specific service principal or IAM role is known to perform a given sensitive action as its entire job, excluding that identity from the detection (or routing its matches to a low-priority queue instead of a page) removes the largest and most predictable source of noise without weakening the detection against anything unexpected. This has to be maintained deliberately, since an allowlist that never gets reviewed becomes a blind spot an attacker could eventually exploit by compromising one of the excluded identities.
The second lever is baselining normal call volume per identity before alerting on deviation, rather than alerting on the presence of an event at all. An identity that calls a given API a few hundred times a day, every day, generating an alert on the two-hundred-and-first call accomplishes nothing. Alerting when that same identity's call volume jumps well outside its established pattern, or when it calls an API it has never called before, produces a signal that actually correlates with something having changed, which is usually what a detection is trying to catch in the first place.
The third and most durable lever is combining multiple weak signals instead of keying a detection off any single event name. An unusual action, from an unusual source, outside the identity's normal operating hours, is a meaningfully stronger signal than any one of those three conditions alone, and building the detection logic to require some combination of them (rather than firing on the first condition met) is usually what separates a detection that survives contact with a real production environment from one that gets disabled within a week because it never stops firing.
From Detection to Response
A detection firing is the start of the response process, not the end of it, and cloud response looks meaningfully different from the traditional playbook most analysts learn first. Traditional incident response against a compromised host centers on network containment: pull the machine off the network, isolate it, stop it from talking to anything else while it's investigated. That model doesn't map onto a compromised cloud identity, because there's no network segment to isolate an identity from. An attacker holding valid credentials for a service principal or an IAM role can act against the cloud API from anywhere on the internet, and revoking their network access changes nothing about their ability to keep calling that API.
Containing a compromised cloud identity means disabling or rotating its credentials (resetting the password or disabling the account for a human identity, rotating or deleting the access key for a service account), revoking any active sessions and refresh tokens so previously issued authentication doesn't keep working even after the credential itself is changed, and reviewing what permissions and OAuth consents were granted recently, since an attacker who had access for any length of time may have added a persistence mechanism, like a new application registration or a broadened role assignment, that survives a simple credential rotation. Skipping that last step is one of the more common reasons an incident that looked closed reopens a few weeks later.
Doing all of that manually, one console click or one CLI command at a time, is often too slow to matter. An attacker operating through a compromised cloud identity can act as fast as the API allows, and a human analyst working through a runbook step by step is not going to out-pace that. This is where SOAR playbooks and response automation earn their keep in a cloud environment specifically: a detection firing can trigger an automated sequence that disables the identity, revokes its tokens, and opens a case for human review, all within seconds of the alert, with the analyst's role shifting from executing each containment step to validating that the automation did the right thing and picking up the investigation from there.
Key Takeaways
- Query language choice follows the log platform: KQL for Sentinel/Log Analytics (typically Entra sign-in and audit data), SPL for Splunk (typically AWS CloudTrail), Sigma as a vendor-neutral format that converts to either.
- KQL password spray detections work by aggregating SigninLogs failures by UserPrincipalName and IPAddress, looking for many distinct users and few attempts each from a small set of sources. Consent-grant detections filter AuditLogs OperationName for consent events and inspect the granted scopes.
- Sigma rules define a logsource, a detection block of named selections, and a condition, written once and converted per backend, which is what makes shared community detection content practical.
- SPL CloudTrail detections typically use stats/count grouped by userIdentity fields, often joined against a lookup of known automation identities to separate expected activity from unexpected volume.
- The dominant false-positive source in cloud detection is legitimate automation. Allowlisting known identities, baselining normal call volume, and requiring multiple weak signals together are the practical levers for reducing noise without losing real detections.
- Containing a compromised cloud identity means rotating credentials, revoking sessions and tokens, and reviewing recently granted permissions, not network isolation, and SOAR automation is usually necessary to do it fast enough to matter.
Knowledge Check
Click an answer to reveal the explanation.
Why is Sigma valuable for a detection engineer working across multiple SIEM backends?
Why do naive detections built only around a sensitive event name (such as alerting on every AttachUserPolicy call) tend to generate excessive false positives in cloud environments?
What does "containing a compromised identity" actually involve in a cloud environment?