EventBridge Pattern Matching: A Field Guide
Introduction
Writing effective patterns requires understanding how EventBridge interprets the JSON structure you provide. Simple patterns are straightforward, but as patterns grow complex-especially with nested $or operators-the matching logic becomes harder to reason about.
AWS's official documentation covers the mechanics, but it's intentionally terse. When you're building event-driven architectures and need to detect specific conditions across CloudTrail events, security alerts, or application events, you need more than syntax reference-you need mental models for how patterns actually work.
This guide walks through pattern construction step by step, from basic field matching to complex multi-level logic. Throughout, we'll use boolean algebra notation alongside the JSON patterns. This isn't something you need to write yourself-it's a tool to help visualize and reason about the logic of your rule. Whether you're building detection rules, automating responses, or orchestrating workflows, understanding pattern expansion will help you write rules that match exactly what you intend.
Foundation: Fields Are AND, Arrays Are OR
Two core rules govern EventBridge pattern evaluation:
- Multiple fields at the same level = AND (all must match)
- Multiple values in an array = OR (any can match)
Pattern:
1{2 "source": ["aws.iam"],3 "detail-type": ["AWS API Call via CloudTrail"],4 "detail": {5 "eventName": ["CreateAccessKey", "DeleteAccessKey", "UpdateAccessKey"]6 }7}8
Boolean Algebra:
1(2 source = "aws.iam"3 AND detail-type = "AWS API Call via CloudTrail"4 AND (5 detail.eventName = "CreateAccessKey"6 OR detail.eventName = "DeleteAccessKey"7 OR detail.eventName = "UpdateAccessKey"8 )9)10
The source and detail-type fields are ANDed together-both must match. The eventName array creates OR logic: the event matches if eventName equals any of the three values.
Matching Event:
1{2 "version": "0",3 "id": "c7118dc7-e242-4e48-ab7c-4c65e5a6b9d7",4 "detail-type": "AWS API Call via CloudTrail",5 "source": "aws.iam",6 "account": "123456789012",7 "time": "2025-01-15T10:30:00Z",8 "region": "us-east-1",9 "resources": [],10 "detail": {11 "eventVersion": "1.08",12 "eventName": "CreateAccessKey",13 "eventSource": "iam.amazonaws.com",14 "userIdentity": {15 "type": "IAMUser",16 "principalId": "AIDAI23HXD4EXAMPLE",17 "arn": "arn:aws:iam::123456789012:user/alice",18 "accountId": "123456789012",19 "userName": "alice"20 },21 "requestParameters": {22 "userName": "bob"23 },24 "responseElements": {25 "accessKey": {26 "accessKeyId": "AKIAIOSFODNN7EXAMPLE",27 "status": "Active",28 "userName": "bob",29 "createDate": "2025-01-15T11:45:00Z"30 }31 }32 }33}34
The $or Operator: OR Between Different Fields
Field-level arrays create OR within a single field. When you need OR logic between different fields, use the $or operator.
Pattern:
1{2 "source": ["aws.iam"],3 "detail-type": ["AWS API Call via CloudTrail"],4 "detail": {5 "eventName": ["AssumeRole"],6 "$or": [7 {8 "errorCode": ["AccessDenied"]9 },10 {11 "userIdentity": {12 "type": ["Root"]13 }14 }15 ]16 }17}18
Boolean Algebra:
1(2 source = "aws.iam"3 AND detail-type = "AWS API Call via CloudTrail"4 AND (5 detail.eventName = "AssumeRole"6 AND (7 detail.errorCode = "AccessDenied"8 OR detail.userIdentity.type = "Root"9 )10 )11)12
This matches AssumeRole calls that either failed with AccessDenied OR were made by the Root user.
Matching Event (Root user):
1{2 "version": "0",3 "id": "c7118dc7-e242-4e48-ab7c-4c65e5a6b9d7",4 "detail-type": "AWS API Call via CloudTrail",5 "source": "aws.iam",6 "account": "123456789012",7 "time": "2025-01-15T10:30:00Z",8 "region": "us-east-1",9 "resources": [],10 "detail": {11 "eventVersion": "1.08",12 "eventName": "AssumeRole",13 "eventSource": "sts.amazonaws.com",14 "userIdentity": {15 "type": "Root",16 "principalId": "123456789012",17 "arn": "arn:aws:iam::123456789012:root",18 "accountId": "123456789012"19 },20 "requestParameters": {21 "roleArn": "arn:aws:iam::123456789012:role/ProductionRole",22 "roleSessionName": "RootSession"23 },24 "responseElements": {25 "credentials": {26 "accessKeyId": "ASIAIOSFODNN7EXAMPLE",27 "expiration": "2025-01-15T13:00:00Z"28 }29 }30 }31}32
Matching Event (AccessDenied):
1{2 "version": "0",3 "id": "c7118dc7-e242-4e48-ab7c-4c65e5a6b9d7",4 "detail-type": "AWS API Call via CloudTrail",5 "source": "aws.iam",6 "account": "123456789012",7 "time": "2025-01-15T10:30:00Z",8 "region": "us-east-1",9 "resources": [],10 "detail": {11 "eventVersion": "1.08",12 "eventName": "AssumeRole",13 "eventSource": "sts.amazonaws.com",14 "errorCode": "AccessDenied",15 "errorMessage": "User: arn:aws:iam::123456789012:user/alice is not authorized to perform: sts:AssumeRole",16 "userIdentity": {17 "type": "IAMUser",18 "principalId": "AIDAI23HXD4EXAMPLE",19 "arn": "arn:aws:iam::123456789012:user/alice",20 "accountId": "123456789012",21 "userName": "alice"22 },23 "requestParameters": null,24 "responseElements": null25 }26}27
Pattern Operators
EventBridge supports matching operators beyond exact equality.
prefix / suffix
Match strings that start or end with a specific value.
Pattern:
1{2 "detail": {3 "eventName": [{ "prefix": "Delete" }]4 }5}6
Matches: DeleteBucket, DeleteObject, DeleteUser
wildcard
Glob pattern matching using * as wildcard.
Pattern:
1{2 "detail": {3 "eventSource": [{ "wildcard": "*.amazonaws.com" }]4 }5}6
Matches: s3.amazonaws.com, ec2.amazonaws.com, iam.amazonaws.com
numeric
Number comparisons with operators: >, >=, <, <=, =.
Pattern:
1{2 "detail": {3 "responseElements": {4 "httpStatusCode": [{ "numeric": [">=", 400, "<", 500] }]5 }6 }7}8
Matches: 400, 404, 403, 499 (client errors)
exists
Check for field presence or absence.
Pattern:
1{2 "detail": {3 "errorCode": [{ "exists": true }],4 "responseElements": [{ "exists": false }]5 }6}7
Matches events that have an error code but no response.
cidr
IP address matching within CIDR range.
Pattern:
1{2 "detail": {3 "sourceIPAddress": [{ "cidr": "10.0.0.0/8" }]4 }5}6
Matches: 10.0.0.1, 10.255.255.255
equals-ignore-case
Case-insensitive string matching.
Pattern:
1{2 "detail": {3 "errorCode": [{ "equals-ignore-case": "accessdenied" }]4 }5}6
Matches: AccessDenied, ACCESSDENIED, accessdenied
anything-but
Negation operator. Matches when value does NOT equal any of the specified values.
Pattern:
1{2 "detail": {3 "eventName": [{ "anything-but": ["GetObject", "HeadObject"] }]4 }5}6
Matches any eventName EXCEPT GetObject or HeadObject.
Important: anything-but with multiple values uses AND logic. The value must NOT match "GetObject" AND must NOT match "HeadObject".
anything-but with Nested Operators
anything-but can wrap other operators to negate them.
Pattern:
1{2 "detail.userIdentity.userName": [3 {4 "anything-but": {5 "prefix": ["system-", "service-"]6 }7 }8 ]9}10
When anything-but contains multiple values in an operator like {"prefix": ["test-", "dev-"]}, it creates AND logic. The value must NOT start with "test-" AND must NOT start with "dev-".
1(2 detail.userIdentity.userName NOT_STARTS_WITH "system-"3 AND detail.userIdentity.userName NOT_STARTS_WITH "service-"4)5
Pattern Expansion: How $or Works
EventBridge doesn't evaluate $or at runtime. During rule creation, it expands patterns containing $or into multiple sub-patterns-one for each possible path through all OR branches.
Pattern:
1{2 "source": ["aws.iam"],3 "detail-type": ["AWS API Call via CloudTrail"],4 "detail": {5 "eventName": ["AssumeRole"],6 "$or": [7 { "errorCode": ["AccessDenied"] },8 { "userIdentity": { "type": ["Root"] } }9 ]10 }11}12
EventBridge expands this to 2 sub-patterns:
Sub-pattern 1:
1source = "aws.iam"2AND detail-type = "AWS API Call via CloudTrail"3AND eventName = "AssumeRole"4AND errorCode = "AccessDenied"5
Sub-pattern 2:
1source = "aws.iam"2AND detail-type = "AWS API Call via CloudTrail"3AND eventName = "AssumeRole"4AND userIdentity.type = "Root"5
An event matches if it satisfies ANY complete sub-pattern. All fields outside $or are distributed to every sub-pattern.
Multiple $or: Combinatorial Expansion
When you use multiple $or operators in a pattern, they multiply to create more sub-patterns.
Pattern:
1{2 "source": ["aws.s3"],3 "detail-type": ["AWS API Call via CloudTrail"],4 "detail": {5 "$or": [6 { "eventName": ["PutBucketPolicy"] },7 { "eventName": ["PutBucketAcl"] }8 ],9 "requestParameters": {10 "$or": [{ "acl": ["public-read"] }, { "acl": ["public-read-write"] }]11 }12 }13}14
Expansion: 2 branches × 2 branches = 4 sub-patterns
eventName = "PutBucketPolicy" AND acl = "public-read"eventName = "PutBucketPolicy" AND acl = "public-read-write"eventName = "PutBucketAcl" AND acl = "public-read"eventName = "PutBucketAcl" AND acl = "public-read-write"
Three $or operators with 2 branches each: 2 × 2 × 2 = 8 sub-patterns.
Important: Field-level arrays don't multiply sub-patterns-they stay intact within each sub-pattern. An array like ["CreateAccessKey", "DeleteAccessKey"] creates OR logic within the sub-pattern, not additional sub-patterns.
Matching Event:
1{2 "version": "0",3 "id": "c7118dc7-e242-4e48-ab7c-4c65e5a6b9d7",4 "detail-type": "AWS API Call via CloudTrail",5 "source": "aws.s3",6 "account": "123456789012",7 "time": "2025-01-15T10:30:00Z",8 "region": "us-east-1",9 "resources": [],10 "detail": {11 "eventVersion": "1.08",12 "eventName": "PutBucketAcl",13 "eventSource": "s3.amazonaws.com",14 "requestParameters": {15 "bucketName": "my-public-bucket",16 "acl": "public-read"17 },18 "responseElements": null,19 "userIdentity": {20 "type": "IAMUser",21 "principalId": "AIDAI23HXD4EXAMPLE",22 "arn": "arn:aws:iam::123456789012:user/alice",23 "accountId": "123456789012",24 "userName": "alice"25 }26 }27}28
Nested $or: Hierarchical Logic
You can nest $or within $or branches to create complex logic trees.
Pattern:
1{2 "source": ["aws.iam"],3 "detail-type": ["AWS API Call via CloudTrail"],4 "detail": {5 "$or": [6 {7 "eventName": ["CreateAccessKey"],8 "$or": [9 { "userIdentity": { "type": ["Root"] } },10 { "userIdentity": { "type": ["AssumedRole"] } }11 ]12 },13 {14 "eventName": ["CreateUser"],15 "requestParameters": {16 "$or": [17 { "userName": [{ "prefix": "admin-" }] },18 { "userName": [{ "prefix": "root-" }] }19 ]20 }21 }22 ]23 }24}25
Expands to 4 sub-patterns:
CreateAccessKey AND type = "Root"CreateAccessKey AND type = "AssumedRole"CreateUser AND userName starts with "admin-"CreateUser AND userName starts with "root-"
The outer $or creates two main paths. Each path contains its own $or, which further subdivides that path.
Matching Event:
1{2 "version": "0",3 "id": "c7118dc7-e242-4e48-ab7c-4c65e5a6b9d7",4 "detail-type": "AWS API Call via CloudTrail",5 "source": "aws.iam",6 "account": "123456789012",7 "time": "2025-01-15T10:30:00Z",8 "region": "us-east-1",9 "resources": [],10 "detail": {11 "eventVersion": "1.08",12 "eventName": "CreateAccessKey",13 "eventSource": "iam.amazonaws.com",14 "userIdentity": {15 "type": "Root",16 "principalId": "123456789012",17 "arn": "arn:aws:iam::123456789012:root",18 "accountId": "123456789012"19 },20 "requestParameters": {21 "userName": "test-user"22 },23 "responseElements": {24 "accessKey": {25 "accessKeyId": "AKIAIOSFODNN7EXAMPLE",26 "status": "Active",27 "userName": "test-user",28 "createDate": "2025-01-15T16:00:00Z"29 }30 }31 }32}33
Root-Level $or: Independent Patterns
The $or operator can appear at the root level to match completely different event types in a single rule.
Pattern:
1{2 "detail-type": ["AWS API Call via CloudTrail"],3 "$or": [4 {5 "source": ["aws.s3"],6 "detail": {7 "eventName": ["DeleteBucket"]8 }9 },10 {11 "source": ["aws.iam"],12 "detail": {13 "eventName": ["CreateAccessKey"]14 }15 }16 ]17}18
1(2 detail-type = "AWS API Call via CloudTrail"3 AND (4 (5 source = "aws.s3"6 AND detail.eventName = "DeleteBucket"7 )8 OR (9 source = "aws.iam"10 AND detail.eventName = "CreateAccessKey"11 )12 )13)14
This single rule matches operations across two different AWS services. Each branch is completely independent.
Matching Event (S3):
1{2 "version": "0",3 "id": "c7118dc7-e242-4e48-ab7c-4c65e5a6b9d7",4 "detail-type": "AWS API Call via CloudTrail",5 "source": "aws.s3",6 "account": "123456789012",7 "time": "2025-01-15T10:30:00Z",8 "region": "us-east-1",9 "resources": [],10 "detail": {11 "eventVersion": "1.08",12 "eventName": "DeleteBucket",13 "eventSource": "s3.amazonaws.com",14 "requestParameters": {15 "bucketName": "old-test-bucket"16 },17 "responseElements": null,18 "userIdentity": {19 "type": "IAMUser",20 "principalId": "AIDAI23HXD4EXAMPLE",21 "arn": "arn:aws:iam::123456789012:user/alice",22 "accountId": "123456789012",23 "userName": "alice"24 }25 }26}27
Three-Level Nesting Example
Pattern:
1{2 "source": ["aws.s3"],3 "detail-type": ["AWS API Call via CloudTrail"],4 "detail": {5 "readOnly": [false],6 "$or": [7 {8 "eventSource": ["s3.amazonaws.com"],9 "$or": [10 {11 "eventName": ["DeleteBucket", "DeleteBucketPolicy"],12 "errorCode": [{ "exists": false }]13 },14 {15 "eventName": ["PutBucketAcl"],16 "requestParameters": {17 "$or": [18 { "acl": ["public-read"] },19 { "acl": ["public-read-write"] },20 { "grantFullControl": [{ "wildcard": "*" }] }21 ]22 }23 }24 ]25 },26 {27 "eventSource": ["iam.amazonaws.com"],28 "eventName": [{ "prefix": "Delete" }],29 "userIdentity": {30 "type": ["IAMUser"],31 "$or": [32 { "userName": [{ "prefix": "admin-" }] },33 { "userName": [{ "prefix": "service-" }] }34 ]35 }36 }37 ]38 }39}40
Expansion:
S3 branch:
- Sub-pattern 1: Delete bucket operations (successful)
- Sub-pattern 2a: PutBucketAcl with public-read
- Sub-pattern 2b: PutBucketAcl with public-read-write
- Sub-pattern 2c: PutBucketAcl with grantFullControl wildcard
IAM branch:
- Sub-pattern 3a: Delete operations by IAMUser with username starting "admin-"
- Sub-pattern 3b: Delete operations by IAMUser with username starting "service-"
Total: 6 sub-patterns
Each sub-pattern includes readOnly = false from the root level.
To verify our intuition
1(2 source = "aws.s3"3 AND detail-type = "AWS API Call via CloudTrail"4 AND (5 detail.readOnly = false6 AND (7 (8 detail.eventSource = "s3.amazonaws.com"9 AND (10 (11 (12 detail.eventName = "DeleteBucket"13 OR detail.eventName = "DeleteBucketPolicy"14 )15 AND detail.errorCode NOT_EXISTS16 )17 OR (18 detail.eventName = "PutBucketAcl"19 AND (20 (21 detail.requestParameters.acl = "public-read"22 OR detail.requestParameters.acl = "public-read-write"23 OR detail.requestParameters.grantFullControl MATCHES_WILDCARD "*"24 )25 )26 )27 )28 )29 OR (30 detail.eventSource = "iam.amazonaws.com"31 AND detail.eventName STARTS_WITH "Delete"32 AND (33 detail.userIdentity.type = "IAMUser"34 AND (35 detail.userIdentity.userName STARTS_WITH "admin-"36 OR detail.userIdentity.userName STARTS_WITH "service-"37 )38 )39 )40 )41 )42)43
Common Pitfalls
Match-All Events
An empty rule, {} while makes sense to catch everything - EventBridge does not allow empty objects - thus, a simple rule that validates the source exists can be used to match all events.
1{2 "source": [{ "exists": true }]3}4
The Root-Level Field Trap
Mixing root-level fields with root-level $or creates logical contradictions that make branches unreachable.
Pattern (INCORRECT):
1{2 "source": ["aws.s3"],3 "$or": [4 {5 "source": ["aws.s3"],6 "detail": {7 "eventName": ["DeleteBucket"]8 }9 },10 {11 "source": ["aws.cloudtrail"],12 "detail": {13 "eventName": ["DeleteTrail"]14 }15 }16 ]17}18
The root-level source = "aws.s3" gets ANDed with BOTH branches:
- Branch 1:
source = "aws.s3" AND source = "aws.s3"- Redundant but works - Branch 2:
source = "aws.s3" AND source = "aws.cloudtrail"- Impossible
1(2 source = "aws.s3"3 AND (4 (5 detail.eventName = "DeleteBucket"6 AND source = "aws.s3"7 )8 OR (9 source = "aws.cloudtrail"10 AND detail.eventName = "DeleteTrail"11 )12 )13)14
The second branch can never match because a source field cannot equal two different values simultaneously.
The Fix:
1{2 "$or": [3 {4 "source": ["aws.s3"],5 "detail": { "eventName": ["DeleteBucket"] }6 },7 {8 "source": ["aws.cloudtrail"],9 "detail": { "eventName": ["DeleteTrail"] }10 }11 ]12}13
Sub-Pattern Explosion
Each $or operator multiplies the number of sub-patterns:
- 2
$orwith 2 branches each = 4 sub-patterns - 3
$orwith 2 branches each = 8 sub-patterns - 3
$orwith 3 branches each = 27 sub-patterns
If your pattern creates too many sub-patterns, consider:
- Reducing nesting depth
- Splitting into multiple rules
- Using field-level arrays instead of
$orwhere possible
As a fun example, here's a less than optimal example to match all - that should be easy to follow/understand why exactly..
1{2 "$or": [3 {4 "source": [{ "prefix": "" }],5 "$or": [6 {7 "source": [{ "wildcard": "*" }],8 "$or": [9 { "source": [{ "exists": true }] },10 { "source": [{ "suffix": "" }] }11 ]12 },13 {14 "source": [{ "anything-but": "absolutely-never-this-source" }],15 "$or": [16 {17 "source": [{ "equals-ignore-case": "aws.s3" }, "aws.s3", "AWS.S3"]18 },19 { "source": [{ "wildcard": "aws.*" }] }20 ]21 }22 ]23 },24 {25 "source": [{ "wildcard": "*.amazonaws.com" }],26 "$or": [27 {28 "source": [{ "prefix": "aws" }],29 "$or": [30 { "source": [{ "suffix": "com" }] },31 { "source": [{ "exists": true }] }32 ]33 },34 {35 "source": [{ "anything-but": { "prefix": "never-matches-" } }],36 "$or": [37 { "source": [{ "wildcard": "*.*" }] },38 { "source": [{ "prefix": "" }] }39 ]40 }41 ]42 }43 ]44}45
1(2 (3 (4 source STARTS_WITH ""5 AND (6 (7 source MATCHES_WILDCARD "*"8 AND (9 source EXISTS10 OR source ENDS_WITH ""11 )12 )13 OR (14 source != "absolutely-never-this-source"15 AND (16 (17 source EQUALS_IGNORECASE "aws.s3"18 OR source = "aws.s3"19 OR source = "AWS.S3"20 )21 OR source MATCHES_WILDCARD "aws.*"22 )23 )24 )25 )26 OR (27 source MATCHES_WILDCARD "*.amazonaws.com"28 AND (29 (30 source STARTS_WITH "aws"31 AND (32 source ENDS_WITH "com"33 OR source EXISTS34 )35 )36 OR (37 source NOT_STARTS_WITH "never-matches-"38 AND (39 source MATCHES_WILDCARD "*.*"40 OR source STARTS_WITH ""41 )42 )43 )44 )45 )46)47
Pattern Boundaries
Field-level arrays already provide OR logic:
1{2 "detail": {3 "eventName": [{ "prefix": "Create" }, { "prefix": "Delete" }]4 }5}6
This means: eventName starts with "Create" OR eventName starts with "Delete"
1(2 detail.eventName STARTS_WITH "Create"3 OR detail.eventName STARTS_WITH "Delete"4)5
Empty arrays/objects not allowed:
You need at least two objects within an $or relationship.
1{2 "detail": {3 "$or": [] // INVALID4 }5}6
Pattern Construction Strategy
When writing patterns:
1. Identify AND relationships:
Place these as sibling fields.
1{2 "field1": ["value1"],3 "field2": ["value2"]4}5
2. Identify OR relationships within a field:
Use field-level arrays.
1{2 "eventName": ["Create", "Delete", "Update"]3}4
3. Identify OR relationships between fields:
Use the $or operator.
1{2 "$or": [{ "field1": ["value1"] }, { "field2": ["value2"] }]3}4
4. Count sub-pattern explosion:
Each $or multiplies sub-patterns. Verify this matches your intent.
5. Reason about the logic:
Does the pattern capture your intent? Are there contradictions or unreachable branches?
6. Verify field paths:
Ensure you're using the correct field nesting. detail.eventName vs eventName without any nesting matters!
Debugging Pattern Matching Issues
When patterns don't match expected events:
- Trace the pattern's logic
- Check each sub-pattern's constraints
- Verify constraints aren't contradictory
- Check field paths
- Confirm operator semantics
Example of a problematic pattern:
1{2 "source": ["aws.s3"],3 "$or": [4 {5 "source": ["aws.s3"],6 "detail": {7 "eventName": ["DeleteBucket"]8 }9 },10 {11 "source": ["aws.cloudtrail"],12 "detail": {13 "eventName": ["DeleteTrail"]14 }15 }16 ]17}18
The second branch can never match because of the root-level source = "aws.s3" constraint creating a contradiction with source = "aws.cloudtrail".
Conclusion
EventBridge patterns combine these primitives:
- Fields create AND relationships
- Arrays create OR relationships within fields
$orcreates OR relationships between fields- Nesting creates hierarchical evaluation paths
- Operators modify field matching semantics
Understanding pattern expansion-how $or unfolds into sub-patterns-is key to writing patterns that work as intended.
Good patterns:
- Match what you intend to detect
- Don't have logical contradictions
- Are readable when you revisit them later
Reasoning about the boolean logic can help validate patterns and debug when things don't match as expected.
Want more insights like this?
Related Articles
Rethinking Deception: Why We're Moving from Product to Enablement
After years of building deception technology and watching SOC teams struggle with yet another dashboard, we've made a fundamental shift in how we deliver cyber deception.
DeceptIQ: High-Fidelity Detection at Cloud Scale
Built by red teamers to catch adversaries. The deception technology platform we wish every organization we compromised had in place.
Early Warning Honey Tokens: Give Adversaries Options
This risk calculus has held for years. Early warning honey tokens (eWHT) exist to break it.