These are general patterns, not production rules. Queries are illustrative, written in Sumo Logic syntax, and every account name and IP address is made up.
Console sign-ins without MFA, minus the federation noise
"Alert on any console login without MFA" sounds simple until you turn it on. In an environment where people sign in through an SSO provider, CloudTrail records those federated sign-ins with MFAUsed set to No, because the MFA happened at the identity provider rather than in AWS. The naive rule fires on nearly everyone.
The fix is to look at who signed in. Federated users appear as assumed roles; IAM users with their own passwords appear as IAMUser. Those are the accounts that should be rare, and the ones where a missing MFA really matters.
_sourceCategory=aws/cloudtrail "ConsoleLogin"
| json "eventName", "userIdentity.type", "userIdentity.userName",
"additionalEventData.MFAUsed", "sourceIPAddress"
as event, type, user, mfa, src_ip
| where event = "ConsoleLogin" and mfa = "No"
// federated SSO sign-ins enforce MFA at the IdP
| where type = "IAMUser"
| count by user, src_ip
Security group changes that actually matter
Security group edits happen constantly, and most of them come through a reviewed infrastructure pipeline. Alerting on every AuthorizeSecurityGroupIngress buries the one that matters: someone opening a port to the whole internet from outside that pipeline.
So the detection narrows twice. It keeps only rules that open ingress to 0.0.0.0/0 or ::/0, then excludes the deployment role whose changes already went through code review.
_sourceCategory=aws/cloudtrail "AuthorizeSecurityGroupIngress"
| json "eventName", "userIdentity.arn", "requestParameters.groupId"
as event, actor, sg
| where event = "AuthorizeSecurityGroupIngress"
| where _raw matches "*0.0.0.0/0*" or _raw matches "*::/0*"
| where !(actor matches "*:assumed-role/terraform-deploy/*")
| count by actor, sg
GuardDuty triage: was the connection accepted?
When GuardDuty reports an instance talking to a threat-listed IP, the first question isn't how scary the IP is. It's whether the traffic got through. VPC Flow Logs answer that in one query: if there are no ACCEPT records between that IP and the instance, the security group did its job and the finding can be closed with evidence. If there are, the destination port tells you where to look next.
_sourceCategory=aws/vpcflow "198.51.100.7"
| parse "* * * * * * * * * * * * * *" as version, account, eni, src, dst,
srcport, dstport, proto, packets, bytes, start, end, action, status
| where src = "198.51.100.7" and action = "ACCEPT"
| count by eni, dst, dstport
Least privilege is detection work too
Some of the highest-value findings aren't events at all. A role that can call sts:AssumeRole on Resource: "*" can pivot into any role whose trust policy allows it. Scoping that grant to the specific admin roles it needs removes a whole category of lateral movement, and it makes any remaining AssumeRole alert far easier to judge.
// before
{ "Effect": "Allow", "Action": "sts:AssumeRole", "Resource": "*" }
// after
{ "Effect": "Allow", "Action": "sts:AssumeRole",
"Resource": [
"arn:aws:iam::111122223333:role/platform-admin",
"arn:aws:iam::444455556666:role/platform-admin"
] }
Rules I hold every alert to
- It has a next step. If the person paged can't say what they'd check first, the alert isn't done.
- It's baselined before it's on. I run the query over recent history first and look at what would have fired.
- Suppressions are written down. Every exception carries a business justification someone can review later.
- It's tested with a known-bad event. An alert that has never fired might just be broken.