Skip to content

Conversation

ykethan
Copy link
Contributor

@ykethan ykethan commented Jun 5, 2025

Issue # (if applicable)

Closes #34136.

Reason for this change

AWS SNS now supports Message Data Protection policies, which allow users to define rules to detect and handle sensitive data in SNS messages. This feature enables users to implement security controls like auditing, masking, redacting, or blocking messages containing sensitive data.

Description of changes

This PR adds support for SNS Data Protection policies to the AWS CDK, including:

Added dataProtectionPolicy property to TopicProps

Adds Core Data Protection:

  • DataProtectionPolicy - The main construct for defining an SNS data protection policy
  • DataProtectionPolicyStatement - Represents a rule within the policy
  • DataDirection - Enum to specify inbound or outbound message flow
  • Operations classes (AuditOperation, MaskOperation, RedactOperation, DenyOperation)
  • Custom data identifier support via CustomDataIdentifier

Adds Managed Data Identifier collections:

  • PersonalIdentifiers - For detecting PII (names, addresses, SSNs, etc.)
  • FinancialIdentifiers - For detecting financial information (credit cards, bank accounts)
  • CredentialsIdentifiers - For detecting sensitive credentials (AWS keys, private keys)
  • DeviceIdentifiers - For detecting device-related information (IP addresses)
  • HealthIdentifiers - For detecting health-related information (PHI)

Updated README with examples

Describe any new or updated permissions being added

Description of how you validated changes

