diff --git a/events/s3.go b/events/s3.go index 15677997..a0f5d988 100644 --- a/events/s3.go +++ b/events/s3.go @@ -15,15 +15,20 @@ type S3Event struct { // S3EventRecord which wrap record data type S3EventRecord struct { - EventVersion string `json:"eventVersion"` - EventSource string `json:"eventSource"` - AWSRegion string `json:"awsRegion"` - EventTime time.Time `json:"eventTime"` - EventName string `json:"eventName"` - PrincipalID S3UserIdentity `json:"userIdentity"` - RequestParameters S3RequestParameters `json:"requestParameters"` - ResponseElements map[string]string `json:"responseElements"` - S3 S3Entity `json:"s3"` + EventVersion string `json:"eventVersion"` + EventSource string `json:"eventSource"` + AWSRegion string `json:"awsRegion"` + EventTime time.Time `json:"eventTime"` + EventName string `json:"eventName"` + PrincipalID S3UserIdentity `json:"userIdentity"` + RequestParameters S3RequestParameters `json:"requestParameters"` + ResponseElements map[string]string `json:"responseElements"` + S3 S3Entity `json:"s3"` + GlacierEventData *S3GlacierEventData `json:"glacierEventData,omitempty"` + RestoreEventData *S3RestoreEventData `json:"restoreEventData,omitempty"` + ReplicationEventData *S3ReplicationEventData `json:"replicationEventData,omitempty"` + IntelligentTieringEventData *S3IntelligentTieringEventData `json:"intelligentTieringEventData,omitempty"` + LifecycleEventData *S3LifecycleEventData `json:"lifecycleEventData,omitempty"` } type S3UserIdentity struct { @@ -70,6 +75,35 @@ func (o *S3Object) UnmarshalJSON(data []byte) error { return nil } +type S3GlacierEventData struct { + RestoreEventData *S3RestoreEventData `json:"restoreEventData"` +} + +type S3RestoreEventData struct { + LifecycleRestorationExpiryTime time.Time `json:"lifecycleRestorationExpiryTime"` + LifecycleRestoreStorageClass string `json:"lifecycleRestoreStorageClass"` +} + +type S3ReplicationEventData struct { + ReplicationRuleID string `json:"replicationRuleId"` + DestinationBucket string `json:"destinationBucket"` + S3Operation string `json:"s3Operation"` + RequestTime time.Time `json:"requestTime"` + FailureReason string `json:"failureReason"` +} + +type S3IntelligentTieringEventData struct { + DestinationAccessTier string `json:"destinationAccessTier"` +} + +type S3LifecycleEventData struct { + TransitionEventData *S3TransitionEventData `json:"transitionEventData"` +} + +type S3TransitionEventData struct { + DestinationStorageClass string `json:"destinationStorageClass"` +} + type S3TestEvent struct { Service string `json:"Service"` Bucket string `json:"Bucket"` diff --git a/events/s3_test.go b/events/s3_test.go index 968bfe6b..8ec48fbf 100644 --- a/events/s3_test.go +++ b/events/s3_test.go @@ -56,3 +56,148 @@ func TestS3TestEventMarshaling(t *testing.T) { func TestS3MarshalingMalformedJSON(t *testing.T) { test.TestMalformedJson(t, S3Event{}) } + +func TestS3GlacierEventMarshaling(t *testing.T) { + // 1. read JSON from file + inputJSON := test.ReadJSONFromFile(t, "./testdata/s3-glacier-event.json") + + // 2. de-serialize into Go object + var inputEvent S3Event + if err := json.Unmarshal(inputJSON, &inputEvent); err != nil { + t.Errorf("could not unmarshal event. details: %v", err) + } + + // 3. verify glacierEventData is correctly parsed + if inputEvent.Records[0].GlacierEventData == nil { + t.Error("glacierEventData should not be nil for glacier restore events") + } + + // 4. verify restoreEventData is correctly parsed + if inputEvent.Records[0].GlacierEventData.RestoreEventData == nil { + t.Error("restoreEventData should not be nil") + } + + // 5. serialize to JSON + outputJSON, err := json.Marshal(inputEvent) + if err != nil { + t.Errorf("could not marshal event. details: %v", err) + } + + // 6. check result + assert.JSONEq(t, string(inputJSON), string(outputJSON)) +} + +func TestS3RestoreEventMarshaling(t *testing.T) { + // 1. read JSON from file + inputJSON := test.ReadJSONFromFile(t, "./testdata/s3-restore-event.json") + + // 2. de-serialize into Go object + var inputEvent S3Event + if err := json.Unmarshal(inputJSON, &inputEvent); err != nil { + t.Errorf("could not unmarshal event. details: %v", err) + } + + // 3. verify restoreEventData is correctly parsed + if inputEvent.Records[0].RestoreEventData == nil { + t.Error("restoreEventData should not be nil") + } + + // 4. serialize to JSON + outputJSON, err := json.Marshal(inputEvent) + if err != nil { + t.Errorf("could not marshal event. details: %v", err) + } + + // 5. check result + assert.JSONEq(t, string(inputJSON), string(outputJSON)) +} + +func TestS3IntelligentTieringEventMarshaling(t *testing.T) { + // 1. read JSON from file + inputJSON := test.ReadJSONFromFile(t, "./testdata/s3-intelligenttier-event.json") + + // 2. de-serialize into Go object + var inputEvent S3Event + if err := json.Unmarshal(inputJSON, &inputEvent); err != nil { + t.Errorf("could not unmarshal event. details: %v", err) + } + + // 3. verify intelligentTieringEventData is correctly parsed + if inputEvent.Records[0].IntelligentTieringEventData == nil { + t.Error("intelligentTieringEventData should not be nil for intelligent tiering events") + } + + // 4. verify destinationAccessTier is correctly parsed + if inputEvent.Records[0].IntelligentTieringEventData.DestinationAccessTier == "" { + t.Error("destinationAccessTier should not be empty") + } + + // 5. serialize to JSON + outputJSON, err := json.Marshal(inputEvent) + if err != nil { + t.Errorf("could not marshal event. details: %v", err) + } + + // 6. check result + assert.JSONEq(t, string(inputJSON), string(outputJSON)) +} + +func TestS3LifecycleEventMarshaling(t *testing.T) { + // 1. read JSON from file + inputJSON := test.ReadJSONFromFile(t, "./testdata/s3-lifecycle-event.json") + + // 2. de-serialize into Go object + var inputEvent S3Event + if err := json.Unmarshal(inputJSON, &inputEvent); err != nil { + t.Errorf("could not unmarshal event. details: %v", err) + } + + // 3. verify lifecycleEventData is correctly parsed + if inputEvent.Records[0].LifecycleEventData == nil { + t.Error("lifecycleEventData should not be nil for lifecycle events") + } + + // 4. verify transitionEventData is correctly parsed + if inputEvent.Records[0].LifecycleEventData.TransitionEventData == nil { + t.Error("transitionEventData should not be nil") + } + + // 5. verify destinationStorageClass is correctly parsed + if inputEvent.Records[0].LifecycleEventData.TransitionEventData.DestinationStorageClass == "" { + t.Error("destinationStorageClass should not be empty") + } + + // 6. serialize to JSON + outputJSON, err := json.Marshal(inputEvent) + if err != nil { + t.Errorf("could not marshal event. details: %v", err) + } + + // 7. check result + assert.JSONEq(t, string(inputJSON), string(outputJSON)) +} + +func TestS3ReplicationEventMarshaling(t *testing.T) { + // 1. read JSON from file + inputJSON := test.ReadJSONFromFile(t, "./testdata/s3-replication-event.json") + + // 2. de-serialize into Go object + var inputEvent S3Event + if err := json.Unmarshal(inputJSON, &inputEvent); err != nil { + t.Errorf("could not unmarshal event. details: %v", err) + } + + // 3. verify replicationEventData is correctly parsed + if inputEvent.Records[0].ReplicationEventData == nil { + t.Error("replicationEventData should not be nil for replication events") + } + + // 4. serialize to JSON + outputJSON, err := json.Marshal(inputEvent) + if err != nil { + t.Errorf("could not marshal event. details: %v", err) + } + + // 5. check result + assert.JSONEq(t, string(inputJSON), string(outputJSON)) +} diff --git a/events/testdata/s3-glacier-event.json b/events/testdata/s3-glacier-event.json new file mode 100644 index 00000000..56eba8bf --- /dev/null +++ b/events/testdata/s3-glacier-event.json @@ -0,0 +1,46 @@ +{ + "Records": [ + { + "eventVersion": "2.1", + "eventSource": "aws:s3", + "awsRegion": "us-west-2", + "eventTime": "2023-05-10T15:00:00.123Z", + "eventName": "s3:ObjectRestore:Completed", + "userIdentity": { + "principalId": "EXAMPLE" + }, + "requestParameters": { + "sourceIPAddress": "127.0.0.1" + }, + "responseElements": { + "x-amz-request-id": "EXAMPLE123456789", + "x-amz-id-2": "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH" + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "testConfigRule", + "bucket": { + "name": "example-bucket", + "ownerIdentity": { + "principalId": "EXAMPLE" + }, + "arn": "arn:aws:s3:::example-bucket" + }, + "object": { + "key": "glacier%2Dtest.txt", + "urlDecodedKey": "glacier-test.txt", + "size": 1024, + "versionId": "abcdeH0Xp66ep__QDjR76LK7Gc9X4wKO", + "eTag": "0123456789abcdef0123456789abcdef", + "sequencer": "0A1B2C3D4E5F678901" + } + }, + "glacierEventData": { + "restoreEventData": { + "lifecycleRestorationExpiryTime": "2023-05-17T15:00:00.456Z", + "lifecycleRestoreStorageClass": "STANDARD" + } + } + } + ] +} diff --git a/events/testdata/s3-intelligenttier-event.json b/events/testdata/s3-intelligenttier-event.json new file mode 100644 index 00000000..1a403868 --- /dev/null +++ b/events/testdata/s3-intelligenttier-event.json @@ -0,0 +1,43 @@ +{ + "Records": [ + { + "eventVersion": "2.3", + "eventSource": "aws:s3", + "awsRegion": "us-east-1", + "eventTime": "2023-11-15T10:30:00.789Z", + "eventName": "s3:IntelligentTiering:TransitionAccess", + "userIdentity": { + "principalId": "s3.amazonaws.com" + }, + "requestParameters": { + "sourceIPAddress": "s3.amazonaws.com" + }, + "responseElements": { + "x-amz-request-id": "EXAMPLE123456789", + "x-amz-id-2": "EXAMPLEabcdefghijklmnopqrstuvwxyz" + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "IntelligentTieringConfig", + "bucket": { + "name": "example-bucket", + "ownerIdentity": { + "principalId": "EXAMPLE" + }, + "arn": "arn:aws:s3:::example-bucket" + }, + "object": { + "key": "intelligent%2Dtier%2Dtest.dat", + "urlDecodedKey": "intelligent-tier-test.dat", + "size": 5242880, + "versionId": "versionExample123", + "eTag": "d41d8cd98f00b204e9800998ecf8427e", + "sequencer": "0066ABC1234567890" + } + }, + "intelligentTieringEventData": { + "destinationAccessTier": "ARCHIVE_ACCESS" + } + } + ] +} diff --git a/events/testdata/s3-lifecycle-event.json b/events/testdata/s3-lifecycle-event.json new file mode 100644 index 00000000..712c798c --- /dev/null +++ b/events/testdata/s3-lifecycle-event.json @@ -0,0 +1,45 @@ +{ + "Records": [ + { + "eventVersion": "2.3", + "eventSource": "aws:s3", + "awsRegion": "us-west-2", + "eventTime": "2023-08-20T14:25:00.321Z", + "eventName": "s3:LifecycleTransition", + "userIdentity": { + "principalId": "s3.amazonaws.com" + }, + "requestParameters": { + "sourceIPAddress": "s3.amazonaws.com" + }, + "responseElements": { + "x-amz-request-id": "LIFECYCLE123456789", + "x-amz-id-2": "LIFECYCLEabcdefghijklmnopqrstuvwxyz" + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "LifecycleTransitionRule", + "bucket": { + "name": "example-bucket", + "ownerIdentity": { + "principalId": "EXAMPLE" + }, + "arn": "arn:aws:s3:::example-bucket" + }, + "object": { + "key": "lifecycle%2Dtest.dat", + "urlDecodedKey": "lifecycle-test.dat", + "size": 10485760, + "versionId": "lifecycleVersion123", + "eTag": "abcd1234ef5678901234567890abcdef", + "sequencer": "0066LIFECYCLE890" + } + }, + "lifecycleEventData": { + "transitionEventData": { + "destinationStorageClass": "GLACIER" + } + } + } + ] +} diff --git a/events/testdata/s3-replication-event.json b/events/testdata/s3-replication-event.json new file mode 100644 index 00000000..300d7474 --- /dev/null +++ b/events/testdata/s3-replication-event.json @@ -0,0 +1,47 @@ +{ + "Records": [ + { + "eventVersion": "2.2", + "eventSource": "aws:s3", + "awsRegion": "us-east-1", + "eventTime": "2024-09-05T21:04:32.527Z", + "eventName": "s3:Replication:OperationFailedReplication", + "userIdentity": { + "principalId": "s3.amazonaws.com" + }, + "requestParameters": { + "sourceIPAddress": "s3.amazonaws.com" + }, + "responseElements": { + "x-amz-request-id": "123bf045-2b4b-4ca8-a211-c34a63c59426", + "x-amz-id-2": "12VAWNDIHnwJsRhTccqQTeAPoXQmRt22KkewMV8G3XZihAuf9CLDdmkApgZzudaIe2KlLfDqGS0=" + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "ReplicationEventName", + "bucket": { + "name": "amzn-s3-demo-bucket1", + "ownerIdentity": { + "principalId": "111122223333" + }, + "arn": "arn:aws:s3:::amzn-s3-demo-bucket1" + }, + "object": { + "key": "replication%2Dtest.png", + "urlDecodedKey": "replication-test.png", + "size": 520080, + "versionId": "abcdeH0Xp66ep__QDjR76LK7Gc9X4wKO", + "eTag": "e12345ca7e88a38428305d3ff7fcb99f", + "sequencer": "0066DA1CBF104C0D51" + } + }, + "replicationEventData": { + "replicationRuleId": "notification-test-replication-rule", + "destinationBucket": "arn:aws:s3:::amzn-s3-demo-bucket2", + "s3Operation": "OBJECT_PUT", + "requestTime": "2024-09-05T21:03:59.168Z", + "failureReason": "AssumeRoleNotPermitted" + } + } + ] +} diff --git a/events/testdata/s3-restore-event.json b/events/testdata/s3-restore-event.json new file mode 100644 index 00000000..6b1ac24e --- /dev/null +++ b/events/testdata/s3-restore-event.json @@ -0,0 +1,44 @@ +{ + "Records": [ + { + "eventVersion": "2.1", + "eventSource": "aws:s3", + "awsRegion": "us-west-2", + "eventTime": "2023-05-10T15:00:00.123Z", + "eventName": "s3:ObjectRestore:Completed", + "userIdentity": { + "principalId": "EXAMPLE" + }, + "requestParameters": { + "sourceIPAddress": "127.0.0.1" + }, + "responseElements": { + "x-amz-request-id": "EXAMPLE123456789", + "x-amz-id-2": "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH" + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "testConfigRule", + "bucket": { + "name": "example-bucket", + "ownerIdentity": { + "principalId": "EXAMPLE" + }, + "arn": "arn:aws:s3:::example-bucket" + }, + "object": { + "key": "glacier%2Dtest.txt", + "urlDecodedKey": "glacier-test.txt", + "size": 1024, + "versionId": "abcdeH0Xp66ep__QDjR76LK7Gc9X4wKO", + "eTag": "0123456789abcdef0123456789abcdef", + "sequencer": "0A1B2C3D4E5F678901" + } + }, + "restoreEventData": { + "lifecycleRestorationExpiryTime": "2023-05-17T15:00:00.456Z", + "lifecycleRestoreStorageClass": "STANDARD" + } + } + ] +}