Skip to content
This repository was archived by the owner on Mar 23, 2026. It is now read-only.

Commit 76c6c86

Browse files
authored
Events: fix anything-but with null values (#13268)
1 parent 814a597 commit 76c6c86

29 files changed

Lines changed: 1920 additions & 271 deletions

localstack-core/localstack/services/events/event_rule_engine.py

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,21 @@ def _evaluate_nested_event_pattern_on_dict(self, event_pattern, payload: dict) -
5959
for flat_pattern in flat_pattern_conditions
6060
)
6161

62-
def _evaluate_condition(self, value, condition, field_exists: bool):
62+
def _evaluate_condition(self, value: t.Any, condition: t.Any, field_exists: bool) -> bool:
6363
if not isinstance(condition, dict):
6464
return field_exists and value == condition
65+
6566
elif (must_exist := condition.get("exists")) is not None:
6667
# if must_exists is True then field_exists must be True
6768
# if must_exists is False then fields_exists must be False
6869
return must_exist == field_exists
70+
6971
elif (anything_but := condition.get("anything-but")) is not None:
72+
if not field_exists:
73+
# anything-but can handle None `value`, but it needs to differentiate between user-set `null` and
74+
# missing value
75+
return False
76+
7077
if isinstance(anything_but, dict):
7178
if (not_condition := anything_but.get("prefix")) is not None:
7279
predicate = self._evaluate_prefix
@@ -95,6 +102,7 @@ def _evaluate_condition(self, value, condition, field_exists: bool):
95102
elif value is None:
96103
# the remaining conditions require the value to not be None
97104
return False
105+
98106
elif (prefix := condition.get("prefix")) is not None:
99107
if isinstance(prefix, dict):
100108
if (prefix_equal_ignore_case := prefix.get("equals-ignore-case")) is not None:
@@ -104,7 +112,7 @@ def _evaluate_condition(self, value, condition, field_exists: bool):
104112

105113
elif (suffix := condition.get("suffix")) is not None:
106114
if isinstance(suffix, dict):
107-
if suffix_equal_ignore_case := suffix.get("equals-ignore-case"):
115+
if (suffix_equal_ignore_case := suffix.get("equals-ignore-case")) is not None:
108116
return self._evaluate_suffix(suffix_equal_ignore_case.lower(), value.lower())
109117
else:
110118
return self._evaluate_suffix(suffix, value)
@@ -126,28 +134,30 @@ def _evaluate_condition(self, value, condition, field_exists: bool):
126134
return False
127135

128136
@staticmethod
129-
def _evaluate_prefix(condition: str | list, value: str) -> bool:
130-
return value.startswith(condition)
137+
def _evaluate_prefix(condition: str | list, value: t.Any) -> bool:
138+
return isinstance(value, str) and value.startswith(condition)
131139

132140
@staticmethod
133-
def _evaluate_suffix(condition: str | list, value: str) -> bool:
134-
return value.endswith(condition)
141+
def _evaluate_suffix(condition: str | list, value: t.Any) -> bool:
142+
return isinstance(value, str) and value.endswith(condition)
135143

136144
@staticmethod
137-
def _evaluate_equal_ignore_case(condition: str, value: str) -> bool:
138-
return condition.lower() == value.lower()
145+
def _evaluate_equal_ignore_case(condition: str, value: t.Any) -> bool:
146+
return isinstance(value, str) and condition.lower() == value.lower()
139147

140148
@staticmethod
141-
def _evaluate_cidr(condition: str, value: str) -> bool:
149+
def _evaluate_cidr(condition: str, value: t.Any) -> bool:
142150
try:
143151
ip = ipaddress.ip_address(value)
144152
return ip in ipaddress.ip_network(condition)
145153
except ValueError:
146154
return False
147155

148156
@staticmethod
149-
def _evaluate_wildcard(condition: str, value: str) -> bool:
150-
return bool(re.match(re.escape(condition).replace("\\*", ".+") + "$", value))
157+
def _evaluate_wildcard(condition: str, value: t.Any) -> bool:
158+
return isinstance(value, str) and bool(
159+
re.match(re.escape(condition).replace("\\*", ".+") + "$", value)
160+
)
151161

152162
@staticmethod
153163
def _evaluate_numeric_condition(conditions: list, value: t.Any) -> bool:
@@ -457,10 +467,18 @@ def _validate_rule(self, rule: t.Any, from_: str | None = None) -> None:
457467
return
458468

459469
elif operator == "anything-but":
460-
# anything-but can actually contain any kind of simple rule (str, number, and list)
470+
# anything-but can actually contain any kind of simple rule (str, number, and list) except Null
471+
if value is None:
472+
raise InvalidEventPatternException(
473+
f"{self.error_prefix}Value of anything-but must be an array or single string/number value."
474+
)
461475
if isinstance(value, list):
462476
for v in value:
463-
self._validate_rule(v)
477+
if v is None:
478+
raise InvalidEventPatternException(
479+
f"{self.error_prefix}Inside anything but list, start|null|boolean is not supported."
480+
)
481+
self._validate_rule(v, from_="anything-but")
464482

