Fix: EventBus: match null value for anything-but rule
diff --git a/localstack/services/events/provider.py b/localstack/services/events/provider.py
index f34c1fe47c006..5cf2125b6b82c 100644
--- a/localstack/services/events/provider.py
+++ b/localstack/services/events/provider.py
@@ -516,9 +516,9 @@ def filter_event_based_on_event_format(
 ):
     def filter_event(event_pattern_filter: Dict[str, Any], event: Dict[str, Any]):
         for key, value in event_pattern_filter.items():
-            # match keys in the event in a case-agnostic way
-            event_value = event.get(key.lower(), event.get(key))
-            if event_value is None:
+            fallback = object()
+            event_value = event.get(key.lower(), event.get(key, fallback))
+            if event_value is fallback:
                 return False
 
             # 1. check if certain values in the event do not match the expected pattern
diff --git a/tests/aws/services/events/test_events.py b/tests/aws/services/events/test_events.py
index 7aa2c5ab458b8..65fba00aa15b5 100644
--- a/tests/aws/services/events/test_events.py
+++ b/tests/aws/services/events/test_events.py
@@ -393,17 +393,20 @@ def _put_events_with_filter_to_sqs(
             assert rs["FailedEntries"] == []
 
             try:
+                messages = []
                 for entry_asserts in entries_asserts:
                     entries = entry_asserts[0]
                     for entry in entries:
                         entry.setdefault("EventBusName", bus_name)
-                    self._put_entries_assert_results_sqs(
+                    message = self._put_entries_assert_results_sqs(
                         events_client,
                         sqs_client,
                         queue_url,
                         entries=entries,
                         should_match=entry_asserts[1],
                     )
+                    if message is not None:
+                        messages.extend(message)
             finally:
                 clean_up(
                     bus_name=bus_name,
@@ -412,6 +415,8 @@ def _put_events_with_filter_to_sqs(
                     queue_url=queue_url,
                 )
 
+            return messages
+
         yield _put_events_with_filter_to_sqs
 
     def _put_entries_assert_results_sqs(
@@ -433,10 +438,64 @@ def get_message(queue_url):
             actual_event = json.loads(messages[0]["Body"])
             if "detail" in actual_event:
                 self.assert_valid_event(actual_event)
+            return messages
         else:
             assert not messages
+            return None
+
+    @markers.aws.validated
+    def test_put_events_with_rule_anything_but_to_sqs(
+        self, put_events_with_filter_to_sqs, snapshot
+    ):
+        snapshot.add_transformer(
+            [
+                snapshot.transform.key_value("MD5OfBody"),
+                snapshot.transform.key_value("ReceiptHandle"),
+                snapshot.transform.jsonpath("$..EventBusName", "event-bus-name"),
+            ]
+        )
 
-        return messages
+        event_detail_match = {"command": "display-message", "payload": "baz"}
+        event_detail_null = {"command": None, "payload": "baz"}
+        event_detail_no_match = {"command": "no-message", "payload": "baz"}
+        test_event_pattern_anything_but = {
+            "source": ["core.update-account-command"],
+            "detail-type": ["core.update-account-command"],
+            "detail": {"command": [{"anything-but": ["no-message"]}]},
+        }
+        entries_match = [
+            {
+                "Source": test_event_pattern_anything_but["source"][0],
+                "DetailType": test_event_pattern_anything_but["detail-type"][0],
+                "Detail": json.dumps(event_detail_match),
+            }
+        ]
+        entries_match_null = [
+            {
+                "Source": test_event_pattern_anything_but["source"][0],
+                "DetailType": test_event_pattern_anything_but["detail-type"][0],
+                "Detail": json.dumps(event_detail_null),
+            }
+        ]
+        entries_no_match = [
+            {
+                "Source": test_event_pattern_anything_but["source"][0],
+                "DetailType": test_event_pattern_anything_but["detail-type"][0],
+                "Detail": json.dumps(event_detail_no_match),
+            }
+        ]
+
+        entries_asserts = [
+            (entries_match, True),
+            (entries_match_null, True),
+            (entries_no_match, False),
+        ]
+
+        messages = put_events_with_filter_to_sqs(
+            pattern=test_event_pattern_anything_but,
+            entries_asserts=entries_asserts,
+        )
+        snapshot.match("rule-anything-but", messages)
 
     # TODO: further unify/parameterize the tests for the different target types below
 
diff --git a/tests/aws/services/events/test_events.snapshot.json b/tests/aws/services/events/test_events.snapshot.json
index 8cd4dd04ee358..2ffed622b6800 100644
--- a/tests/aws/services/events/test_events.snapshot.json
+++ b/tests/aws/services/events/test_events.snapshot.json
@@ -247,5 +247,50 @@
         }
       }
     }
+  },
+  "tests/aws/services/events/test_events.py::TestEvents::test_put_events_with_rule_anything_but_to_sqs": {
+    "recorded-date": "21-12-2023, 13:00:42",
+    "recorded-content": {
+      "rule-anything-but": [
+        {
+          "MessageId": "<uuid:1>",
+          "ReceiptHandle": "<receipt-handle:1>",
+          "MD5OfBody": "<m-d5-of-body:1>",
+          "Body": {
+            "version": "0",
+            "id": "<uuid:2>",
+            "detail-type": "core.update-account-command",
+            "source": "core.update-account-command",
+            "account": "111111111111",
+            "time": "date",
+            "region": "<region>",
+            "resources": [],
+            "detail": {
+              "command": "display-message",
+              "payload": "baz"
+            }
+          }
+        },
+        {
+          "MessageId": "<uuid:3>",
+          "ReceiptHandle": "<receipt-handle:2>",
+          "MD5OfBody": "<m-d5-of-body:2>",
+          "Body": {
+            "version": "0",
+            "id": "<uuid:4>",
+            "detail-type": "core.update-account-command",
+            "source": "core.update-account-command",
+            "account": "111111111111",
+            "time": "date",
+            "region": "<region>",
+            "resources": [],
+            "detail": {
+              "command": null,
+              "payload": "baz"
+            }
+          }
+        }
+      ]
+    }
   }
 }

