From 987e1bed8bcd9cfc7558715bfa44ff6419843b2b Mon Sep 17 00:00:00 2001 From: ykethan Date: Wed, 4 Jun 2025 20:57:57 -0400 Subject: [PATCH 1/6] feat(sns): add sns data protection policy --- .../aws-sns/lib/data-protection.ts | 961 ++++++++++++++++++ packages/aws-cdk-lib/aws-sns/lib/index.ts | 1 + packages/aws-cdk-lib/aws-sns/lib/topic.ts | 27 + 3 files changed, 989 insertions(+) create mode 100644 packages/aws-cdk-lib/aws-sns/lib/data-protection.ts diff --git a/packages/aws-cdk-lib/aws-sns/lib/data-protection.ts b/packages/aws-cdk-lib/aws-sns/lib/data-protection.ts new file mode 100644 index 0000000000000..30305f297453c --- /dev/null +++ b/packages/aws-cdk-lib/aws-sns/lib/data-protection.ts @@ -0,0 +1,961 @@ +import { UnscopedValidationError } from '../../core'; + +/** + * Represents the data direction in a data protection policy statement. + */ +export enum DataDirection { + /** + * Applies to inbound messages (for Publish API requests). + */ + INBOUND = 'Inbound', + + /** + * Applies to outbound messages (for notification deliveries). + */ + OUTBOUND = 'Outbound', +} + +/** + * Represents an Amazon SNS data identifier for detecting sensitive data. + */ +export interface IDataIdentifier { + /** + * Returns the ARN or name of the data identifier. + */ + readonly identifier: string; +} + +/** + * Represents a predefined AWS managed data identifier for detecting specific types of sensitive data. + */ +export class ManagedDataIdentifier implements IDataIdentifier { + /** + * Creates a custom data identifier. + */ + constructor(private readonly identifierName: string) {} + + /** + * Returns the identifier ARN format. + */ + public get identifier(): string { + // https://docs.aws.amazon.com/sns/latest/dg/sns-message-data-protection-sensitive-data-types-credentials.html + // eslint-disable-next-line @cdklabs/no-literal-partition + return `arn:aws:dataprotection::aws:data-identifier/${this.identifierName}`; + } +} + +/** + * Properties for defining a custom data identifier. + * + * https://docs.aws.amazon.com/sns/latest/dg/sns-message-data-protection-custom-data-identifiers.html + */ +export interface CustomDataIdentifierProps { + /** + * The name of the custom data identifier. + * Names have a maximum length of 128 characters and can only contain alphanumeric characters, underscores, and hyphens. + */ + readonly name: string; + + /** + * The regular expression that identifies the sensitive data pattern. + * Regex patterns have a maximum length of 200 characters. + */ + readonly regex: string; +} + +/** + * Represents a custom data identifier for detecting organization-specific sensitive data patterns. + * + * Custom data identifiers in Amazon SNS only support Name and Regex properties. + */ +export class CustomDataIdentifier implements IDataIdentifier { + /** + * The name of the custom data identifier. + */ + public readonly name: string; + + /** + * The regular expression that identifies the sensitive data pattern. + */ + public readonly regex: string; + + constructor(props: CustomDataIdentifierProps) { + this.name = props.name; + this.regex = props.regex; + + // Validate regex length + if (this.regex.length > 200) { + throw new UnscopedValidationError( + 'Custom data identifier regex must be at most 200 characters', + ); + } + + // Validate name length + if (this.name.length > 128) { + throw new UnscopedValidationError( + 'Custom data identifier name must be at most 128 characters', + ); + } + + // Validate name format (alphanumeric, underscore, hyphen) + if (!/^[a-zA-Z0-9_-]+$/.test(this.name)) { + throw new UnscopedValidationError( + 'Custom data identifier name can only contain alphanumeric characters, underscores, and hyphens', + ); + } + } + + /** + * Returns the name of the custom identifier. + */ + public get identifier(): string { + return this.name; + } + + /** + * Creates the custom data identifier definition for the policy. + */ + public toJSON(): any { + return { + Name: this.name, + Regex: this.regex, + }; + } +} + +/** + * Represents a CloudWatch Logs destination for audit findings. + */ +export interface CloudWatchLogsDestination { + /** + * The name of the CloudWatch Logs log group. + * The name must start with "/aws/vendedlogs/". + */ + readonly logGroup: string; +} + +/** + * Represents a Firehose destination for audit findings. + */ +export interface FirehoseDestination { + /** + * The name of the Firehose delivery stream. + * The delivery stream must have "Direct PUT" as the source. + */ + readonly deliveryStream: string; +} + +/** + * Represents an S3 destination for audit findings. + */ +export interface S3Destination { + /** + * The name of the S3 bucket. + */ + readonly bucket: string; +} + +/** + * Represents a destination for audit findings. + */ +export interface AuditDestination { + /** + * CloudWatch Logs destination for audit findings. + * @default - no CloudWatch Logs destination + */ + readonly cloudWatchLogs?: CloudWatchLogsDestination; + + /** + * Firehose destination for audit findings. + * @default - no Firehose destination + */ + readonly firehose?: FirehoseDestination; + + /** + * S3 destination for audit findings. + * @default - no S3 destination + */ + readonly s3?: S3Destination; +} + +/** + * Properties for the Audit operation. + */ +export interface AuditOperationProps { + /** + * The percentage of messages to sample for audit. + * Must be an integer between 0-99. + * @default 99 + */ + readonly sampleRate?: number; + + /** + * The logging destination when sensitive data is found. + * @default - no findings destination + */ + readonly findingsDestination?: AuditDestination; + + /** + * The logging destination when no sensitive data is found. + * @default - no findings destination + */ + readonly noFindingsDestination?: AuditDestination; +} + +/** + * Represents the Audit operation for data protection. + */ +export class AuditOperation { + /** + * The percentage of messages to sample for audit. + */ + public readonly sampleRate?: number; + + /** + * The logging destination when sensitive data is found. + */ + public readonly findingsDestination?: AuditDestination; + + /** + * The logging destination when no sensitive data is found. + */ + public readonly noFindingsDestination?: AuditDestination; + + constructor(props: AuditOperationProps) { + this.sampleRate = props.sampleRate ?? 99; + this.findingsDestination = props.findingsDestination; + this.noFindingsDestination = props.noFindingsDestination; + + if (this.sampleRate < 0 || this.sampleRate > 99) { + throw new UnscopedValidationError( + 'Sample rate must be an integer between 0 and 99', + ); + } + + // Validate CloudWatch log group name pattern if specified + if ( + this.findingsDestination?.cloudWatchLogs && + !this.findingsDestination.cloudWatchLogs.logGroup.startsWith( + '/aws/vendedlogs/', + ) + ) { + throw new UnscopedValidationError( + 'CloudWatch Logs log group name must start with "/aws/vendedlogs/"', + ); + } + + if ( + this.noFindingsDestination?.cloudWatchLogs && + !this.noFindingsDestination.cloudWatchLogs.logGroup.startsWith( + '/aws/vendedlogs/', + ) + ) { + throw new UnscopedValidationError( + 'CloudWatch Logs log group name must start with "/aws/vendedlogs/"', + ); + } + } + + /** + * Creates the audit operation object for the policy. + */ + public toJSON(): any { + const audit: any = { + SampleRate: this.sampleRate?.toString(), + }; + + if (this.findingsDestination) { + const findingsDestination: any = {}; + + if (this.findingsDestination.cloudWatchLogs) { + findingsDestination.CloudWatchLogs = { + LogGroup: this.findingsDestination.cloudWatchLogs.logGroup, + }; + } + + if (this.findingsDestination.firehose) { + findingsDestination.Firehose = { + DeliveryStream: this.findingsDestination.firehose.deliveryStream, + }; + } + + if (this.findingsDestination.s3) { + findingsDestination.S3 = { + Bucket: this.findingsDestination.s3.bucket, + }; + } + + if (Object.keys(findingsDestination).length > 0) { + audit.FindingsDestination = findingsDestination; + } + } + + if (this.noFindingsDestination) { + const noFindingsDestination: any = {}; + + if (this.noFindingsDestination.cloudWatchLogs) { + noFindingsDestination.CloudWatchLogs = { + LogGroup: this.noFindingsDestination.cloudWatchLogs.logGroup, + }; + } + + if (this.noFindingsDestination.firehose) { + noFindingsDestination.Firehose = { + DeliveryStream: this.noFindingsDestination.firehose.deliveryStream, + }; + } + + if (this.noFindingsDestination.s3) { + noFindingsDestination.S3 = { + Bucket: this.noFindingsDestination.s3.bucket, + }; + } + + if (Object.keys(noFindingsDestination).length > 0) { + audit.NoFindingsDestination = noFindingsDestination; + } + } + + return { Audit: audit }; + } +} + +/** + * Properties for MaskOperation. + */ +export interface MaskOperationProps { + /** + * The character to use for masking sensitive data. + * @default '*' + */ + readonly maskWithCharacter?: string; +} + +/** + * Represents the mask operation for deidentifying sensitive data. + */ +export class MaskOperation { + /** + * Valid characters that can be used for masking. + */ + private static readonly VALID_MASK_CHARACTERS = [ + '*', + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + 'G', + 'H', + 'I', + 'J', + 'K', + 'L', + 'M', + 'N', + 'O', + 'P', + 'Q', + 'R', + 'S', + 'T', + 'U', + 'V', + 'W', + 'X', + 'Y', + 'Z', + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i', + 'j', + 'k', + 'l', + 'm', + 'n', + 'o', + 'p', + 'q', + 'r', + 's', + 't', + 'u', + 'v', + 'w', + 'x', + 'y', + 'z', + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + ' ', + '!', + '$', + '%', + '&', + '(', + ')', + '+', + ',', + '-', + '.', + '/', + '\\', + ':', + ';', + '<', + '=', + '>', + '@', + '[', + ']', + '^', + '_', + '`', + '|', + '~', + '#', + ]; + + /** + * The character to use for masking sensitive data. + */ + public readonly maskWithCharacter: string; + + constructor(props: MaskOperationProps = {}) { + this.maskWithCharacter = props.maskWithCharacter ?? '*'; + + if (!MaskOperation.VALID_MASK_CHARACTERS.includes(this.maskWithCharacter)) { + throw new UnscopedValidationError( + `Invalid mask character. Valid characters are: ${MaskOperation.VALID_MASK_CHARACTERS.join( + ', ', + )}`, + ); + } + } + + /** + * Creates the mask operation object for the policy. + */ + public toJSON(): any { + return { + Deidentify: { + MaskConfig: { + MaskWithCharacter: this.maskWithCharacter, + }, + }, + }; + } +} + +/** + * Represents the redact operation for deidentifying sensitive data. + */ +export class RedactOperation { + /** + * Creates the redact operation object for the policy. + */ + public toJSON(): any { + return { + Deidentify: { + RedactConfig: {}, + }, + }; + } +} + +/** + * Represents the deny operation to block messages with sensitive data. + */ +export class DenyOperation { + /** + * Creates the deny operation object for the policy. + */ + public toJSON(): any { + return { Deny: {} }; + } +} + +/** + * Type union of all possible data protection operations. + */ +export type DataProtectionOperation = + | AuditOperation + | MaskOperation + | RedactOperation + | DenyOperation; + +/** + * Represents a statement in a data protection policy. + */ +export interface DataProtectionPolicyStatementProps { + /** + * Optional identifier for the statement. + * @default - No statement ID + */ + readonly sid?: string; + + /** + * The direction of data flow to apply the policy to. + */ + readonly dataDirection: DataDirection; + + /** + * The data identifiers to detect. + */ + readonly dataIdentifiers: IDataIdentifier[]; + + /** + * The principals to apply the policy to. + * @default '*' (all principals) + */ + readonly principals?: string[]; + + /** + * The operation to perform when sensitive data is detected. + */ + readonly operation: DataProtectionOperation; +} + +/** + * Represents a statement in a data protection policy. + */ +export class DataProtectionPolicyStatement { + /** + * Optional identifier for the statement. + */ + public readonly sid?: string; + + /** + * The direction of data flow to apply the policy to. + */ + public readonly dataDirection: DataDirection; + + /** + * The data identifiers to detect. + */ + public readonly dataIdentifiers: IDataIdentifier[]; + + /** + * The principals to apply the policy to. + */ + public readonly principals: string[]; + + /** + * The operation to perform when sensitive data is detected. + */ + public readonly operation: DataProtectionOperation; + + constructor(props: DataProtectionPolicyStatementProps) { + this.sid = props.sid; + this.dataDirection = props.dataDirection; + this.dataIdentifiers = props.dataIdentifiers; + this.principals = props.principals ?? ['*']; + this.operation = props.operation; + } + + /** + * Creates the statement object for the policy. + */ + public toJSON(): any { + const statement: any = { + DataDirection: this.dataDirection, + Principal: this.principals, + DataIdentifier: this.dataIdentifiers.map( + (identifier) => identifier.identifier, + ), + Operation: this.operation.toJSON(), + }; + + if (this.sid) { + statement.Sid = this.sid; + } + + return statement; + } +} + +/** + * Properties for creating a data protection policy. + */ +export interface DataProtectionPolicyProps { + /** + * The name of the data protection policy. + * + * This is a friendly identifier for the policy. + */ + readonly name: string; + + /** + * Description of the data protection policy. + * @default - No description + */ + readonly description?: string; + + /** + * The version of the data protection policy. + * The current value should be '2021-06-01'. + * @default '2021-06-01' + */ + readonly version?: string; + + /** + * Statements for the data protection policy. + * + * Each statement defines a rule to detect and handle sensitive data. + * A policy can have multiple statements for different data types and operations. + */ + readonly statements: DataProtectionPolicyStatement[]; +} + +/** + * Represents a data protection policy for an SNS topic. + * + * Message Data Protection policies allow you to define rules to detect and handle + * sensitive data in your SNS messages. You can configure the policy to: + * + * - Audit: Log sensitive data detections to CloudWatch, S3, or Firehose + * - Mask: Replace sensitive data with a character like '*' + * - Redact: Remove sensitive data completely + * - Deny: Block messages containing sensitive data + * + * Each policy consists of one or more statements that define what data to protect and + * what action to take when that data is detected. Policies can use both AWS managed data + * identifiers for common sensitive data patterns and custom data identifiers for your + * organization-specific patterns. + * + * @see https://docs.aws.amazon.com/sns/latest/dg/message-data-protection.html + * @see https://docs.aws.amazon.com/sns/latest/dg/sns-message-data-protection-policies.html + */ +export class DataProtectionPolicy { + /** + * The name of the data protection policy. + */ + public readonly name: string; + + /** + * Description of the data protection policy. + */ + public readonly description?: string; + + /** + * The version of the data protection policy. + */ + public readonly version: string; + + /** + * Statements for the data protection policy. + */ + public readonly statements: DataProtectionPolicyStatement[]; + + /** + * Custom data identifiers in the policy. + */ + private readonly customDataIdentifiers: CustomDataIdentifier[]; + + constructor(props: DataProtectionPolicyProps) { + this.name = props.name; + this.description = props.description; + this.version = props.version ?? '2021-06-01'; + this.statements = props.statements; + + // Collect custom data identifiers from statements + this.customDataIdentifiers = []; + for (const statement of this.statements) { + for (const dataIdentifier of statement.dataIdentifiers) { + if (dataIdentifier instanceof CustomDataIdentifier) { + this.customDataIdentifiers.push(dataIdentifier); + } + } + } + + // Validate audit statement count + const auditStatements = this.statements.filter( + (statement) => statement.operation instanceof AuditOperation, + ); + + if (auditStatements.length > 1) { + throw new UnscopedValidationError( + 'A data protection policy can only have one audit statement', + ); + } + } + + /** + * Creates the data protection policy document. + */ + public toJSON(): any { + const policy: any = { + Name: this.name, + Version: this.version, + Statement: this.statements.map((statement) => statement.toJSON()), + }; + + if (this.description) { + policy.Description = this.description; + } + + // Format custom data identifiers according to AWS documentation + if (this.customDataIdentifiers.length > 0) { + // Maximum of 10 custom data identifiers per policy + if (this.customDataIdentifiers.length > 10) { + throw new UnscopedValidationError( + 'A maximum of 10 custom data identifiers are supported per data protection policy', + ); + } + + policy.Configuration = { + CustomDataIdentifier: this.customDataIdentifiers.map((identifier) => + identifier.toJSON(), + ), + }; + } + + return policy; + } + + /** + * Returns the policy document as a string. + */ + public toString(): string { + return JSON.stringify(this.toJSON()); + } +} + +/** + * Collection of AWS managed data identifiers for credentials. + * + * Based on AWS documentation: https://docs.aws.amazon.com/sns/latest/dg/sns-message-data-protection-sensitive-data-types-credentials.html + */ +export class CredentialsIdentifiers { + /** + * Detects AWS secret keys. + */ + public static readonly AWS_SECRET_KEY = new ManagedDataIdentifier( + 'AwsSecretKey', + ); + + /** + * Detects PGP private keys. + */ + public static readonly PGP_PRIVATE_KEY = new ManagedDataIdentifier( + 'PgpPrivateKey', + ); + + /** + * Detects private keys. + */ + public static readonly PRIVATE_KEY = new ManagedDataIdentifier( + 'PkcsPrivateKey', + ); + + /** + * Detects OpenSSH private keys. + */ + public static readonly OPENSSH_PRIVATE_KEY = new ManagedDataIdentifier( + 'OpenSshPrivateKey', + ); + + /** + * Detects PuTTY private keys. + */ + public static readonly PUTTY_PRIVATE_KEY = new ManagedDataIdentifier( + 'PuttyPrivateKey', + ); +} + +/** + * Collection of AWS managed data identifiers for device-related information. + * + * Based on AWS documentation: https://docs.aws.amazon.com/sns/latest/dg/sns-message-data-protection-sensitive-data-types-devices.html + */ +export class DeviceIdentifiers { + /** + * Detects IP addresses. + */ + public static readonly IP_ADDRESS = new ManagedDataIdentifier('IpAddress'); +} + +/** + * Collection of AWS managed data identifiers for financial information. + */ +export class FinancialIdentifiers { + /** + * Detects credit card numbers. + */ + public static readonly CREDIT_CARD_NUMBER = new ManagedDataIdentifier( + 'CreditCardNumber', + ); + + /** + * Detects credit card expiration dates. + */ + public static readonly CREDIT_CARD_EXPIRATION = new ManagedDataIdentifier( + 'CreditCardExpiration', + ); + + /** + * Detects credit card security codes (CVV). + */ + public static readonly CREDIT_CARD_CVV = new ManagedDataIdentifier( + 'CreditCardSecurityCode', + ); + + /** + * Detects bank account numbers. + * @param country Two-letter country code (e.g., 'US', 'GB') + */ + public static bankAccountNumber(country: string): ManagedDataIdentifier { + return new ManagedDataIdentifier(`BankAccountNumber-${country}`); + } +} + +/** + * Collection of AWS managed data identifiers for Protected Health Information (PHI). + */ +export class HealthIdentifiers { + /** + * Detects health insurance card numbers (EU). + */ + public static readonly HEALTH_INSURANCE_CARD_NUMBER_EU = + new ManagedDataIdentifier('HealthInsuranceCardNumber-EU'); + + /** + * Detects health insurance numbers (FR). + */ + public static readonly HEALTH_INSURANCE_NUMBER_FR = new ManagedDataIdentifier( + 'HealthInsuranceNumber-FR', + ); + + /** + * Detects healthcare common procedure coding system (HCPCS) codes (US). + */ + public static readonly HEALTHCARE_PROCEDURE_CODE_US = + new ManagedDataIdentifier('HealthcareProcedureCode-US'); + + /** + * Detects National Provider Identifier (NPI) numbers (US). + */ + public static readonly NATIONAL_PROVIDER_ID_US = new ManagedDataIdentifier( + 'NationalProviderId-US', + ); + + /** + * Detects Medicare Beneficiary Numbers (MBN) (US). + */ + public static readonly MEDICARE_BENEFICIARY_NUMBER_US = + new ManagedDataIdentifier('MedicareBeneficiaryNumber-US'); + + /** + * Detects National Drug Codes (NDC) (US). + */ + public static readonly NATIONAL_DRUG_CODE_US = new ManagedDataIdentifier( + 'NationalDrugCode-US', + ); + + /** + * Detects Drug Enforcement Agency Numbers (US). + */ + public static readonly DRUG_ENFORCEMENT_AGENCY_NUMBER_US = + new ManagedDataIdentifier('DrugEnforcementAgencyNumber-US'); + + /** + * Detects Health Insurance Claim Numbers (US). + */ + public static readonly HEALTH_INSURANCE_CLAIM_NUMBER_US = + new ManagedDataIdentifier('HealthInsuranceClaimNumber-US'); + + /** + * Detects National Insurance Numbers (GB). + */ + public static readonly NATIONAL_INSURANCE_NUMBER_GB = + new ManagedDataIdentifier('NationalInsuranceNumber-GB'); + + /** + * Detects NHS Numbers (GB). + */ + public static readonly NHS_NUMBER_GB = new ManagedDataIdentifier( + 'NhsNumber-GB', + ); + + /** + * Detects Personal Health Numbers (CA). + */ + public static readonly PERSONAL_HEALTH_NUMBER_CA = new ManagedDataIdentifier( + 'PersonalHealthNumber-CA', + ); +} + +/** + * Collection of AWS managed data identifiers for Personally Identifiable Information (PII). + */ +export class PersonalIdentifiers { + /** + * Detects postal addresses. + */ + public static readonly ADDRESS = new ManagedDataIdentifier('Address'); + + /** + * Detects email addresses. + */ + public static readonly EMAIL_ADDRESS = new ManagedDataIdentifier( + 'EmailAddress', + ); + + /** + * Detects full names of individuals. + */ + public static readonly NAME = new ManagedDataIdentifier('Name'); + + /** + * Detects vehicle identification numbers (VIN). + */ + public static readonly VEHICLE_IDENTIFICATION_NUMBER = + new ManagedDataIdentifier('VehicleIdentificationNumber'); + + /** + * Detects phone numbers. + * @param country Two-letter country code (e.g., 'US', 'GB') + */ + public static phoneNumber(country: string): ManagedDataIdentifier { + return new ManagedDataIdentifier(`PhoneNumber-${country}`); + } + + /** + * Detects driver's license numbers. + * @param country Two-letter country code (e.g., 'US', 'GB') + */ + public static driversLicense(country: string): ManagedDataIdentifier { + return new ManagedDataIdentifier(`DriversLicense-${country}`); + } + + /** + * Detects passport numbers. + * @param country Two-letter country code (e.g., 'US', 'GB') + */ + public static passportNumber(country: string): ManagedDataIdentifier { + return new ManagedDataIdentifier(`PassportNumber-${country}`); + } + + /** + * Detects Social Security Numbers (SSN). + * @param country Two-letter country code (e.g., 'US') + */ + public static ssn(country: string): ManagedDataIdentifier { + return new ManagedDataIdentifier(`Ssn-${country}`); + } +} diff --git a/packages/aws-cdk-lib/aws-sns/lib/index.ts b/packages/aws-cdk-lib/aws-sns/lib/index.ts index 2ea429718b107..fbae6eb8b083a 100644 --- a/packages/aws-cdk-lib/aws-sns/lib/index.ts +++ b/packages/aws-cdk-lib/aws-sns/lib/index.ts @@ -5,6 +5,7 @@ export * from './subscription'; export * from './subscriber'; export * from './subscription-filter'; export * from './delivery-policy'; +export * from './data-protection'; // AWS::SNS CloudFormation Resources: export * from './sns.generated'; diff --git a/packages/aws-cdk-lib/aws-sns/lib/topic.ts b/packages/aws-cdk-lib/aws-sns/lib/topic.ts index 261be3c006666..6af9c0fa94e5c 100644 --- a/packages/aws-cdk-lib/aws-sns/lib/topic.ts +++ b/packages/aws-cdk-lib/aws-sns/lib/topic.ts @@ -1,4 +1,5 @@ import { Construct } from 'constructs'; +import { DataProtectionPolicy } from './data-protection'; import { CfnTopic } from './sns.generated'; import { ITopic, TopicBase } from './topic-base'; import { IRole } from '../../aws-iam'; @@ -110,6 +111,22 @@ export interface TopicProps { * @default undefined - SNS default setting is FifoThroughputScope.TOPIC */ readonly fifoThroughputScope?: FifoThroughputScope; + + /** + * Data protection policy for the topic to detect, protect, and redact sensitive data. + * + * Message Data Protection enables you to define policies to audit, mask, redact, + * or block sensitive information in messages published to the SNS topic. This helps + * you comply with data privacy regulations and protect sensitive information. + * + * This feature is only available for standard SNS topics (not FIFO). + * + * @see https://docs.aws.amazon.com/sns/latest/dg/message-data-protection.html + * @see https://docs.aws.amazon.com/sns/latest/dg/sns-message-data-protection-policies.html + * + * @default - no data protection policy + */ + readonly dataProtectionPolicy?: DataProtectionPolicy; } /** @@ -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) { + throw new ValidationError('Data protection policy is only available for standard SNS topics, not FIFO topics', this); + } + + // Use data protection policy object directly without JSON.stringify + const dataProtectionPolicyObj = props.dataProtectionPolicy ? + props.dataProtectionPolicy.toJSON() : undefined; + const resource = new CfnTopic(this, 'Resource', { archivePolicy: props.messageRetentionPeriodInDays ? { MessageRetentionPeriod: props.messageRetentionPeriodInDays, @@ -368,6 +394,7 @@ export class Topic extends TopicBase { deliveryStatusLogging: Lazy.any({ produce: () => this.renderLoggingConfigs() }, { omitEmptyArray: true }), tracingConfig: props.tracingConfig, fifoThroughputScope: props.fifoThroughputScope, + dataProtectionPolicy: dataProtectionPolicyObj, }); this.topicArn = this.getResourceArnAttribute(resource.ref, { From fdeb530c3803474a685c44343cd3518e4b4381de Mon Sep 17 00:00:00 2001 From: ykethan Date: Wed, 4 Jun 2025 21:29:19 -0400 Subject: [PATCH 2/6] add validations and update statements --- .../aws-sns/lib/data-protection.ts | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/packages/aws-cdk-lib/aws-sns/lib/data-protection.ts b/packages/aws-cdk-lib/aws-sns/lib/data-protection.ts index 30305f297453c..a50768bbe951e 100644 --- a/packages/aws-cdk-lib/aws-sns/lib/data-protection.ts +++ b/packages/aws-cdk-lib/aws-sns/lib/data-protection.ts @@ -103,6 +103,23 @@ export class CustomDataIdentifier implements IDataIdentifier { 'Custom data identifier name can only contain alphanumeric characters, underscores, and hyphens', ); } + + // Validate regex characters + const validRegexPattern = /^[a-zA-Z0-9_#=@\/;,\- ^$?\[\]{}\|\\*+.]+$/; + if (!validRegexPattern.test(this.regex)) { + throw new UnscopedValidationError( + 'Custom data identifier regex can only contain alphanumeric characters and specific symbols (_, #, =, @, /, ;, ,, -, space) or regex special characters (^, $, ?, [, ], {, }, |, \\, *, +, .)', + ); + } + + // Validate that custom identifier name doesn't conflict with managed data identifiers + const managedIdPattern = + /^(Address|EmailAddress|Name|VehicleIdentificationNumber|PhoneNumber-|DriversLicense-|PassportNumber-|Ssn-|AwsSecretKey|PgpPrivateKey|PkcsPrivateKey|OpenSshPrivateKey|PuttyPrivateKey|IpAddress|CreditCardNumber|CreditCardExpiration|CreditCardSecurityCode|BankAccountNumber-|HealthInsuranceCardNumber-|HealthInsuranceNumber-|HealthcareProcedureCode-|NationalProviderId-|MedicareBeneficiaryNumber-|NationalDrugCode-|DrugEnforcementAgencyNumber-|HealthInsuranceClaimNumber-|NationalInsuranceNumber-|NhsNumber-|PersonalHealthNumber-)/; + if (managedIdPattern.test(this.name)) { + throw new UnscopedValidationError( + 'Custom data identifier name cannot match a managed data identifier name', + ); + } } /** @@ -674,14 +691,12 @@ export class DataProtectionPolicy { this.statements = props.statements; // Collect custom data identifiers from statements - this.customDataIdentifiers = []; - for (const statement of this.statements) { - for (const dataIdentifier of statement.dataIdentifiers) { - if (dataIdentifier instanceof CustomDataIdentifier) { - this.customDataIdentifiers.push(dataIdentifier); - } - } - } + this.customDataIdentifiers = this.statements + .flatMap((statement) => statement.dataIdentifiers) + .filter( + (dataIdentifier): dataIdentifier is CustomDataIdentifier => + dataIdentifier instanceof CustomDataIdentifier, + ); // Validate audit statement count const auditStatements = this.statements.filter( From ae5458ffb32eaa6f4c75189556da240c7710b634 Mon Sep 17 00:00:00 2001 From: ykethan Date: Thu, 5 Jun 2025 12:44:04 -0400 Subject: [PATCH 3/6] add integration test --- .../SNSDataProtectionInteg.assets.json | 20 ++ .../SNSDataProtectionInteg.template.json | 147 +++++++++ ...efaultTestDeployAssertFE9E7674.assets.json | 20 ++ ...aultTestDeployAssertFE9E7674.template.json | 36 +++ .../cdk.out | 1 + .../integ.json | 13 + .../manifest.json | 137 ++++++++ .../tree.json | 1 + .../aws-sns/test/integ.sns-data-protection.ts | 117 +++++++ .../aws-sns/lib/data-protection.ts | 36 ++- .../aws-sns/test/data-protection.test.ts | 305 ++++++++++++++++++ 11 files changed, 816 insertions(+), 17 deletions(-) create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/SNSDataProtectionInteg.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/SNSDataProtectionInteg.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/SNSDataProtectionTestDefaultTestDeployAssertFE9E7674.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/SNSDataProtectionTestDefaultTestDeployAssertFE9E7674.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/integ.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/tree.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.ts create mode 100644 packages/aws-cdk-lib/aws-sns/test/data-protection.test.ts diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/SNSDataProtectionInteg.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/SNSDataProtectionInteg.assets.json new file mode 100644 index 0000000000000..9d962ec22c245 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/SNSDataProtectionInteg.assets.json @@ -0,0 +1,20 @@ +{ + "version": "44.0.0", + "files": { + "15f4987ccbbd41912a8433ab226ffb9f8549c327152df7850757644d59c86a2b": { + "displayName": "SNSDataProtectionInteg Template", + "source": { + "path": "SNSDataProtectionInteg.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "15f4987ccbbd41912a8433ab226ffb9f8549c327152df7850757644d59c86a2b.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/SNSDataProtectionInteg.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/SNSDataProtectionInteg.template.json new file mode 100644 index 0000000000000..b3f4c7fb93211 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/SNSDataProtectionInteg.template.json @@ -0,0 +1,147 @@ +{ + "Resources": { + "DataProtectionLogGroup833D047D": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "LogGroupName": "/aws/vendedlogs/sns-data-protection", + "RetentionInDays": 731 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "ProtectedTopic275DD48D": { + "Type": "AWS::SNS::Topic", + "Properties": { + "DataProtectionPolicy": { + "Name": "ComprehensiveProtectionPolicy", + "Version": "2021-06-01", + "Statement": [ + { + "DataDirection": "Inbound", + "Principal": [ + "*" + ], + "DataIdentifier": [ + "arn:aws:dataprotection::aws:data-identifier/Name", + "arn:aws:dataprotection::aws:data-identifier/EmailAddress", + "arn:aws:dataprotection::aws:data-identifier/Address" + ], + "Operation": { + "Audit": { + "SampleRate": "99", + "FindingsDestination": { + "CloudWatchLogs": { + "LogGroup": { + "Ref": "DataProtectionLogGroup833D047D" + } + } + } + } + }, + "Sid": "AuditPII" + }, + { + "DataDirection": "Inbound", + "Principal": [ + "*" + ], + "DataIdentifier": [ + "arn:aws:dataprotection::aws:data-identifier/CreditCardNumber", + "arn:aws:dataprotection::aws:data-identifier/CreditCardExpiration", + "arn:aws:dataprotection::aws:data-identifier/CreditCardSecurityCode" + ], + "Operation": { + "Deidentify": { + "MaskConfig": { + "MaskWithCharacter": "#" + } + } + }, + "Sid": "MaskFinancialData" + }, + { + "DataDirection": "Inbound", + "Principal": [ + "*" + ], + "DataIdentifier": [ + "EmployeeID", + "CustomerID" + ], + "Operation": { + "Deidentify": { + "RedactConfig": {} + } + }, + "Sid": "RedactCustomPatterns" + }, + { + "DataDirection": "Inbound", + "Principal": [ + "*" + ], + "DataIdentifier": [ + "arn:aws:dataprotection::aws:data-identifier/AwsSecretKey", + "arn:aws:dataprotection::aws:data-identifier/PkcsPrivateKey" + ], + "Operation": { + "Deny": {} + }, + "Sid": "BlockCredentials" + } + ], + "Description": "SNS data protection policy integration test", + "Configuration": { + "CustomDataIdentifier": [ + { + "Name": "EmployeeID", + "Regex": "EMP-[0-9]{6}" + }, + { + "Name": "CustomerID", + "Regex": "CUST-[0-9]{8}" + } + ] + } + }, + "TopicName": "topic-data-protection" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/SNSDataProtectionTestDefaultTestDeployAssertFE9E7674.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/SNSDataProtectionTestDefaultTestDeployAssertFE9E7674.assets.json new file mode 100644 index 0000000000000..e1c0860dde709 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/SNSDataProtectionTestDefaultTestDeployAssertFE9E7674.assets.json @@ -0,0 +1,20 @@ +{ + "version": "44.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "displayName": "SNSDataProtectionTestDefaultTestDeployAssertFE9E7674 Template", + "source": { + "path": "SNSDataProtectionTestDefaultTestDeployAssertFE9E7674.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/SNSDataProtectionTestDefaultTestDeployAssertFE9E7674.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/SNSDataProtectionTestDefaultTestDeployAssertFE9E7674.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/SNSDataProtectionTestDefaultTestDeployAssertFE9E7674.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/cdk.out new file mode 100644 index 0000000000000..b3a26d44a5f73 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"44.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/integ.json new file mode 100644 index 0000000000000..35fe7778d15e8 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/integ.json @@ -0,0 +1,13 @@ +{ + "version": "44.0.0", + "testCases": { + "SNSDataProtectionTest/DefaultTest": { + "stacks": [ + "SNSDataProtectionInteg" + ], + "assertionStack": "SNSDataProtectionTest/DefaultTest/DeployAssert", + "assertionStackName": "SNSDataProtectionTestDefaultTestDeployAssertFE9E7674" + } + }, + "minimumCliVersion": "2.1017.1" +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/manifest.json new file mode 100644 index 0000000000000..245fefebb31bf --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/manifest.json @@ -0,0 +1,137 @@ +{ + "version": "44.0.0", + "artifacts": { + "SNSDataProtectionInteg.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "SNSDataProtectionInteg.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "SNSDataProtectionInteg": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "SNSDataProtectionInteg.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/15f4987ccbbd41912a8433ab226ffb9f8549c327152df7850757644d59c86a2b.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "SNSDataProtectionInteg.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "SNSDataProtectionInteg.assets" + ], + "metadata": { + "/SNSDataProtectionInteg/DataProtectionLogGroup": [ + { + "type": "aws:cdk:analytics:construct", + "data": { + "logGroupName": "*", + "removalPolicy": "destroy" + } + } + ], + "/SNSDataProtectionInteg/DataProtectionLogGroup/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "DataProtectionLogGroup833D047D" + } + ], + "/SNSDataProtectionInteg/ProtectedTopic": [ + { + "type": "aws:cdk:analytics:construct", + "data": { + "topicName": "*" + } + } + ], + "/SNSDataProtectionInteg/ProtectedTopic/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "ProtectedTopic275DD48D" + } + ], + "/SNSDataProtectionInteg/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/SNSDataProtectionInteg/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "SNSDataProtectionInteg" + }, + "SNSDataProtectionTestDefaultTestDeployAssertFE9E7674.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "SNSDataProtectionTestDefaultTestDeployAssertFE9E7674.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "SNSDataProtectionTestDefaultTestDeployAssertFE9E7674": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "SNSDataProtectionTestDefaultTestDeployAssertFE9E7674.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "SNSDataProtectionTestDefaultTestDeployAssertFE9E7674.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "SNSDataProtectionTestDefaultTestDeployAssertFE9E7674.assets" + ], + "metadata": { + "/SNSDataProtectionTest/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/SNSDataProtectionTest/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "SNSDataProtectionTest/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + }, + "minimumCliVersion": "2.1017.1" +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/tree.json new file mode 100644 index 0000000000000..e7a79066348b7 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.js.snapshot/tree.json @@ -0,0 +1 @@ +{"version":"tree-0.1","tree":{"id":"App","path":"","constructInfo":{"fqn":"aws-cdk-lib.App","version":"0.0.0"},"children":{"SNSDataProtectionInteg":{"id":"SNSDataProtectionInteg","path":"SNSDataProtectionInteg","constructInfo":{"fqn":"aws-cdk-lib.Stack","version":"0.0.0"},"children":{"DataProtectionLogGroup":{"id":"DataProtectionLogGroup","path":"SNSDataProtectionInteg/DataProtectionLogGroup","constructInfo":{"fqn":"aws-cdk-lib.aws_logs.LogGroup","version":"0.0.0","metadata":[{"logGroupName":"*","removalPolicy":"destroy"}]},"children":{"Resource":{"id":"Resource","path":"SNSDataProtectionInteg/DataProtectionLogGroup/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_logs.CfnLogGroup","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Logs::LogGroup","aws:cdk:cloudformation:props":{"logGroupName":"/aws/vendedlogs/sns-data-protection","retentionInDays":731}}}}},"ProtectedTopic":{"id":"ProtectedTopic","path":"SNSDataProtectionInteg/ProtectedTopic","constructInfo":{"fqn":"aws-cdk-lib.aws_sns.Topic","version":"0.0.0","metadata":[{"topicName":"*"}]},"children":{"Resource":{"id":"Resource","path":"SNSDataProtectionInteg/ProtectedTopic/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_sns.CfnTopic","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::SNS::Topic","aws:cdk:cloudformation:props":{"dataProtectionPolicy":{"Name":"ComprehensiveProtectionPolicy","Version":"2021-06-01","Statement":[{"DataDirection":"Inbound","Principal":["*"],"DataIdentifier":["arn:aws:dataprotection::aws:data-identifier/Name","arn:aws:dataprotection::aws:data-identifier/EmailAddress","arn:aws:dataprotection::aws:data-identifier/Address"],"Operation":{"Audit":{"SampleRate":"99","FindingsDestination":{"CloudWatchLogs":{"LogGroup":{"Ref":"DataProtectionLogGroup833D047D"}}}}},"Sid":"AuditPII"},{"DataDirection":"Inbound","Principal":["*"],"DataIdentifier":["arn:aws:dataprotection::aws:data-identifier/CreditCardNumber","arn:aws:dataprotection::aws:data-identifier/CreditCardExpiration","arn:aws:dataprotection::aws:data-identifier/CreditCardSecurityCode"],"Operation":{"Deidentify":{"MaskConfig":{"MaskWithCharacter":"#"}}},"Sid":"MaskFinancialData"},{"DataDirection":"Inbound","Principal":["*"],"DataIdentifier":["EmployeeID","CustomerID"],"Operation":{"Deidentify":{"RedactConfig":{}}},"Sid":"RedactCustomPatterns"},{"DataDirection":"Inbound","Principal":["*"],"DataIdentifier":["arn:aws:dataprotection::aws:data-identifier/AwsSecretKey","arn:aws:dataprotection::aws:data-identifier/PkcsPrivateKey"],"Operation":{"Deny":{}},"Sid":"BlockCredentials"}],"Description":"SNS data protection policy integration test","Configuration":{"CustomDataIdentifier":[{"Name":"EmployeeID","Regex":"EMP-[0-9]{6}"},{"Name":"CustomerID","Regex":"CUST-[0-9]{8}"}]}},"topicName":"topic-data-protection"}}}}},"BootstrapVersion":{"id":"BootstrapVersion","path":"SNSDataProtectionInteg/BootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"0.0.0"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"SNSDataProtectionInteg/CheckBootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnRule","version":"0.0.0"}}}},"SNSDataProtectionTest":{"id":"SNSDataProtectionTest","path":"SNSDataProtectionTest","constructInfo":{"fqn":"@aws-cdk/integ-tests-alpha.IntegTest","version":"0.0.0"},"children":{"DefaultTest":{"id":"DefaultTest","path":"SNSDataProtectionTest/DefaultTest","constructInfo":{"fqn":"@aws-cdk/integ-tests-alpha.IntegTestCase","version":"0.0.0"},"children":{"Default":{"id":"Default","path":"SNSDataProtectionTest/DefaultTest/Default","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}},"DeployAssert":{"id":"DeployAssert","path":"SNSDataProtectionTest/DefaultTest/DeployAssert","constructInfo":{"fqn":"aws-cdk-lib.Stack","version":"0.0.0"},"children":{"BootstrapVersion":{"id":"BootstrapVersion","path":"SNSDataProtectionTest/DefaultTest/DeployAssert/BootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"0.0.0"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"SNSDataProtectionTest/DefaultTest/DeployAssert/CheckBootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnRule","version":"0.0.0"}}}}}}}},"Tree":{"id":"Tree","path":"Tree","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}}}}} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.ts new file mode 100644 index 0000000000000..bad0c3198ff95 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-data-protection.ts @@ -0,0 +1,117 @@ +import { App, Stack, StackProps, RemovalPolicy } from 'aws-cdk-lib'; +import { + Topic, + DataProtectionPolicy, + DataProtectionPolicyStatement, + DataDirection, + CustomDataIdentifier, + MaskOperation, + RedactOperation, + AuditOperation, + DenyOperation, + PersonalIdentifiers, + FinancialIdentifiers, + CredentialsIdentifiers, +} from 'aws-cdk-lib/aws-sns'; +import * as integ from '@aws-cdk/integ-tests-alpha'; +import * as logs from 'aws-cdk-lib/aws-logs'; + +class SNSDataProtectionInteg extends Stack { + constructor(scope: App, id: string, props?: StackProps) { + super(scope, id, props); + + // Create custom data identifiers + const employeeIdPattern = new CustomDataIdentifier({ + name: 'EmployeeID', + regex: 'EMP-[0-9]{6}', + }); + + const customerIdPattern = new CustomDataIdentifier({ + name: 'CustomerID', + regex: 'CUST-[0-9]{8}', + }); + + // Create a CloudWatch log group for SNS data protection findings + const logGroup = new logs.LogGroup(this, 'DataProtectionLogGroup', { + logGroupName: '/aws/vendedlogs/sns-data-protection', + removalPolicy: RemovalPolicy.DESTROY, + }); + + // Create an SNS topic with data protection policy + const topic = new Topic(this, 'ProtectedTopic', { + topicName: 'topic-data-protection', + dataProtectionPolicy: new DataProtectionPolicy({ + name: 'ComprehensiveProtectionPolicy', + description: 'SNS data protection policy integration test', + statements: [ + // Audit statement for PII data + new DataProtectionPolicyStatement({ + sid: 'AuditPII', + dataDirection: DataDirection.INBOUND, + dataIdentifiers: [ + // Basic personal identifiers + PersonalIdentifiers.NAME, + PersonalIdentifiers.EMAIL_ADDRESS, + PersonalIdentifiers.ADDRESS, + ], + operation: new AuditOperation({ + sampleRate: 99, + findingsDestination: { + cloudWatchLogs: { + logGroup: logGroup.logGroupName, + }, + }, + }), + }), + + // Mask financial information + new DataProtectionPolicyStatement({ + sid: 'MaskFinancialData', + dataDirection: DataDirection.INBOUND, + dataIdentifiers: [ + FinancialIdentifiers.CREDIT_CARD_NUMBER, + FinancialIdentifiers.CREDIT_CARD_EXPIRATION, + FinancialIdentifiers.CREDIT_CARD_CVV, + ], + operation: new MaskOperation({ + maskWithCharacter: '#', + }), + }), + + // Redact custom patterns + new DataProtectionPolicyStatement({ + sid: 'RedactCustomPatterns', + dataDirection: DataDirection.INBOUND, + dataIdentifiers: [employeeIdPattern, customerIdPattern], + operation: new RedactOperation(), + }), + + // Block messages with credentials + new DataProtectionPolicyStatement({ + sid: 'BlockCredentials', + dataDirection: DataDirection.INBOUND, + dataIdentifiers: [ + CredentialsIdentifiers.AWS_SECRET_KEY, + CredentialsIdentifiers.PRIVATE_KEY, + ], + operation: new DenyOperation(), + }), + ], + }), + }); + + // Apply removal policy to SNS topic + topic.applyRemovalPolicy(RemovalPolicy.DESTROY); + } +} + +const app = new App(); + +const stack = new SNSDataProtectionInteg(app, 'SNSDataProtectionInteg'); + +// Create an integration test that will deploy the stack and create a snapshot test +new integ.IntegTest(app, 'SNSDataProtectionTest', { + testCases: [stack], +}); + +app.synth(); diff --git a/packages/aws-cdk-lib/aws-sns/lib/data-protection.ts b/packages/aws-cdk-lib/aws-sns/lib/data-protection.ts index a50768bbe951e..5a6407ef186ac 100644 --- a/packages/aws-cdk-lib/aws-sns/lib/data-protection.ts +++ b/packages/aws-cdk-lib/aws-sns/lib/data-protection.ts @@ -1,4 +1,4 @@ -import { UnscopedValidationError } from '../../core'; +import { Token, UnscopedValidationError } from '../../core'; /** * Represents the data direction in a data protection policy statement. @@ -249,23 +249,25 @@ export class AuditOperation { ); } - // Validate CloudWatch log group name pattern if specified - if ( - this.findingsDestination?.cloudWatchLogs && - !this.findingsDestination.cloudWatchLogs.logGroup.startsWith( - '/aws/vendedlogs/', - ) - ) { - throw new UnscopedValidationError( - 'CloudWatch Logs log group name must start with "/aws/vendedlogs/"', - ); - } + // Validate CloudWatch log group names if specified + this.validateLogGroupName( + this.findingsDestination?.cloudWatchLogs?.logGroup, + ); + this.validateLogGroupName( + this.noFindingsDestination?.cloudWatchLogs?.logGroup, + ); + } + /** + * Validates that a CloudWatch log group name starts with '/aws/vendedlogs/' if it's a concrete string + * For token values (references), validation is deferred to deployment time + */ + private validateLogGroupName(logGroupName?: string): void { if ( - this.noFindingsDestination?.cloudWatchLogs && - !this.noFindingsDestination.cloudWatchLogs.logGroup.startsWith( - '/aws/vendedlogs/', - ) + logGroupName !== undefined && + typeof logGroupName === 'string' && + !Token.isUnresolved(logGroupName) && + !logGroupName.startsWith('/aws/vendedlogs/') ) { throw new UnscopedValidationError( 'CloudWatch Logs log group name must start with "/aws/vendedlogs/"', @@ -349,7 +351,7 @@ export interface MaskOperationProps { } /** - * Represents the mask operation for deidentifying sensitive data. + * Represents the mask operation for de-identifying sensitive data. */ export class MaskOperation { /** diff --git a/packages/aws-cdk-lib/aws-sns/test/data-protection.test.ts b/packages/aws-cdk-lib/aws-sns/test/data-protection.test.ts new file mode 100644 index 0000000000000..59f09d6738149 --- /dev/null +++ b/packages/aws-cdk-lib/aws-sns/test/data-protection.test.ts @@ -0,0 +1,305 @@ +import { Template } from '../../assertions'; +import * as cdk from '../../core'; +import * as sns from '../lib'; + +describe('SNS Data Protection', () => { + describe('CustomDataIdentifier', () => { + test('creates a valid custom data identifier', () => { + // WHEN + const customId = new sns.CustomDataIdentifier({ + name: 'ValidIdentifier', + regex: '[A-Z]{3}-[0-9]{4}', + }); + + // THEN + expect(customId.name).toEqual('ValidIdentifier'); + expect(customId.regex).toEqual('[A-Z]{3}-[0-9]{4}'); + expect(customId.identifier).toEqual('ValidIdentifier'); + }); + + test('throws when name exceeds maximum length', () => { + // GIVEN + const tooLongName = 'A'.repeat(129); // 129 chars (max is 128) + + // THEN + expect(() => { + new sns.CustomDataIdentifier({ + name: tooLongName, + regex: '[A-Z]{3}-[0-9]{4}', + }); + }).toThrow(/Custom data identifier name must be at most 128 characters/); + }); + + test('throws when name has invalid format', () => { + // THEN + expect(() => { + new sns.CustomDataIdentifier({ + name: 'Invalid@Name$', + regex: '[A-Z]{3}-[0-9]{4}', + }); + }).toThrow( + /Custom data identifier name can only contain alphanumeric characters, underscores, and hyphens/, + ); + }); + + test('throws when regex exceeds maximum length', () => { + // GIVEN + const tooLongRegex = '[0-9]-'.repeat(70); // 280 chars (max is 200) + + // THEN + expect(() => { + new sns.CustomDataIdentifier({ + name: 'ValidName', + regex: tooLongRegex, + }); + }).toThrow(/Custom data identifier regex must be at most 200 characters/); + }); + + test('throws when regex has invalid characters', () => { + // THEN + expect(() => { + new sns.CustomDataIdentifier({ + name: 'ValidName', + regex: 'Invalid~Regex&Characters', + }); + }).toThrow( + /Custom data identifier regex can only contain alphanumeric characters and specific symbols/, + ); + }); + + test('throws when name conflicts with managed identifier', () => { + // THEN + expect(() => { + new sns.CustomDataIdentifier({ + name: 'EmailAddress', // conflicts with managed identifier + regex: '[a-z]+@[a-z]+\\.[a-z]+', + }); + }).toThrow( + /Custom data identifier name cannot match a managed data identifier name/, + ); + }); + + test('toJSON returns proper structure', () => { + // GIVEN + const customId = new sns.CustomDataIdentifier({ + name: 'CustomId', + regex: 'CUSTOM-[0-9]{6}', + }); + + // WHEN + const json = customId.toJSON(); + + // THEN + expect(json).toEqual({ + Name: 'CustomId', + Regex: 'CUSTOM-[0-9]{6}', + }); + }); + }); + + describe('DataProtectionPolicy', () => { + test('collects custom data identifiers from statements using flatMap/filter', () => { + // GIVEN + const stack = new cdk.Stack(); + + // Create custom data identifiers + const customId1 = new sns.CustomDataIdentifier({ + name: 'CustomId1', + regex: 'CUST-[0-9]{6}', + }); + + const customId2 = new sns.CustomDataIdentifier({ + name: 'CustomId2', + regex: 'ID-[A-Z]{3}', + }); + + // Statement with a mix of custom and managed identifiers + const statements = [ + new sns.DataProtectionPolicyStatement({ + sid: 'Statement1', + dataDirection: sns.DataDirection.INBOUND, + dataIdentifiers: [ + customId1, + sns.PersonalIdentifiers.NAME, // Managed identifier + customId2, + ], + operation: new sns.RedactOperation(), + }), + ]; + + // WHEN + const policy = new sns.DataProtectionPolicy({ + name: 'TestPolicy', + statements, + }); + + // Create topic with the policy + new sns.Topic(stack, 'Topic', { + dataProtectionPolicy: policy, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::SNS::Topic', { + DataProtectionPolicy: { + Configuration: { + CustomDataIdentifier: [ + { + Name: 'CustomId1', + Regex: 'CUST-[0-9]{6}', + }, + { + Name: 'CustomId2', + Regex: 'ID-[A-Z]{3}', + }, + ], + }, + }, + }); + }); + + test('collects custom data identifiers across multiple statements', () => { + // GIVEN + const stack = new cdk.Stack(); + + // Create custom data identifiers + const customId1 = new sns.CustomDataIdentifier({ + name: 'CustomId1', + regex: 'CUST-[0-9]{6}', + }); + + const customId2 = new sns.CustomDataIdentifier({ + name: 'CustomId2', + regex: 'ID-[A-Z]{3}', + }); + + const customId3 = new sns.CustomDataIdentifier({ + name: 'CustomId3', + regex: 'TICKET-[0-9]{5}', + }); + + // Statements with distributed custom identifiers + const statements = [ + new sns.DataProtectionPolicyStatement({ + sid: 'Statement1', + dataDirection: sns.DataDirection.INBOUND, + dataIdentifiers: [ + customId1, + sns.PersonalIdentifiers.NAME, // Managed identifier + ], + operation: new sns.RedactOperation(), + }), + new sns.DataProtectionPolicyStatement({ + sid: 'Statement2', + dataDirection: sns.DataDirection.OUTBOUND, + dataIdentifiers: [customId2, customId3], + operation: new sns.MaskOperation(), + }), + ]; + + // WHEN + const policy = new sns.DataProtectionPolicy({ + name: 'TestPolicy', + statements, + }); + + // Create topic with the policy + new sns.Topic(stack, 'Topic', { + dataProtectionPolicy: policy, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::SNS::Topic', { + DataProtectionPolicy: { + Configuration: { + CustomDataIdentifier: [ + { + Name: 'CustomId1', + Regex: 'CUST-[0-9]{6}', + }, + { + Name: 'CustomId2', + Regex: 'ID-[A-Z]{3}', + }, + { + Name: 'CustomId3', + Regex: 'TICKET-[0-9]{5}', + }, + ], + }, + }, + }); + }); + + test('throws error when exceeding maximum of 10 custom data identifiers', () => { + // GIVEN + // Create 11 custom data identifiers (exceeds the limit) + const identifiers: sns.CustomDataIdentifier[] = []; + for (let i = 1; i <= 11; i++) { + identifiers.push( + new sns.CustomDataIdentifier({ + name: `CustomId${i}`, + regex: `ID-[A-Z]{${i}}`, + }), + ); + } + + // Split across two statements + const statements = [ + new sns.DataProtectionPolicyStatement({ + sid: 'Statement1', + dataDirection: sns.DataDirection.INBOUND, + dataIdentifiers: identifiers.slice(0, 6), // First 6 identifiers + operation: new sns.RedactOperation(), + }), + new sns.DataProtectionPolicyStatement({ + sid: 'Statement2', + dataDirection: sns.DataDirection.INBOUND, + dataIdentifiers: identifiers.slice(6), // Remaining 5 identifiers + operation: new sns.MaskOperation(), + }), + ]; + + // WHEN creating a policy with too many identifiers + const policy = new sns.DataProtectionPolicy({ + name: 'TestPolicy', + statements, + }); + + // THEN expect an error when converting to JSON (this is when validation occurs) + expect(() => { + policy.toString(); // This calls toJSON() internally + }).toThrow(/maximum of 10 custom data identifiers/); + }); + + test('throws error when creating multiple audit statements', () => { + // GIVEN + // Create two statements with Audit operations + const statements = [ + new sns.DataProtectionPolicyStatement({ + sid: 'AuditStatement1', + dataDirection: sns.DataDirection.INBOUND, + dataIdentifiers: [sns.PersonalIdentifiers.NAME], + operation: new sns.AuditOperation({ + sampleRate: 50, + }), + }), + new sns.DataProtectionPolicyStatement({ + sid: 'AuditStatement2', + dataDirection: sns.DataDirection.OUTBOUND, + dataIdentifiers: [sns.PersonalIdentifiers.EMAIL_ADDRESS], + operation: new sns.AuditOperation({ + sampleRate: 75, + }), + }), + ]; + + // WHEN/THEN expect error during policy creation + expect(() => { + new sns.DataProtectionPolicy({ + name: 'TestPolicy', + statements, + }); + }).toThrow(/A data protection policy can only have one audit statement/); + }); + }); +}); From 33d3589b9fa998139c4255cd40bcf00d1a630b38 Mon Sep 17 00:00:00 2001 From: ykethan Date: Thu, 5 Jun 2025 12:52:35 -0400 Subject: [PATCH 4/6] add readme --- packages/aws-cdk-lib/aws-sns/README.md | 124 +++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/packages/aws-cdk-lib/aws-sns/README.md b/packages/aws-cdk-lib/aws-sns/README.md index de58c748d8323..f3819ad1ae9f4 100644 --- a/packages/aws-cdk-lib/aws-sns/README.md +++ b/packages/aws-cdk-lib/aws-sns/README.md @@ -364,3 +364,127 @@ const topic = new sns.Topic(this, 'MyTopic', { ``` **Note**: The `fifoThroughputScope` property is only available for FIFO topics. + +## Message Data Protection + +Amazon SNS Message Data Protection allows you to define policies to detect and handle sensitive data in your SNS messages. You can configure policies to: + +- **Audit**: Log sensitive data detections to CloudWatch Logs, S3, or Firehose +- **Mask**: Replace sensitive data with specified characters (e.g., '*' or '#') +- **Redact**: Remove sensitive data completely +- **Deny**: Block messages containing sensitive data + +### Creating a Data Protection Policy + +You can attach a data protection policy to your topic: + +```ts +import { + DataProtectionPolicy, + DataProtectionPolicyStatement, + DataDirection, + MaskOperation, + AuditOperation, + PersonalIdentifiers, + FinancialIdentifiers, + CustomDataIdentifier, +} from 'aws-cdk-lib/aws-sns'; +import * as logs from 'aws-cdk-lib/aws-logs'; + +// Create a CloudWatch log group for audit findings +const logGroup = new logs.LogGroup(this, 'DataProtectionLogGroup', { + logGroupName: '/aws/vendedlogs/sns-data-protection', +}); + +// Create custom data identifiers +const employeeIdPattern = new CustomDataIdentifier({ + name: 'EmployeeID', + regex: 'EMP-[0-9]{6}', +}); + +// Create an SNS topic with data protection policy +const topic = new sns.Topic(this, 'ProtectedTopic', { + dataProtectionPolicy: new DataProtectionPolicy({ + name: 'SensitiveDataProtectionPolicy', + description: 'Policy to detect and handle sensitive data in messages', + statements: [ + // Audit statement for PII data + new DataProtectionPolicyStatement({ + sid: 'AuditPII', + dataDirection: DataDirection.INBOUND, + dataIdentifiers: [ + PersonalIdentifiers.NAME, + PersonalIdentifiers.EMAIL_ADDRESS, + PersonalIdentifiers.ADDRESS, + ], + operation: new AuditOperation({ + sampleRate: 99, + findingsDestination: { + cloudWatchLogs: { + logGroup: logGroup.logGroupName, + }, + }, + }), + }), + + // Mask financial information + new DataProtectionPolicyStatement({ + sid: 'MaskFinancialData', + dataDirection: DataDirection.INBOUND, + dataIdentifiers: [ + FinancialIdentifiers.CREDIT_CARD_NUMBER, + FinancialIdentifiers.CREDIT_CARD_EXPIRATION, + ], + operation: new MaskOperation({ + maskWithCharacter: '#', + }), + }), + ], + }), +}); +``` + +### AWS Managed Data Identifiers + +AWS provides many built-in managed data identifiers for common sensitive data patterns: + +```ts +// Personal information identifiers +PersonalIdentifiers.NAME // Full names +PersonalIdentifiers.EMAIL_ADDRESS // Email addresses +PersonalIdentifiers.ADDRESS // Postal addresses +PersonalIdentifiers.phoneNumber('US') // Phone numbers (country-specific) +PersonalIdentifiers.driversLicense('US') // Driver's license numbers +PersonalIdentifiers.passportNumber('US') // Passport numbers +PersonalIdentifiers.ssn('US') // Social Security Numbers + +// Financial information identifiers +FinancialIdentifiers.CREDIT_CARD_NUMBER // Credit card numbers +FinancialIdentifiers.CREDIT_CARD_EXPIRATION // Expiration dates +FinancialIdentifiers.CREDIT_CARD_CVV // Security codes (CVV) +FinancialIdentifiers.bankAccountNumber('US') // Bank account numbers + +// Credentials identifiers +CredentialsIdentifiers.AWS_SECRET_KEY // AWS secret access keys +CredentialsIdentifiers.PRIVATE_KEY // Private keys +``` + +### Custom Data Identifiers + +You can define custom data identifiers using regular expressions: + +```ts +// Custom pattern for employee IDs +const employeeIdPattern = new CustomDataIdentifier({ + name: 'EmployeeID', + regex: 'EMP-[0-9]{6}', +}); + +// Custom pattern for internal project codes +const projectCodePattern = new CustomDataIdentifier({ + name: 'ProjectCode', + regex: 'PROJ-[A-Z]{3}-[0-9]{4}', +}); +``` + +For more information about SNS Message Data Protection, see [the AWS documentation](https://docs.aws.amazon.com/sns/latest/dg/message-data-protection.html). From 5a8724038a73d42de42110d8ad21089143ee9214 Mon Sep 17 00:00:00 2001 From: ykethan Date: Fri, 6 Jun 2025 13:04:00 -0400 Subject: [PATCH 5/6] fix readme --- packages/aws-cdk-lib/aws-sns/README.md | 34 ++++++++++++++------------ 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/aws-cdk-lib/aws-sns/README.md b/packages/aws-cdk-lib/aws-sns/README.md index f3819ad1ae9f4..78855c1dd4ad5 100644 --- a/packages/aws-cdk-lib/aws-sns/README.md +++ b/packages/aws-cdk-lib/aws-sns/README.md @@ -450,23 +450,26 @@ AWS provides many built-in managed data identifiers for common sensitive data pa ```ts // Personal information identifiers -PersonalIdentifiers.NAME // Full names -PersonalIdentifiers.EMAIL_ADDRESS // Email addresses -PersonalIdentifiers.ADDRESS // Postal addresses -PersonalIdentifiers.phoneNumber('US') // Phone numbers (country-specific) -PersonalIdentifiers.driversLicense('US') // Driver's license numbers -PersonalIdentifiers.passportNumber('US') // Passport numbers -PersonalIdentifiers.ssn('US') // Social Security Numbers +declare const personalIdentifiers: sns.PersonalIdentifiers; +personalIdentifiers.NAME; // Full names +personalIdentifiers.EMAIL_ADDRESS; // Email addresses +personalIdentifiers.ADDRESS; // Postal addresses +personalIdentifiers.phoneNumber('US'); // Phone numbers (country-specific) +personalIdentifiers.driversLicense('US'); // Driver's license numbers +personalIdentifiers.passportNumber('US'); // Passport numbers +personalIdentifiers.ssn('US'); // Social Security Numbers // Financial information identifiers -FinancialIdentifiers.CREDIT_CARD_NUMBER // Credit card numbers -FinancialIdentifiers.CREDIT_CARD_EXPIRATION // Expiration dates -FinancialIdentifiers.CREDIT_CARD_CVV // Security codes (CVV) -FinancialIdentifiers.bankAccountNumber('US') // Bank account numbers +declare const financialIdentifiers: sns.FinancialIdentifiers; +financialIdentifiers.CREDIT_CARD_NUMBER; // Credit card numbers +financialIdentifiers.CREDIT_CARD_EXPIRATION; // Expiration dates +financialIdentifiers.CREDIT_CARD_CVV; // Security codes (CVV) +financialIdentifiers.bankAccountNumber('US'); // Bank account numbers // Credentials identifiers -CredentialsIdentifiers.AWS_SECRET_KEY // AWS secret access keys -CredentialsIdentifiers.PRIVATE_KEY // Private keys +declare const credentialsIdentifiers: sns.CredentialsIdentifiers; +credentialsIdentifiers.AWS_SECRET_KEY; // AWS secret access keys +credentialsIdentifiers.PRIVATE_KEY; // Private keys ``` ### Custom Data Identifiers @@ -475,13 +478,14 @@ You can define custom data identifiers using regular expressions: ```ts // Custom pattern for employee IDs -const employeeIdPattern = new CustomDataIdentifier({ +declare const customDataIdentifier: sns.CustomDataIdentifier; +const employeeIdPattern = new customDataIdentifier({ name: 'EmployeeID', regex: 'EMP-[0-9]{6}', }); // Custom pattern for internal project codes -const projectCodePattern = new CustomDataIdentifier({ +const projectCodePattern = new customDataIdentifier({ name: 'ProjectCode', regex: 'PROJ-[A-Z]{3}-[0-9]{4}', }); From 79b95f86292f194010ce2cc81eec324c1fc612dd Mon Sep 17 00:00:00 2001 From: ykethan Date: Fri, 6 Jun 2025 14:10:48 -0400 Subject: [PATCH 6/6] fix readme errors --- packages/aws-cdk-lib/aws-sns/README.md | 42 +++++++++++--------------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/packages/aws-cdk-lib/aws-sns/README.md b/packages/aws-cdk-lib/aws-sns/README.md index 78855c1dd4ad5..e993dd1c3be40 100644 --- a/packages/aws-cdk-lib/aws-sns/README.md +++ b/packages/aws-cdk-lib/aws-sns/README.md @@ -449,27 +449,26 @@ const topic = new sns.Topic(this, 'ProtectedTopic', { AWS provides many built-in managed data identifiers for common sensitive data patterns: ```ts +import { PersonalIdentifiers, FinancialIdentifiers, CredentialsIdentifiers } from 'aws-cdk-lib/aws-sns'; + // Personal information identifiers -declare const personalIdentifiers: sns.PersonalIdentifiers; -personalIdentifiers.NAME; // Full names -personalIdentifiers.EMAIL_ADDRESS; // Email addresses -personalIdentifiers.ADDRESS; // Postal addresses -personalIdentifiers.phoneNumber('US'); // Phone numbers (country-specific) -personalIdentifiers.driversLicense('US'); // Driver's license numbers -personalIdentifiers.passportNumber('US'); // Passport numbers -personalIdentifiers.ssn('US'); // Social Security Numbers +PersonalIdentifiers.NAME; // Full names +PersonalIdentifiers.EMAIL_ADDRESS; // Email addresses +PersonalIdentifiers.ADDRESS; // Postal addresses +PersonalIdentifiers.phoneNumber('US'); // Phone numbers (country-specific) +PersonalIdentifiers.driversLicense('US'); // Driver's license numbers +PersonalIdentifiers.passportNumber('US'); // Passport numbers +PersonalIdentifiers.ssn('US'); // Social Security Numbers // Financial information identifiers -declare const financialIdentifiers: sns.FinancialIdentifiers; -financialIdentifiers.CREDIT_CARD_NUMBER; // Credit card numbers -financialIdentifiers.CREDIT_CARD_EXPIRATION; // Expiration dates -financialIdentifiers.CREDIT_CARD_CVV; // Security codes (CVV) -financialIdentifiers.bankAccountNumber('US'); // Bank account numbers +FinancialIdentifiers.CREDIT_CARD_NUMBER; // Credit card numbers +FinancialIdentifiers.CREDIT_CARD_EXPIRATION; // Expiration dates +FinancialIdentifiers.CREDIT_CARD_CVV; // Security codes (CVV) +FinancialIdentifiers.bankAccountNumber('US'); // Bank account numbers // Credentials identifiers -declare const credentialsIdentifiers: sns.CredentialsIdentifiers; -credentialsIdentifiers.AWS_SECRET_KEY; // AWS secret access keys -credentialsIdentifiers.PRIVATE_KEY; // Private keys +CredentialsIdentifiers.AWS_SECRET_KEY; // AWS secret access keys +CredentialsIdentifiers.PRIVATE_KEY; // Private keys ``` ### Custom Data Identifiers @@ -477,18 +476,13 @@ credentialsIdentifiers.PRIVATE_KEY; // Private keys You can define custom data identifiers using regular expressions: ```ts +import { CustomDataIdentifier } from 'aws-cdk-lib/aws-sns'; + // Custom pattern for employee IDs -declare const customDataIdentifier: sns.CustomDataIdentifier; -const employeeIdPattern = new customDataIdentifier({ +new CustomDataIdentifier({ name: 'EmployeeID', regex: 'EMP-[0-9]{6}', }); - -// Custom pattern for internal project codes -const projectCodePattern = new customDataIdentifier({ - name: 'ProjectCode', - regex: 'PROJ-[A-Z]{3}-[0-9]{4}', -}); ``` For more information about SNS Message Data Protection, see [the AWS documentation](https://docs.aws.amazon.com/sns/latest/dg/message-data-protection.html).