465483
return
466484

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but
2+
{
3+
"Event": {
4+
"id": "1",
5+
"source": "test-source",
6+
"detail-type": "test-detail-type",
7+
"account": "123456789012",
8+
"region": "us-east-2",
9+
"time": "2022-07-13T13:48:01Z",
10+
"detail": {}
11+
},
12+
"EventPattern": {
13+
"detail": {
14+
"state" : [{ "anything-but": { "equals-ignore-case": ["initializing", "stopped"] }}]
15+
}
16+
}
17+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but
2+
{
3+
"Event": {
4+
"id": "1",
5+
"source": "test-source",
6+
"detail-type": "test-detail-type",
7+
"account": "123456789012",
8+
"region": "us-east-2",
9+
"time": "2022-07-13T13:48:01Z",
10+
"detail": {
11+
"state": null
12+
}
13+
},
14+
"EventPattern": {
15+
"detail": {
16+
"state" : [{ "anything-but": { "equals-ignore-case": ["initializing", "stopped"] }}]
17+
}
18+
}
19+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but
2+
{
3+
"Event": {
4+
"id": "1",
5+
"source": "test-source",
6+
"detail-type": "test-detail-type",
7+
"account": "123456789012",
8+
"region": "us-east-2",
9+
"time": "2022-07-13T13:48:01Z",
10+
"detail": {}
11+
},
12+
"EventPattern": {
13+
"detail": {
14+
"state" : [{ "anything-but": { "equals-ignore-case": "initializing" }}]
15+
}
16+
}
17+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but
2+
{
3+
"Event": {
4+
"id": "1",
5+
"source": "test-source",
6+
"detail-type": "test-detail-type",
7+
"account": "123456789012",
8+
"region": "us-east-2",
9+
"time": "2022-07-13T13:48:01Z",
10+
"detail": {
11+
"state": null
12+
}
13+
},
14+
"EventPattern": {
15+
"detail": {
16+
"state" : [{ "anything-but": { "equals-ignore-case": "initializing" }}]
17+
}
18+
}
19+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but
2+
{
3+
"Event": {
4+
"id": "1",
5+
"source": "test-source",
6+
"detail-type": "test-detail-type",
7+
"account": "123456789012",
8+
"region": "us-east-2",
9+
"time": "2022-07-13T13:48:01Z",
10+
"detail": {}
11+
},
12+
"EventPattern": {
13+
"detail": {
14+
"state": [ { "anything-but": [ "stopped", "overloaded" ] } ]
15+
}
16+
}
17+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but
2+
{
3+
"Event": {
4+
"id": "1",
5+
"source": "test-source",
6+
"detail-type": "test-detail-type",
7+
"account": "123456789012",
8+
"region": "us-east-2",
9+
"time": "2022-07-13T13:48:01Z",
10+
"detail": {
11+
"state": null
12+
}
13+
},
14+
"EventPattern": {
15+
"detail": {
16+
"state": [ { "anything-but": [ "stopped", "running" ] } ]
17+
}
18+
}
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but
2+
{
3+
"Event": {
4+
"id": "1",
5+
"source": "test-source",
6+
"detail-type": "test-detail-type",
7+
"account": "123456789012",
8+
"region": "us-east-2",
9+
"time": "2022-07-13T13:48:01Z",
10+
"detail": {
11+
"state": "pending"
12+
}
13+
},
14+
"EventPattern": {
15+
"detail": {
16+
"state": [ { "anything-but": [ "stopped", null ] } ]
17+
}
18+
}
19+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but
2+
{
3+
"Event": {
4+
"id": "1",
5+
"source": "test-source",
6+
"detail-type": "test-detail-type",
7+
"account": "123456789012",
8+
"region": "us-east-2",
9+
"time": "2022-07-13T13:48:01Z",
10+
"detail": {}
11+
},
12+
"EventPattern": {
13+
"detail": {
14+
"state": [ { "anything-but": "initializing" } ]
15+
}
16+
}
17+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but
2+
{
3+
"Event": {
4+
"id": "1",
5+
"source": "test-source",
6+
"detail-type": "test-detail-type",
7+
"account": "123456789012",
8+
"region": "us-east-2",
9+
"time": "2022-07-13T13:48:01Z",
10+
"detail": {
11+
"state": null
12+
}
13+
},
14+
"EventPattern": {
15+
"detail": {
16+
"state": [ { "anything-but": null } ]
17+
}
18+
}
19+
}

0 commit comments

Comments
 (0)