Created unit tests for all new components
Created integration test
Manually tested deployment using

    // Create a CloudWatch log group for SNS data protection findings
    const logGroup = new logs.LogGroup(this, 'DataProtectionLogGroup', {
      logGroupName: '/aws/vendedlogs/sns-data-protection',
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // Create 10 custom data identifiers (maximum allowed per policy)
    const employeeIdPattern = new sns.CustomDataIdentifier({
      name: 'EmployeeID',
      regex: 'EMP-[0-9]{6}',
    });

    const projectCodePattern = new sns.CustomDataIdentifier({
      name: 'ProjectCode',
      regex: 'PRJ-[A-Z]{3}-[0-9]{4}',
    });

    const customerIdPattern = new sns.CustomDataIdentifier({
      name: 'CustomerID',
      regex: 'CUST-[0-9]{8}',
    });

    const orderIdPattern = new sns.CustomDataIdentifier({
      name: 'OrderID',
      regex: 'ORD-[0-9]{10}',
    });

    const caseIdPattern = new sns.CustomDataIdentifier({
      name: 'CaseID',
      regex: 'CASE-[0-9]{7}',
    });

    const accountIdPattern = new sns.CustomDataIdentifier({
      name: 'AccountID',
      regex: 'ACC-[0-9]{8}',
    });

    const productCodePattern = new sns.CustomDataIdentifier({
      name: 'ProductCode',
      regex: 'PROD-[A-Z]{2}-[0-9]{4}',
    });

    const contractIdPattern = new sns.CustomDataIdentifier({
      name: 'ContractID',
      regex: 'CNTR-[0-9]{6}-[A-Z]{2}',
    });

    const invoiceIdPattern = new sns.CustomDataIdentifier({
      name: 'InvoiceID',
      regex: 'INV-[0-9]{8}',
    });

    const ticketIdPattern = new sns.CustomDataIdentifier({
      name: 'TicketID',
      regex: 'TKT-[A-Z]{3}-[0-9]{6}',
    });

    // Create a comprehensive data protection policy
    const dataProtectionPolicy = new sns.DataProtectionPolicy({
      name: 'ComprehensiveProtectionPolicy',
      description:
        'Complete example of all SNS Message Data Protection capabilities',
      statements: [
        // Statement 1: Audit for all PII data types
        new sns.DataProtectionPolicyStatement({
          sid: 'AuditAllPII',
          dataDirection: sns.DataDirection.INBOUND,
          dataIdentifiers: [
            // Basic personal identifiers
            sns.PersonalIdentifiers.NAME,
            sns.PersonalIdentifiers.EMAIL_ADDRESS,
            sns.PersonalIdentifiers.VEHICLE_IDENTIFICATION_NUMBER,
            sns.PersonalIdentifiers.ADDRESS,

            // Drivers license across multiple regions
            sns.PersonalIdentifiers.driversLicense('US'),
            sns.PersonalIdentifiers.driversLicense('GB'),
            sns.PersonalIdentifiers.driversLicense('CA'),
            sns.PersonalIdentifiers.driversLicense('AU'),
            sns.PersonalIdentifiers.driversLicense('FR'),
            sns.PersonalIdentifiers.driversLicense('DE'),
            sns.PersonalIdentifiers.driversLicense('IT'),
            sns.PersonalIdentifiers.driversLicense('ES'),

            // Phone numbers across multiple regions
            sns.PersonalIdentifiers.phoneNumber('US'),
            sns.PersonalIdentifiers.phoneNumber('GB'),
            sns.PersonalIdentifiers.phoneNumber('FR'),
            sns.PersonalIdentifiers.phoneNumber('DE'),
            sns.PersonalIdentifiers.phoneNumber('IT'),
            sns.PersonalIdentifiers.phoneNumber('ES'),
            sns.PersonalIdentifiers.phoneNumber('BR'),

            // Passport numbers across multiple regions
            sns.PersonalIdentifiers.passportNumber('US'),
            sns.PersonalIdentifiers.passportNumber('GB'),
            sns.PersonalIdentifiers.passportNumber('CA'),
            sns.PersonalIdentifiers.passportNumber('DE'),
            sns.PersonalIdentifiers.passportNumber('FR'),
            sns.PersonalIdentifiers.passportNumber('IT'),
            sns.PersonalIdentifiers.passportNumber('ES'),

            // SSNs across regions
            sns.PersonalIdentifiers.ssn('US'),
            sns.PersonalIdentifiers.ssn('ES'),
          ],
          operation: new sns.AuditOperation({
            sampleRate: 99,
            findingsDestination: {
              cloudWatchLogs: {
                logGroup: logGroup.logGroupName,
              },
            },
          }),
        }),

        // Statement 2: Mask financial information
        new sns.DataProtectionPolicyStatement({
          sid: 'MaskFinancialData',
          dataDirection: sns.DataDirection.INBOUND,
          dataIdentifiers: [
            // Credit card related
            sns.FinancialIdentifiers.CREDIT_CARD_NUMBER,
            sns.FinancialIdentifiers.CREDIT_CARD_EXPIRATION,
            sns.FinancialIdentifiers.CREDIT_CARD_CVV,

            // Bank accounts across multiple regions
            sns.FinancialIdentifiers.bankAccountNumber('US'),
            sns.FinancialIdentifiers.bankAccountNumber('GB'),
            sns.FinancialIdentifiers.bankAccountNumber('DE'),
            sns.FinancialIdentifiers.bankAccountNumber('FR'),
            sns.FinancialIdentifiers.bankAccountNumber('IT'),
            sns.FinancialIdentifiers.bankAccountNumber('ES'),
          ],
          operation: new sns.MaskOperation({
            maskWithCharacter: '#',
          }),
        }),

        // Statement 3: Redact custom organization-specific patterns
        new sns.DataProtectionPolicyStatement({
          sid: 'RedactCustomIdentifiers',
          dataDirection: sns.DataDirection.INBOUND,
          dataIdentifiers: [
            employeeIdPattern,
            projectCodePattern,
            customerIdPattern,
            orderIdPattern,
            caseIdPattern,
          ],
          operation: new sns.RedactOperation(),
        }),

        // Statement 4: Additional redact statement for remaining custom identifiers
        new sns.DataProtectionPolicyStatement({
          sid: 'RedactMoreCustomIdentifiers',
          dataDirection: sns.DataDirection.INBOUND,
          dataIdentifiers: [
            accountIdPattern,
            productCodePattern,
            contractIdPattern,
            invoiceIdPattern,
            ticketIdPattern,
          ],
          operation: new sns.RedactOperation(),
        }),

        // Statement 5: Block messages containing credentials
        new sns.DataProtectionPolicyStatement({
          sid: 'BlockCredentials',
          dataDirection: sns.DataDirection.INBOUND,
          dataIdentifiers: [
            // All credential types
            sns.CredentialsIdentifiers.AWS_SECRET_KEY,
            sns.CredentialsIdentifiers.OPENSSH_PRIVATE_KEY,
            sns.CredentialsIdentifiers.PGP_PRIVATE_KEY,
            sns.CredentialsIdentifiers.PRIVATE_KEY,
            sns.CredentialsIdentifiers.PUTTY_PRIVATE_KEY,
          ],
          operation: new sns.DenyOperation(),
        }),

        // Statement 6: Block messages containing device information
        new sns.DataProtectionPolicyStatement({
          sid: 'BlockDeviceInfo',
          dataDirection: sns.DataDirection.INBOUND,
          dataIdentifiers: [sns.DeviceIdentifiers.IP_ADDRESS],
          operation: new sns.DenyOperation(),
        }),

        // Statement 7: Mask health information in outbound messages
        new sns.DataProtectionPolicyStatement({
          sid: 'MaskHealthData',
          dataDirection: sns.DataDirection.OUTBOUND,
          dataIdentifiers: [
            // US health identifiers
            sns.HealthIdentifiers.DRUG_ENFORCEMENT_AGENCY_NUMBER_US,
            sns.HealthIdentifiers.HEALTHCARE_PROCEDURE_CODE_US,
            sns.HealthIdentifiers.HEALTH_INSURANCE_CLAIM_NUMBER_US,
            sns.HealthIdentifiers.MEDICARE_BENEFICIARY_NUMBER_US,
            sns.HealthIdentifiers.NATIONAL_DRUG_CODE_US,
            sns.HealthIdentifiers.NATIONAL_PROVIDER_ID_US,

            // EU/FR health identifiers
            sns.HealthIdentifiers.HEALTH_INSURANCE_CARD_NUMBER_EU,
            sns.HealthIdentifiers.HEALTH_INSURANCE_NUMBER_FR,

            // GB health identifiers
            sns.HealthIdentifiers.NATIONAL_INSURANCE_NUMBER_GB,
            sns.HealthIdentifiers.NHS_NUMBER_GB,

            // CA health identifiers
            sns.HealthIdentifiers.PERSONAL_HEALTH_NUMBER_CA,
          ],
          operation: new sns.MaskOperation({
            maskWithCharacter: '*',
          }),
        }),
      ],
    });

    // Create an SNS topic with the comprehensive data protection policy
    const protectedTopic = new sns.Topic(this, 'ProtectedTopic', {
      topicName: 'protected-data-topic',
      dataProtectionPolicy: dataProtectionPolicy,
    });

    // Output the topic ARN
    new cdk.CfnOutput(this, 'ProtectedTopicArn', {
      value: protectedTopic.topicArn,
      description: 'ARN of the SNS topic with comprehensive data protection',
      exportName: 'ProtectedTopicArn',
    });

Checklist


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

@aws-cdk-automation aws-cdk-automation requested a review from a team June 5, 2025 17:06
@github-actions github-actions bot added feature-request A feature should be added or improved. p2 labels Jun 5, 2025
@mergify mergify bot added the contribution/core This is a PR that came from AWS. label Jun 5, 2025
@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildv2Project1C6BFA3F-wQm2hXv2jqQv
  • Commit ID: 68c9086
  • Result: SUCCEEDED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

@aws-cdk-automation aws-cdk-automation added the pr/needs-maintainer-review This PR needs a review from a Core Team Member label Jun 6, 2025
@ozelalisen ozelalisen self-assigned this Jun 11, 2025
@@ -355,6 +372,15 @@ export class Topic extends TopicBase {
throw new ValidationError(`displayName must be less than or equal to 100 characters, got ${props.displayName.length}`, this);
}

// Validate that data protection policy is only used with standard topics
if (props.dataProtectionPolicy && props.fifo) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no unit tests specifically for this validation logic, would like to see a unit test for this

Comment on lines +732 to +736
if (this.customDataIdentifiers.length > 10) {
throw new UnscopedValidationError(
'A maximum of 10 custom data identifiers are supported per data protection policy',
);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation for the maximum number of custom data identifiers happens during toJSON() rather than at construction time, which could lead to late failures. Move this validation to the constructor to fail earlier

/**
* The percentage of messages to sample for audit.
* Must be an integer between 0-99.
* @default 99
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this default value selected? Could it lead to high costs and performance impacts in production environments with high message volumes?

!logGroupName.startsWith('/aws/vendedlogs/')
) {
throw new UnscopedValidationError(
'CloudWatch Logs log group name must start with "/aws/vendedlogs/"',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enhance the error message by including that this is an AWS requirement for SNS data protection


// Validate regex length
if (this.regex.length > 200) {
throw new UnscopedValidationError(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation uses UnscopedValidationError which doesn't provide context about which construct is throwing the error. It is better to use ValidationError with the construct instance.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to refactor every other UnscopedValidationError as well

Comment on lines +696 to +701
this.customDataIdentifiers = this.statements
.flatMap((statement) => statement.dataIdentifiers)
.filter(
(dataIdentifier): dataIdentifier is CustomDataIdentifier =>
dataIdentifier instanceof CustomDataIdentifier,
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code collects all custom data identifiers from all statements, which could lead to duplicates if the same custom identifier is used in multiple statements. Consider deduplicating the custom identifiers to avoid potential issues, for example by using a Set or checking for duplicates by name.

@aws-cdk-automation aws-cdk-automation removed the pr/needs-maintainer-review This PR needs a review from a Core Team Member label Jun 12, 2025
@ykethan ykethan closed this by deleting the head repository Aug 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
contribution/core This is a PR that came from AWS. feature-request A feature should be added or improved. p2
Projects
None yet
Development

Successfully merging this pull request may close these issues.

@aws-cdk/sns: Topic missing DataProtectionPolicy
3 participants