JSON-Pattern Engine
A JSON-Object can be seen as a key-value store, able to describe complex models. The following diagram gives an overview of the most important JSON terms inspired by www.json.org.

ACAI JSON-Pattern Concept
The principle of the ACAI JSON-Pattern Engine is based on the comparison of a JSON-Source with a JSON-Pattern.

Basic Expressions
The syntax of the JSON-Pattern is in alignment with Amazon EventBridge > Create event patterns and the comparison operators.
Superset of EventBridge
The ACAI JSON-Pattern Engine is a superset of the EventBridge syntax. Every valid EventBridge pattern is also a valid ACAI pattern, but the ACAI engine supports additional operators (suffix, contains, contains-not, regex-match, regex-not-match, wildcard, equals-ignore-case, cidr-contains, cidr-contains-not) and the logical combinators not, and, or.
Patterns that use ACAI-only features cannot be translated 1:1 into an aws_cloudwatch_event_rule. Strict EventBridge validation is applied only when a pattern is actually provisioned as an EventBridge rule — in-process matching uses the full engine.
Engine behaviour at a glance
A few rules apply to every operator and combinator. Internalise these first — they are the most common source of confusion:
- Pattern keys are case-insensitive. Both source and pattern keys are lower-cased before matching (
ServiceNamematchesserviceName). Values stay case-sensitive unless you useequals-ignore-case,regex-*, orwildcard. - Sibling pattern keys are an implicit AND. Every key in a pattern dict must match for that level to be
True. - List-valued sources use ALL-semantics. If the source value at a key is a list (e.g. CloudTrail
Resources: [ {…}, {…} ]), the pattern is applied to every element — all must match. Useorinside an operator list when you want any-of semantics. - List-valued pattern values use OR-semantics.
"Region": ["us-east-1", "eu-central-1"]matches if the source region equals either. {}returnsFalse. The empty pattern is treated as "no rule = no match", not "match everything".not/and/orare operator-level combinators — they live inside an operator list ([ { "and": […] } ]) bound to a source key, not at the root of the pattern. To OR two completely independent rules, run two engine calls and OR the booleans in the caller.
| Comparison | Example | Rule syntax | Matching source example |
|---|---|---|---|
| Equals | Name is "Alice" | "Name": "Alice" or "Name": [ "Alice" ] |
"Name": "Alice" |
| And | Location is "New York" and Day is "Monday" | "Location": "New York", <br/>"Day": "Monday" |
"Location": "New York",<br/> "Day": "Monday" |
| Or | PaymentType is "Credit" | "PaymentType": [ "Credit", "Debit"] |
"PaymentType": "Credit" |
| Empty | LastName is empty | "LastName": [""] |
"LastName": "" |
| "Nesting" | Customer.Name is "Alice" | "Customer": JSON-Object |
"Customer": { "Name": "Alice" } |
| Mix | Location is "New York" and Day is "Monday" or "Tuesday" | "Location": "New York", <br/> "Day": ["Monday", "Thuesday"] |
"Location": "New York", "Day": "Monday" or "Location": "New York", "Day": "Tuesday" |
Logical Expressions
Additional Logical Expressions are supported. In this case, the JSON-Value is a JSON-Array of JSON-Objects:
Note
For the ACAI JSON-Engine the keys of the Pattern-JSON are case-insensitive to the Source-JSON.
| Comparison | Comparator | Example | Logical Expression Syntax | Matching Source-JSON |
|---|---|---|---|---|
| Begins with | prefix | Region is in the US | "Region": [ {"prefix": "us-" } ] |
"Region": "us-east-1" |
| Contains | contains | ServiceName contains 'database' | "ServiceName": [<br/> {"contains": "database" }<br/>] |
"serviceName": "employee-database-dev" |
| Does not contain | contains-not | ServiceName does not contain 'database' | "ServiceName": [<br/> {"contains-not": "database" }<br/>] |
"serviceName": "employee-microservice-dev" |
| Ends with | suffix | Service name ends with "-dev" | "serviceName": [ {"suffix": "-dev" } ] |
"ServiceName": "employee-database-dev" |
| Not | anything-but | Weather is anything but "Raining" | "Weather": [<br/> { "anything-but": "Raining" }<br/>] |
"Weather": "Sunny" or "Weather": "Cloudy" |
| Not (nested) | anything-but | Region is anything but those starting with "us-" | "Region": [<br/> { "anything-but": { "prefix": "us-" } }<br/>] |
"Region": "eu-central-1" |
| Exists | exists | ProductName exists | "ProductName": [ { "exists": true } ] |
"ProductName": "SEMPER" |
| Does not exist | exists | ProductName does not exist | "ProductName": [ { "exists": false } ] |
n/a |
| Numeric | numeric | TCP-Port 22 affected | "FromPort": [ { "numeric": [ "<=", 22 ] } ],"ToPort": [ { "numeric": [ ">=", 22 ] } ], |
"FromPort": 22, "ToPort": 22 |
| REGEX match | regex-match | ServiceName matches regex pattern "^prefix-\w+-prod$" | "ServiceName": [<br/> { "regex-match": "^prefix-\w+-prod$" }<br/>] |
"ServiceName": "prefix-database-prod" |
| REGEX not match | regex-not-match | ServiceName does not match regex pattern "^prefix-\w+-prod$" | "ServiceName": [<br/> { "regex-not-match": "^prefix-\w+-prod$" }<br/>] |
"ServiceName": "prefix-database-int" |
| Wildcard | wildcard | ServiceName matches glob "prefix-*-prod" | "ServiceName": [<br/> { "wildcard": "prefix-*-prod" }<br/>] |
"ServiceName": "prefix-database-prod" |
| Equals (case-insensitive) | equals-ignore-case | Environment equals "PROD" regardless of case | "Environment": [<br/> { "equals-ignore-case": "prod" }<br/>] |
"Environment": "Prod" |
| IP in CIDR | cidr-contains | SourceIp is inside 10.0.0.0/8 | "SourceIp": [<br/> { "cidr-contains": "10.0.0.0/8" }<br/>] |
"SourceIp": "10.1.2.3" |
| IP not in CIDR | cidr-contains-not | SourceIp is outside 10.0.0.0/8 | "SourceIp": [<br/> { "cidr-contains-not": "10.0.0.0/8" }<br/>] |
"SourceIp": "192.168.1.1" |
Logical Combinators (ACAI Extension)
In addition to the operators above, the engine supports the logical combinators not, and, or. These compose any other operator(s) arbitrarily deep and are not part of the EventBridge syntax — use them freely for in-process matching, but avoid them in patterns that must be translated into an EventBridge rule.
| Combinator | Logical Expression Syntax | Meaning |
|---|---|---|
| not | "ServiceName": [<br/> { "not": { "prefix": "test-" } }<br/>] |
Negates the nested operator dict |
| and | "ServiceName": [<br/> { "and": [<br/> { "prefix": "prod-" },<br/> { "suffix": "-db" }<br/> ] }<br/>] |
All nested operator dicts must match |
| or | "ServiceName": [<br/> { "or": [<br/> { "contains": "core" },<br/> { "contains": "platform" }<br/> ] }<br/>] |
At least one of the nested operator dicts must match |
Pattern-JSON Examples
AND Expression
Result
Pattern-JSON will match to all Source-JSON where accountTags.confidentiality_level == "Internal" AND ouName == "Prod".
OR Expression
Result
Pattern-JSON will match to all Source-JSON where accountTags.environment == "prod" OR accountTags.environment == "nonprod".
Logical Expression - Comparator "contains"
Result
Pattern-JSON will match to all Source-JSON where account_context.accountName will contain "_core-".
Logical Expression - Comparator "exists"
Result
Pattern-JSON will match to all Source-JSON where the JSON-Key accountTags.confidantiality_level is available.
Numerical Expression - Comparator "numeric"
{
"SecurityGroupRule": {
"FromPort": [ { "numeric": [ "<=", 22 ] } ],
"ToPort": [ { "numeric": [ ">=", 22 ] } ],
"IpProtocol": "tcp"
}
}
Result
Pattern-JSON will match to all Source-JSON where the SecurityGroupRule.FromPort is <= 22 AND SecurityGroupRule.ToPort is >= 22 AND SecurityGroupRule.IpProtocol is "tcp".
Numeric range (four-element form)
Result
Matches every Source-JSON where detail.severity is in the half-open interval [4.0, 7.0) — GuardDuty Medium severity findings.
Wildcard — multiple stars
Result
Matches every Source-JSON whose name starts with core-, contains -db-, and has any suffix — e.g. "name": "core-customer-db-prod". * matches any run of characters (including the empty string); the engine has no escape syntax for a literal asterisk.
Comparator "regex-*"
Result
Pattern-JSON will match to the Source-JSON as the "eventType": "AwsApiCall" matches the pattern.
For regex expressions we recommend: www.autoregex.xyz
Logical Combinator - not / and / or
{
"serviceName": [
{
"and": [
{ "prefix": "core-" },
{ "not": { "suffix": "-test" } },
{ "or": [
{ "contains": "db" },
{ "contains": "cache" }
] }
]
}
]
}
Result
Pattern-JSON will match all Source-JSON where serviceName starts with core-, does not end with -test, and contains either db or cache (e.g. "serviceName": "core-customer-db").
Comparator "anything-but" with nested operator
Result
Pattern-JSON matches every Source-JSON whose region does not start with us- (e.g. "region": "eu-central-1").
Comparator "anything-but" with a nested list
Result
Excludes events sourced from KMS or CloudWatch Logs. The list form is the EventBridge classic; the nested-operator form (previous example) is the ACAI superset.
Operator "exists"
Result
Matches every Source-JSON where the key detail.userIdentity.sessionContext is present (regardless of its value). Use "exists": false to match the absence of a key.
Implicit AND across sibling keys
{
"detail": {
"eventSource": [ "iam.amazonaws.com" ],
"eventName": [ { "prefix": "Create" } ],
"userIdentity": {
"type": [ "IAMUser", "AssumedRole" ]
}
}
}
Result
All sibling keys at the same nesting level are AND-combined. Matches CloudTrail records issued by an IAM user or assumed role that invoke any iam:Create* API.
List source value — ALL elements must match
Result
When the Source-JSON value is itself a list (e.g. CloudTrail resources: [...]), every element must satisfy the pattern. The example matches only when the record references S3 buckets exclusively.
Real-world: CloudTrail root-user activity
{
"detail": {
"userIdentity": {
"type": [ "Root" ]
},
"eventName": [
{ "anything-but": [ "ConsoleLogin", "GetSessionToken" ] }
]
}
}
Result
Flags any root-account API call that is not a console login or token refresh — a classic high-severity SecOps signal.
Real-world: Security Hub CIS finding for IAM users
{
"detail": {
"findings": {
"ProductFields": {
"RuleId": [ { "prefix": "1." } ]
},
"Resources": {
"Type": [ "AwsIamUser" ]
},
"Severity": {
"Normalized": [ { "numeric": [ ">=", 40 ] } ]
}
}
}
}
Result
Matches Security Hub findings tagged with a CIS section-1 rule, scoped to IAM user resources, at MEDIUM severity or higher.
Real-world: GuardDuty high-severity range
{
"source": [ "aws.guardduty" ],
"detail": {
"severity": [ { "numeric": [ ">=", 7.0, "<=", 8.9 ] } ]
}
}
Result
The 4-element numeric form expresses a closed range; this matches GuardDuty findings of severity High (7.0 – 8.9 inclusive).
Real-world: IPv4 / IPv6 CIDR membership
{
"detail": {
"sourceIPAddress": [
{ "cidr": "10.0.0.0/8" },
{ "cidr": "192.168.0.0/16" },
{ "cidr": "fd00::/8" }
]
}
}
Result
Matches any source IP — IPv4 or IPv6 — that falls inside one of the listed RFC 1918 / ULA ranges. Multiple operator dicts inside the same list are OR-combined.
Account-context targeting (SEMPER style)
{
"accountTags": {
"confidentiality_level": [ "Confidential", "Strictly Confidential" ]
},
"ouNameWithPath": [ { "prefix": "Root/Workloads/Prod/" } ],
"accountName": [ { "anything-but": { "suffix": "-sandbox" } } ]
}
Result
SEMPER evaluates this Pattern-JSON against each AWS account's metadata. The account matches when it sits under the production OU, carries a sensitive data classification, and is not named like a sandbox account.
Combinator not wrapping a complex or
{
"principalArn": [
{
"not": {
"or": [
{ "wildcard": "arn:aws:iam::*:role/AWSReservedSSO_*" },
{ "wildcard": "arn:aws:sts::*:assumed-role/AWSReservedSSO_*/*" },
{ "prefix": "arn:aws:iam::123456789012:role/break-glass-" }
]
}
}
]
}
Result
Matches every caller principal that is neither an IAM Identity Center session nor an approved break-glass role — useful for surfacing unexpected long-lived credentials.