diff --git a/functions/events/s3_object_acl_put_notification.json b/functions/events/s3_object_acl_put_notification.json new file mode 100644 index 00000000..1fd2ee39 --- /dev/null +++ b/functions/events/s3_object_acl_put_notification.json @@ -0,0 +1,36 @@ +{ + "Records": [ + { + "eventVersion": "2.3", + "eventSource": "aws:s3", + "awsRegion": "eu-west-1", + "eventTime": "2024-12-13T21:24:59.306Z", + "eventName": "ObjectAcl:Put", + "userIdentity": { + "principalId": "AWS:AXXXXXXXXXXXXXXXXXXXX:test" + }, + "requestParameters": { + "sourceIPAddress": "1.1.1.1" + }, + "responseElements": { + "x-amz-request-id": "XXXXXXXXXXXXXXXX", + "x-amz-id-2": "kJnocM4etuvc6BN1KT31BpAydhXF+3krucrYYGydPe44CasqrjWd4QM28pRvGM4Sg8T/IldaDqYrhD3TiOBDvIMSzwufbDO6" + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "test", + "bucket": { + "name": "test", + "ownerIdentity": { + "principalId": "AXXXXXXXXXXXXX" + }, + "arn": "arn:aws:s3:::test" + }, + "object": { + "key": "test.png", + "eTag": "24979f2d8bb004928be4e6d87888f857" + } + } + } + ] +} diff --git a/functions/events/s3_object_creation_notification.json b/functions/events/s3_object_creation_notification.json new file mode 100644 index 00000000..3a46313a --- /dev/null +++ b/functions/events/s3_object_creation_notification.json @@ -0,0 +1,39 @@ +{ + "Records": [ + { + "eventVersion": "2.1", + "eventSource": "aws:s3", + "awsRegion": "eu-west-1", + "eventTime": "2024-12-12T14:44:56.042Z", + "eventName": "ObjectCreated:CompleteMultipartUpload", + "userIdentity": { + "principalId": "AWS:AXXXXXXXXXXXXXXXXXXXX:test" + }, + "requestParameters": { + "sourceIPAddress": "1.1.1.1" + }, + "responseElements": { + "x-amz-request-id": "22HVMTY7K7BZEANW", + "x-amz-id-2": "7JNov8pldcSneqm8P52a3uaVaI5E+X3EPgnFcHUOta5iC2VHORDQlkOHa3pghQY9Px5p7RgphoJAu6EZFzKVWft3PHccuUhC" + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "backup", + "bucket": { + "name": "test", + "ownerIdentity": { + "principalId": "AXXXXXXXXXXXXX" + }, + "arn": "arn:aws:s3:::test" + }, + "object": { + "key": "test.png", + "size": 557056, + "eTag": "7d347f0d62d856c4dceef44b413b5bb2-1", + "versionId": "2EkRWHmQowiCXIVHSfD48cKVfS87iBZ4", + "sequencer": "00657871E7BEB1C11D" + } + } + } + ] +} diff --git a/functions/events/s3_object_delete_marker_notification.json b/functions/events/s3_object_delete_marker_notification.json new file mode 100644 index 00000000..a4602a6d --- /dev/null +++ b/functions/events/s3_object_delete_marker_notification.json @@ -0,0 +1,38 @@ +{ + "Records": [ + { + "eventVersion": "2.1", + "eventSource": "aws:s3", + "awsRegion": "eu-west-1", + "eventTime": "2024-12-12T15:43:47.889Z", + "eventName": "ObjectRemoved:DeleteMarkerCreated", + "userIdentity": { + "principalId": "AWS:AXXXXXXXXXXXXXXXXXXXX:test" + }, + "requestParameters": { + "sourceIPAddress": "1.1.1.1" + }, + "responseElements": { + "x-amz-request-id": "FSFTKP0F7F4GAYJ7", + "x-amz-id-2": "DLuIxXz8rgdn14lRySy0xePLTs+LL+3Azm4bpGudx7Bqax+t7bWUdKMUuJQf/lmXDaaUEnG+e67kEI4/kdI2LzAI/D+XXUnK" + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "backup", + "bucket": { + "name": "test", + "ownerIdentity": { + "principalId": "AXXXXXXXXXXXXX" + }, + "arn": "arn:aws:s3:::test" + }, + "object": { + "key": "test.png", + "eTag": "d41d8cd98f00b204e9800998ecf8427e", + "versionId": "4hWOOw8KrqaXtuiI9NpQzoFNDmnnlJlw", + "sequencer": "0065787FB3DCA9D828" + } + } + } + ] +} diff --git a/functions/events/s3_object_put_tag_notification.json b/functions/events/s3_object_put_tag_notification.json new file mode 100644 index 00000000..f3e79b32 --- /dev/null +++ b/functions/events/s3_object_put_tag_notification.json @@ -0,0 +1,37 @@ +{ + "Records": [ + { + "eventVersion": "2.3", + "eventSource": "aws:s3", + "awsRegion": "eu-west-1", + "eventTime": "2024-12-12T16:57:37.808Z", + "eventName": "ObjectTagging:Put", + "userIdentity": { + "principalId": "AWS:AXXXXXXXXXXXXXXXXXXXX:test" + }, + "requestParameters": { + "sourceIPAddress": "1.1.1.1" + }, + "responseElements": { + "x-amz-request-id": "CC3E1ZWK9W1X12HF", + "x-amz-id-2": "aXF6jUEXS9805Oa5ZTQgoXJCzUMt0ZCi/vRaB8EQK9O/b723Ct9A/oeAiF/U92GEnA5/cJ1ZUcyo0NSmOqVyaHq1jKz+rSF+" + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "backup", + "bucket": { + "name": "test", + "ownerIdentity": { + "principalId": "AXXXXXXXXXXXXX" + }, + "arn": "arn:aws:s3:::test" + }, + "object": { + "key": "test.png", + "eTag": "6dfa2bd9148dd380b5d56bd1366787f6", + "versionId": "wzGflBz2kJXzspA4UQ0Ik333dbvePdnP" + } + } + } + ] +} diff --git a/functions/events/s3_object_removal_notification.json b/functions/events/s3_object_removal_notification.json new file mode 100644 index 00000000..e60f90e0 --- /dev/null +++ b/functions/events/s3_object_removal_notification.json @@ -0,0 +1,37 @@ +{ + "Records": [ + { + "eventVersion": "2.1", + "eventSource": "aws:s3", + "awsRegion": "eu-west-1", + "eventTime": "2024-12-12T17:04:47.117Z", + "eventName": "ObjectRemoved:Delete", + "userIdentity": { + "principalId": "AWS:AXXXXXXXXXXXXXXXXXXXX:test" + }, + "requestParameters": { + "sourceIPAddress": "1.1.1.1" + }, + "responseElements": { + "x-amz-request-id": "E3MKJZCF0A5CKYQB", + "x-amz-id-2": "2qsHsa9aFfq+oOIl837VxPlE/Xq3Ii2hQ9p3WaCXF22cwSoH1gtg9K0u86hDy8BdorKhpiR0dA/9VSQ2TFoeSAkZ1vH6/kZO" + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "backup", + "bucket": { + "name": "test", + "ownerIdentity": { + "principalId": "AXXXXXXXXXXXXX" + }, + "arn": "arn:aws:s3:::test" + }, + "object": { + "key": "test.png", + "versionId": "sSHvWiZd1mM73EqLISh8ammc3JresZcC", + "sequencer": "00657892AF1C2D606B" + } + } + } + ] +} diff --git a/functions/events/s3_object_replication_failure.json b/functions/events/s3_object_replication_failure.json new file mode 100644 index 00000000..8d7d69e6 --- /dev/null +++ b/functions/events/s3_object_replication_failure.json @@ -0,0 +1,46 @@ +{ + "Records": [ + { + "eventVersion": "2.2", + "eventSource": "aws:s3", + "awsRegion": "eu-west-1", + "eventTime": "2024-12-12T17:04:56.129Z", + "eventName": "Replication:OperationFailedReplication", + "userIdentity": { + "principalId": "s3.amazonaws.com" + }, + "requestParameters": { + "sourceIPAddress": "s3.amazonaws.com" + }, + "responseElements": { + "x-amz-request-id": "a8f51c21-9f7d-4619-b54f-eb6577b67a82", + "x-amz-id-2": "4cWlLlh7RJD8eHJfb7EKfNnnSiDiZjvRBUMPl5rq5wsCSiRF+QePRr0nW9ku2wd2eOJc0TBxf27AxAbL9Irl0A==" + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "backup", + "bucket": { + "name": "test", + "ownerIdentity": { + "principalId": "111222333444" + }, + "arn": "arn:aws:s3:::test" + }, + "object": { + "key": "test.png", + "size": 1024, + "eTag": "d41d8cd98f00b204e9800998ecf8427e", + "versionId": "sSHvWiZd1mM73EqLISh8ammc3JresZcC", + "sequencer": "00657892A0790CB714" + } + }, + "replicationEventData": { + "replicationRuleId": "Replication", + "destinationBucket": "arn:aws:s3:::test-replica", + "s3Operation": "OBJECT_DELETE", + "requestTime": "2024-12-12T17:04:32.489Z", + "failureReason": "SrcObjectNotFound" + } + } + ] +} diff --git a/functions/messages/s3_object_notification.json b/functions/messages/s3_object_notification.json new file mode 100644 index 00000000..3a46313a --- /dev/null +++ b/functions/messages/s3_object_notification.json @@ -0,0 +1,39 @@ +{ + "Records": [ + { + "eventVersion": "2.1", + "eventSource": "aws:s3", + "awsRegion": "eu-west-1", + "eventTime": "2024-12-12T14:44:56.042Z", + "eventName": "ObjectCreated:CompleteMultipartUpload", + "userIdentity": { + "principalId": "AWS:AXXXXXXXXXXXXXXXXXXXX:test" + }, + "requestParameters": { + "sourceIPAddress": "1.1.1.1" + }, + "responseElements": { + "x-amz-request-id": "22HVMTY7K7BZEANW", + "x-amz-id-2": "7JNov8pldcSneqm8P52a3uaVaI5E+X3EPgnFcHUOta5iC2VHORDQlkOHa3pghQY9Px5p7RgphoJAu6EZFzKVWft3PHccuUhC" + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "backup", + "bucket": { + "name": "test", + "ownerIdentity": { + "principalId": "AXXXXXXXXXXXXX" + }, + "arn": "arn:aws:s3:::test" + }, + "object": { + "key": "test.png", + "size": 557056, + "eTag": "7d347f0d62d856c4dceef44b413b5bb2-1", + "versionId": "2EkRWHmQowiCXIVHSfD48cKVfS87iBZ4", + "sequencer": "00657871E7BEB1C11D" + } + } + } + ] +} diff --git a/functions/notify_slack.py b/functions/notify_slack.py index d7e7c864..9e90e4be 100644 --- a/functions/notify_slack.py +++ b/functions/notify_slack.py @@ -326,6 +326,105 @@ def format_aws_backup(message: str) -> Dict[str, Any]: return attachments +class S3ObjectNotificationCategory(Enum): + """Maps S3 Object notification Cateogry to Slack message format color + https://docs.aws.amazon.com/AmazonS3/latest/userguide/notification-content-structure.html + https://docs.aws.amazon.com/AmazonS3/latest/userguide/notification-how-to-event-types-and-destinations.html + """ + + TestEvent = "good" + ObjectCreated_Put = "good" + ObjectCreated_Post = "good" + ObjectCreated_Copy = "good" + ObjectCreated_CompleteMultipartUpload = "good" + ObjectRemoved_Delete = "danger" + ObjectRemoved_DeleteMarkerCreated = "danger" + ObjectRestore_Post = "good" + ObjectRestore_Completed = "good" + ObjectRestore_Delete = "danger" + ReducedRedundancyLostObject = "danger" + Replication_OperationFailedReplication = "danger" + Replication_OperationMissedThreshold = "danger" + Replication_OperationReplicatedAfterThreshold = "warning" + Replication_OperationNotTracked = "danger" + LifecycleExpiration_Delete = "danger" + LifecycleExpiration_DeleteMarkerCreated = "danger" + LifecycleTransition = "warning" + IntelligentTiering = "warning" + ObjectTagging_Put = "warning" + ObjectTagging_Delete = "warning" + ObjectAcl_Put = "warning" + +def format_s3_object_notification(message: Dict[str, Any]) -> Dict[str, Any]: + """Format S3 Object notification event into Slack message format + + :params message: message body containing S3 Object Notification event + :region: AWS region where the event originated from + :returns: formatted Slack message payload + """ + record = message["Records"][0] + event_name = record["eventName"] + event_time = record["eventTime"] + bucket_name = record["s3"]["bucket"]["name"] + region = record["awsRegion"] + object_key = record["s3"]["object"]["key"] + object_url = f"https://s3.console.aws.amazon.com/s3/object/{bucket_name}?region={region}&prefix={object_key}" + source_ip_address = record["requestParameters"]["sourceIPAddress"] + user_identity = record["userIdentity"]["principalId"].split(":")[-1] + + output = { + "color": S3ObjectNotificationCategory[record["eventName"].replace(":", "_")].value, + "fallback": f"Alarm {event_name} triggered", + "fields": [ + {"title": "Event Name", "value": f"`{event_name}`", "short": True}, + {"title": "Event Time", "value": f"`{event_time}`", "short": True}, + {"title": "Region", "value": f"`{region}`", "short": True}, + {"title": "Bucket Name", "value": f"`{bucket_name}`", "short": True}, + {"title": "Object Key", "value": f"`{object_key}`", "short": False}, + + {"title": "Object URL", "value": f"<{object_url}|Link>", "short": False}, + {"title": "Source IP Address", "value": f"`{source_ip_address}`", "short": True}, + {"title": "User Identity", "value": f"`{user_identity}`", "short": True}, + ], + "text": f"*New Amazon S3 Object Notification Event*", + } + + if "size" in record["s3"]["object"]: + object_size = record["s3"]["object"]["size"] + output["fields"].append({"title": "Object Size (Bytes)", "value": f"`{object_size}`", "short": False}) + + if "glacierEventData" in record: + glacier_restore_event_data = record["glacierEventData"]["restoreEventData"]["lifecycleRestorationExpiryTime"] + lifecycle_restoration_expiry_time = glacier_restore_event_data["lifecycleRestorationExpiryTime"] + lifecycle_restore_storage_class = glacier_restore_event_data["lifecycleRestoreStorageClass"] + output["fields"].append({"title": "Lifecycle Restoration Expiry Time", "value": f"`{lifecycle_restoration_expiry_time}`", "short": False}) + output["fields"].append({"title": "Lifecycle Restore Storage Class", "value": f"`{lifecycle_restore_storage_class}`", "short": False}) + + if "replicationEventData" in record: + replication_rule_name = record["replicationEventData"]["replicationRuleId"] + destination_bucket = record["replicationEventData"]["destinationBucket"].split(":")[-1] + request_time = record["replicationEventData"]["requestTime"] + operation = record["replicationEventData"]["s3Operation"] + failureReason = record["replicationEventData"]["failureReason"] + output["fields"].append({"title": "Replication Rule Name", "value": f"`{replication_rule_name}`", "short": True}) + output["fields"].append({"title": "Destination Bucket", "value": f"`{destination_bucket}`", "short": True}) + output["fields"].append({"title": "Request Time", "value": f"`{request_time}`", "short": False}) + output["fields"].append({"title": "Operation", "value": f"`{operation}`", "short": True}) + output["fields"].append({"title": "Failure Reason", "value": f"`{failureReason}`", "short": False}) + + if "intelligentTieringEventData" in record: + tiering_name = record["intelligentTieringEventData"]["tieringId"] + tiering_status = record["intelligentTieringEventData"]["tieringStatus"] + output["fields"].append({"title": "Tiering Name", "value": f"`{tiering_name}`", "short": True}) + output["fields"].append({"title": "Tiering Status", "value": f"`{tiering_status}`", "short": True}) + + if "lifecycleEventData" in record: + lifecycle_transition_days = record["lifecycleEventData"]["lifecycleTransitionAgeDays"] + lifecycle_transition_storage_class = record["lifecycleEventData"]["lifecycleTransitionStorageClass"] + output["fields"].append({"title": "Lifecycle Transition Age Days", "value": f"`{lifecycle_transition_days}`", "short": True}) + output["fields"].append({"title": "Lifecycle Transition Storage Class", "value": f"`{lifecycle_transition_storage_class}`", "short": True}) + + return output def format_default( message: Union[str, Dict], subject: Optional[str] = None @@ -409,6 +508,11 @@ def get_slack_message_payload( notification = format_aws_backup(message=str(message)) attachment = notification + + elif isinstance(message, Dict) and message.get("Records")[0].get("eventSource") == "aws:s3": + notification = format_s3_object_notification(message=message) + attachment = notification + elif "attachments" in message or "text" in message: payload = {**payload, **message} @@ -457,10 +561,15 @@ def lambda_handler(event: Dict[str, Any], context: Dict[str, Any]) -> str: logging.info(f"Event logging enabled: `{json.dumps(event)}`") for record in event["Records"]: - sns = record["Sns"] - subject = sns["Subject"] - message = sns["Message"] - region = sns["TopicArn"].split(":")[3] + try: + sns = record["Sns"] + subject = sns["Subject"] + message = sns["Message"] + region = sns["TopicArn"].split(":")[3] + except KeyError: + region = record["awsRegion"] + subject = "New Amazon S3 Object Event Notification" + message = record payload = get_slack_message_payload( message=message, region=region, subject=subject