Back to Checks
aws

CloudTrail trail records all S3 object-level API operations for all buckets

cloudtrail_s3_dataevents_write_enabled
Service: cloudtrailSeverity: low
OfficialBuilt-inVerified
Categories:loggingforensics-ready
Resource Type:AwsCloudTrailTrail

CloudTrail trails include S3 object-level data events for write (or all) operations across all current and future buckets, via classic or advanced selectors. This records actions like PutObject, DeleteObject, and multipart uploads at the object level.

Risk

Without object-level write logging, unauthorized or accidental changes and deletions can go unobserved, undermining data integrity and availability. Forensics lose visibility into who modified or removed objects, hindering detection of ransomware, rogue automation, or insider tampering.

Run this check with Prowler

Run in Prowler Cloud
Run
prowler aws --checks cloudtrail_s3_dataevents_write_enabled

Recommendation

Enable CloudTrail S3 data events for object-level write (and optionally read) across all buckets on a multi-Region trail. Apply least privilege to log storage, set lifecycle retention, and integrate alerts. Use advanced selectors to target sensitive buckets/operations for cost control and defense in depth.

Remediation

CLI

aws cloudtrail put-event-selectors --trail-name <example_resource_name> --event-selectors '[{"ReadWriteType":"WriteOnly","DataResources":[{"Type":"AWS::S3::Object","Values":["arn:aws:s3"]}]}]'

Terraform

resource "aws_cloudtrail" "<example_resource_name>" {
  name           = "<example_resource_name>"
  s3_bucket_name = "<example_resource_name>"

  event_selector {
    read_write_type = "WriteOnly"
    data_resource {
      type   = "AWS::S3::Object"
      values = ["arn:aws:s3"]  # Critical: logs S3 object-level write events for all buckets to pass the check
    }
  }
}

NativeIaC

Resources:
  <example_resource_name>:
    Type: AWS::CloudTrail::Trail
    Properties:
      TrailName: <example_resource_name>
      S3BucketName: <example_resource_name>
      EventSelectors:
        - ReadWriteType: WriteOnly
          DataResources:
            - Type: AWS::S3::Object
              Values:
                - arn:aws:s3  # Critical: enables S3 object-level write data events for all buckets, fixing the check

Other

1In the AWS Console, open CloudTrail and go to Trails
2Select <your trail> and click Edit under Data events
3For Data event source, choose S3
4Select All current and future S3 buckets
5Check Write events (or All events)
6Click Save changes

Check Code

from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.cloudtrail.cloudtrail_client import (
    cloudtrail_client,
)
from prowler.providers.aws.services.s3.s3_client import s3_client


class cloudtrail_s3_dataevents_write_enabled(Check):
    def execute(self):
        findings = []
        if cloudtrail_client.trails is not None:
            for trail in cloudtrail_client.trails.values():
                for data_event in trail.data_events:
                    # Classic event selectors
                    if not data_event.is_advanced:
                        # Check if trail has a data event for all S3 Buckets for write
                        if (
                            data_event.event_selector["ReadWriteType"] == "All"
                            or data_event.event_selector["ReadWriteType"] == "WriteOnly"
                        ):
                            for resource in data_event.event_selector["DataResources"]:
                                if "AWS::S3::Object" == resource["Type"] and (
                                    f"arn:{cloudtrail_client.audited_partition}:s3"
                                    in resource["Values"]
                                    or f"arn:{cloudtrail_client.audited_partition}:s3:::"
                                    in resource["Values"]
                                    or f"arn:{cloudtrail_client.audited_partition}:s3:::*/*"
                                    in resource["Values"]
                                ):
                                    report = Check_Report_AWS(
                                        metadata=self.metadata(),
                                        resource=trail,
                                    )
                                    report.region = trail.home_region
                                    report.status = "PASS"
                                    report.status_extended = f"Trail {trail.name} from home region {trail.home_region} has a classic data event selector to record all S3 object-level API operations."
                                    findings.append(report)
                    # Advanced event selectors
                    elif data_event.is_advanced:
                        for field_selector in data_event.event_selector[
                            "FieldSelectors"
                        ]:
                            if (
                                field_selector["Field"] == "resources.type"
                                and field_selector["Equals"][0] == "AWS::S3::Object"
                            ):
                                report = Check_Report_AWS(
                                    metadata=self.metadata(), resource=trail
                                )
                                report.region = trail.home_region
                                report.status = "PASS"
                                report.status_extended = f"Trail {trail.name} from home region {trail.home_region} has an advanced data event selector to record all S3 object-level API operations."
                                findings.append(report)
            if not findings and (
                s3_client.buckets or cloudtrail_client.provider.scan_unused_services
            ):
                report = Check_Report_AWS(
                    metadata=self.metadata(), resource=cloudtrail_client.trails
                )
                report.region = cloudtrail_client.region
                report.resource_arn = cloudtrail_client.trail_arn_template
                report.resource_id = cloudtrail_client.audited_account
                report.status = "FAIL"
                report.status_extended = "No CloudTrail trails have a data event to record all S3 object-level API operations."
                findings.append(report)
        return findings