Back to Checks
aws

Bedrock AgentCore log groups have a CloudWatch Logs data protection policy activated

cloudwatch_log_group_agentcore_data_protection_policy_enabled
Service: cloudwatchSeverity: medium
OfficialBuilt-inVerified
Categories:gen-ailogging
Resource Type:Other

Log groups holding Bedrock AgentCore agent telemetry have an active CloudWatch Logs data protection policy, so sensitive data an agent writes to its own logs is masked at ingestion. A policy attached to the log group or inherited from an account-level policy both satisfy this.

Risk

Agents log prompts, tool arguments and retrieved context. Without masking, PII and secrets are stored in clear text and readable by every principal holding logs:GetLogEvents, widening the audience for regulated data far beyond the agent and defeating filtering applied only at the model boundary.

Run this check with Prowler

Run in Prowler Cloud
Run
prowler aws --checks cloudwatch_log_group_agentcore_data_protection_policy_enabled

Recommendation

Attach a data protection policy to every AgentCore log group, choosing the managed data identifiers your agents can plausibly log, or create one account-level policy so service-created log groups are covered as they appear.

Masking is defense in depth behind the model-boundary filters: keep restricting logs:Unmask so only incident responders can read the original values, and keep the log groups encrypted with a customer-managed key.

Remediation

CLI

aws logs put-data-protection-policy --log-group-identifier <LOG_GROUP_NAME> --policy-document '{"Name":"agentcore-data-protection-policy","Version":"2021-06-01","Statement":[{"Sid":"audit","DataIdentifier":["arn:aws:dataprotection::aws:data-identifier/EmailAddress"],"Operation":{"Audit":{"FindingsDestination":{}}}},{"Sid":"redact","DataIdentifier":["arn:aws:dataprotection::aws:data-identifier/EmailAddress"],"Operation":{"Deidentify":{"MaskConfig":{}}}}]}'

Terraform

# Attaches a data protection policy to an existing AgentCore log group
resource "aws_cloudwatch_log_data_protection_policy" "<example_resource_name>" {
  log_group_name = "<example_resource_name>"

  policy_document = jsonencode({
    Name    = "agentcore-data-protection-policy"
    Version = "2021-06-01"
    Statement = [
      {
        Sid            = "audit" # CRITICAL: required block, finds the sensitive terms
        DataIdentifier = ["arn:aws:dataprotection::aws:data-identifier/EmailAddress"]
        Operation = {
          Audit = {
            FindingsDestination = {}
          }
        }
      },
      {
        Sid            = "redact" # CRITICAL: the Deidentify block is what actually masks the data
        DataIdentifier = ["arn:aws:dataprotection::aws:data-identifier/EmailAddress"]
        Operation = {
          Deidentify = {
            MaskConfig = {}
          }
        }
      }
    ]
  })
}

NativeIaC

# CloudFormation: data protection policy on a log group you manage in the template.
# AgentCore runtime log groups are created by the service, not by CloudFormation --
# for those, use the CLI command or an AWS::Logs::AccountPolicy instead.
Resources:
  AgentCoreLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: "<example_resource_name>"
      DataProtectionPolicy:
        Name: agentcore-data-protection-policy
        Version: '2021-06-01'
        Statement:
          - Sid: audit  # CRITICAL: required block, finds the sensitive terms
            DataIdentifier:
              - arn:aws:dataprotection::aws:data-identifier/EmailAddress
            Operation:
              Audit:
                FindingsDestination: {}
          - Sid: redact  # CRITICAL: the Deidentify block is what actually masks the data
            DataIdentifier:
              - arn:aws:dataprotection::aws:data-identifier/EmailAddress
            Operation:
              Deidentify:
                MaskConfig: {}

Other

1In the AWS console, open CloudWatch > Logs > Log groups
2Select the AgentCore log group (/aws/bedrock-agentcore/... or /aws/vendedlogs/bedrock-agentcore/...)
3Choose Actions > Create data protection policy
4Under Managed data identifiers, select the data types your agent can log, for example Address, EmailAddress and PhoneNumber
5(Optional) Choose a destination for the audit findings report
6Choose Activate data protection
7To cover every log group at once, including service-created ones, open Settings > Data protection instead and create an account-level policy

Check Code

from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.cloudwatch.logs_client import logs_client