<!-- Please refer to the contribution guidelines before raising a PR: https://github.com/localstack/localstack/blob/master/CONTRIBUTING.md -->

<!-- Why am I raising this PR? Add context such as related issues, PRs, or documentation. -->
## Motivation
As reported in [#9711](https://github.com/localstack/localstack/issues/9711) the current behavior does not fully match the aws cli behavior regarding the EventBridge pattern `anything-but`  and a `null` value. 

This rule:
```
{
	"source": ["core.update-account-command"],
	"detail-type": ["core.update-account-command"],
	"detail": {"command": [{"anything-but": ["no-message"]}]},
}
``` 

In combination with this message:
```
{
	"version": "0",
	"id": "<uuid:2>",
	"detail-type": "core.update-account-command",
	"source": "core.update-account-command",
	"account": "111111111111",
	"time": "date",
	"region": "<region>",
	"resources": [],
	"detail": {
	    "command": null,
	    "payload": "baz"
	}
}
```

does not match on LocalStack, but does correctly match on AWS.

The issue arises from using the default fallback of the getter while matching event patterns, that also resolves to `None` and thus fields with `None` values are also excluded.

<!-- What notable changes does this PR make? -->
## Changes
Replace the default fallback while matching event patterns with a custom string.

Potential pitfalls: in the rare circumstance, that the custom string is also used as a value in a message, the pattern matching will not have the desired outcome.  


## Testing

To extend snapshot testing for event messages, the custom fixture that handles setting up the perquisites, posting the message, retrieving the message and comparing it was extended to return the message content.
Although comparing expected and actual massage values is still handled inside the fixture, additional snapshot matching is implemented to ensure AWS parity.  

<!-- The following sections are optional, but can be useful! 

## TODO

What's left to do:

- [ ] ...
- [ ] ...

-->