# Log group name prefixes AgentCore uses for agent telemetry. The runtime creates
# /aws/bedrock-agentcore/runtimes/... itself. Log delivery for memory, gateway and built-in tool
# resources is CONFIGURED by the operator rather than defaulted -- the AgentCore devguide's
# observability-configure page is a put_delivery_source / put_delivery_destination /
# create_delivery procedure -- and it targets /aws/vendedlogs/bedrock-agentcore/... because for
# same-account delivery to a /aws/vendedlogs/ log group the log-delivery service-linked role
# grants write access implicitly, while any other destination needs an explicit resource policy
# or the delivery silently fails. So the prefix is the convention that makes delivery work, which
# is why these two and not others.
DEFAULT_AGENTCORE_LOG_GROUP_PREFIXES = [
    "/aws/bedrock-agentcore/",
    "/aws/vendedlogs/bedrock-agentcore/",
]

ACTIVATED = "ACTIVATED"
ACCOUNT_DATA_PROTECTION = "ACCOUNT_DATA_PROTECTION"


class cloudwatch_log_group_agentcore_data_protection_policy_enabled(Check):
    """Ensure AgentCore log groups mask sensitive data with a data protection policy.

    Agents write prompts, tool arguments and retrieved context to their own log groups. A data
    protection policy masks matched data at ingestion, so without one the values are stored in
    clear text and readable by every principal holding logs:GetLogEvents.

    Scope: log groups whose name starts with an AgentCore prefix. AgentCore log delivery can be
    pointed at an arbitrarily named log group, so the prefix list is configurable through
    agentcore_log_group_name_prefixes; a configured list REPLACES the defaults rather than
    extending them, and an explicitly null value falls back to the defaults.

    PASS when the log group has an ACTIVATED policy of its own, or inherits the account-level one.
    FAIL when dataProtectionStatus is DELETED, ARCHIVED or DISABLED, or was never reported: all
    four mean nothing is being masked at ingestion today.
    MANUAL when the log group inventory could not be read, because nothing is then known about any
    log group's masking. Only a denied DescribeLogGroups leaves the inventory unknown -- every
    other collector failure leaves a readable, possibly partial, inventory.
    """

    def execute(self) -> list[Check_Report_AWS]:
        """Execute the AgentCore log group data protection policy check.

        Returns:
            A list of reports containing the result of the check: one per in-scope
            AgentCore log group, or a single account-level report when the log group
            inventory could not be read.
        """
        findings = []

        # An ABSENT or explicitly null value falls back to the defaults; an empty LIST does not.
        # `is None` covers both a missing key and a bare `agentcore_log_group_name_prefixes:` in the
        # YAML, which parses as None, and both mean "not configured". `or` additionally swallowed an
        # explicitly empty list, which IS a configured value and the one way an operator can say "no
        # log group is in scope" -- so the fallback overrode the operator and contradicted the
        # REPLACES-the-defaults behaviour promised above. An empty tuple is meaningful downstream
        # rather than degenerate: str.startswith(()) is False for every name, so nothing is selected,
        # which is exactly the request.
        configured_prefixes = logs_client.audit_config.get(
            "agentcore_log_group_name_prefixes"
        )
        prefixes = tuple(
            DEFAULT_AGENTCORE_LOG_GROUP_PREFIXES
            if configured_prefixes is None
            else configured_prefixes
        )

        # An unreadable log group inventory must not read as compliant: without the
        # inventory there is no way to tell an AgentCore log group that masks
        # sensitive data from one that does not.
        if logs_client.log_groups is None:
            report = Check_Report_AWS(metadata=self.metadata(), resource={})
            report.status = "MANUAL"
            report.status_extended = "Log groups could not be retrieved, so data protection policies for AgentCore log groups could not be verified."
            report.region = logs_client.region
            report.resource_id = logs_client.audited_account
            report.resource_arn = logs_client.log_group_arn_template
            report.resource_tags = []
            return [report]

        for log_group in logs_client.log_groups.values():
            if not log_group.name.startswith(prefixes):
                continue

            report = Check_Report_AWS(metadata=self.metadata(), resource=log_group)
            if log_group.data_protection_status == ACTIVATED:
                report.status = "PASS"
                report.status_extended = f"AgentCore log group {log_group.name} has a data protection policy activated."
            elif ACCOUNT_DATA_PROTECTION in log_group.inherited_properties:
                report.status = "PASS"
                report.status_extended = f"AgentCore log group {log_group.name} inherits the account-level data protection policy."
            else:
                report.status = "FAIL"
                report.status_extended = f"AgentCore log group {log_group.name} does not have an active data protection policy, so sensitive data written by the agent is not masked."
            findings.append(report)

        return findings