aws-cis-controls-assessment 1.1.3__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. aws_cis_assessment/__init__.py +4 -4
  2. aws_cis_assessment/config/rules/cis_controls_ig1.yaml +365 -2
  3. aws_cis_assessment/controls/ig1/control_access_analyzer.py +198 -0
  4. aws_cis_assessment/controls/ig1/control_access_asset_mgmt.py +360 -0
  5. aws_cis_assessment/controls/ig1/control_access_control.py +323 -0
  6. aws_cis_assessment/controls/ig1/control_backup_security.py +579 -0
  7. aws_cis_assessment/controls/ig1/control_cloudfront_logging.py +215 -0
  8. aws_cis_assessment/controls/ig1/control_configuration_mgmt.py +407 -0
  9. aws_cis_assessment/controls/ig1/control_data_classification.py +255 -0
  10. aws_cis_assessment/controls/ig1/control_dynamodb_encryption.py +279 -0
  11. aws_cis_assessment/controls/ig1/control_ebs_encryption.py +177 -0
  12. aws_cis_assessment/controls/ig1/control_efs_encryption.py +243 -0
  13. aws_cis_assessment/controls/ig1/control_elb_logging.py +195 -0
  14. aws_cis_assessment/controls/ig1/control_guardduty.py +156 -0
  15. aws_cis_assessment/controls/ig1/control_inspector.py +184 -0
  16. aws_cis_assessment/controls/ig1/control_inventory.py +511 -0
  17. aws_cis_assessment/controls/ig1/control_macie.py +165 -0
  18. aws_cis_assessment/controls/ig1/control_messaging_encryption.py +419 -0
  19. aws_cis_assessment/controls/ig1/control_mfa.py +485 -0
  20. aws_cis_assessment/controls/ig1/control_network_security.py +194 -619
  21. aws_cis_assessment/controls/ig1/control_patch_management.py +626 -0
  22. aws_cis_assessment/controls/ig1/control_rds_encryption.py +228 -0
  23. aws_cis_assessment/controls/ig1/control_s3_encryption.py +383 -0
  24. aws_cis_assessment/controls/ig1/control_tls_ssl.py +556 -0
  25. aws_cis_assessment/controls/ig1/control_version_mgmt.py +329 -0
  26. aws_cis_assessment/controls/ig1/control_vpc_flow_logs.py +205 -0
  27. aws_cis_assessment/controls/ig1/control_waf_logging.py +226 -0
  28. aws_cis_assessment/core/models.py +20 -1
  29. aws_cis_assessment/core/scoring_engine.py +98 -1
  30. aws_cis_assessment/reporters/base_reporter.py +31 -1
  31. aws_cis_assessment/reporters/html_reporter.py +172 -11
  32. aws_cis_controls_assessment-1.2.0.dist-info/METADATA +320 -0
  33. {aws_cis_controls_assessment-1.1.3.dist-info → aws_cis_controls_assessment-1.2.0.dist-info}/RECORD +39 -15
  34. docs/developer-guide.md +204 -5
  35. docs/user-guide.md +137 -4
  36. aws_cis_controls_assessment-1.1.3.dist-info/METADATA +0 -404
  37. {aws_cis_controls_assessment-1.1.3.dist-info → aws_cis_controls_assessment-1.2.0.dist-info}/WHEEL +0 -0
  38. {aws_cis_controls_assessment-1.1.3.dist-info → aws_cis_controls_assessment-1.2.0.dist-info}/entry_points.txt +0 -0
  39. {aws_cis_controls_assessment-1.1.3.dist-info → aws_cis_controls_assessment-1.2.0.dist-info}/licenses/LICENSE +0 -0
  40. {aws_cis_controls_assessment-1.1.3.dist-info → aws_cis_controls_assessment-1.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,255 @@
1
+ """
2
+ CIS Control 3.12 - Data Classification
3
+ Ensures proper data classification tagging across AWS resources.
4
+ """
5
+
6
+ import logging
7
+ from typing import List, Dict, Any
8
+ from botocore.exceptions import ClientError
9
+
10
+ from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
11
+ from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
12
+ from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class DataClassificationTaggingAssessment(BaseConfigRuleAssessment):
18
+ """
19
+ CIS Control 3.12 - Segment Data Processing and Storage Based on Sensitivity
20
+ AWS Config Rule: data-classification-tagging
21
+
22
+ Ensures data resources have proper classification tags.
23
+ """
24
+
25
+ def __init__(self):
26
+ super().__init__(
27
+ rule_name="data-classification-tagging",
28
+ control_id="3.12",
29
+ resource_types=["AWS::RDS::DBInstance", "AWS::DynamoDB::Table"]
30
+ )
31
+
32
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
33
+ """Get data resources and check for classification tags."""
34
+ resources = []
35
+
36
+ try:
37
+ if resource_type == "AWS::RDS::DBInstance":
38
+ rds_client = aws_factory.get_client('rds', region)
39
+ response = rds_client.describe_db_instances()
40
+
41
+ for db in response.get('DBInstances', []):
42
+ tags = {tag['Key']: tag['Value'] for tag in db.get('TagList', [])}
43
+
44
+ has_classification = bool(
45
+ tags.get('DataClassification') or
46
+ tags.get('Sensitivity') or
47
+ tags.get('Compliance')
48
+ )
49
+
50
+ resources.append({
51
+ 'ResourceId': db.get('DBInstanceIdentifier'),
52
+ 'ResourceType': 'AWS::RDS::DBInstance',
53
+ 'Tags': tags,
54
+ 'HasClassification': has_classification
55
+ })
56
+
57
+ elif resource_type == "AWS::DynamoDB::Table":
58
+ dynamodb_client = aws_factory.get_client('dynamodb', region)
59
+ response = dynamodb_client.list_tables()
60
+
61
+ for table_name in response.get('TableNames', []):
62
+ try:
63
+ table_arn = f"arn:aws:dynamodb:{region}:*:table/{table_name}"
64
+ tags_response = dynamodb_client.list_tags_of_resource(ResourceArn=table_arn)
65
+ tags = {tag['Key']: tag['Value'] for tag in tags_response.get('Tags', [])}
66
+
67
+ has_classification = bool(
68
+ tags.get('DataClassification') or
69
+ tags.get('Sensitivity') or
70
+ tags.get('Compliance')
71
+ )
72
+
73
+ resources.append({
74
+ 'ResourceId': table_name,
75
+ 'ResourceType': 'AWS::DynamoDB::Table',
76
+ 'Tags': tags,
77
+ 'HasClassification': has_classification
78
+ })
79
+ except ClientError:
80
+ continue
81
+
82
+ return resources
83
+
84
+ except ClientError as e:
85
+ logger.error(f"Error retrieving {resource_type} in {region}: {e}")
86
+ return []
87
+
88
+ def _evaluate_resource_compliance(
89
+ self,
90
+ resource: Dict[str, Any],
91
+ aws_factory: AWSClientFactory,
92
+ region: str
93
+ ) -> ComplianceResult:
94
+ """Evaluate if resource has classification tags."""
95
+ resource_id = resource.get('ResourceId', 'unknown')
96
+ resource_type = resource.get('ResourceType', 'Unknown')
97
+ has_classification = resource.get('HasClassification', False)
98
+
99
+ if has_classification:
100
+ evaluation_reason = f"Resource {resource_id} has data classification tags"
101
+ compliance_status = ComplianceStatus.COMPLIANT
102
+ else:
103
+ evaluation_reason = f"Resource {resource_id} missing classification tags (DataClassification, Sensitivity, or Compliance)"
104
+ compliance_status = ComplianceStatus.NON_COMPLIANT
105
+
106
+ return ComplianceResult(
107
+ resource_id=resource_id,
108
+ resource_type=resource_type,
109
+ compliance_status=compliance_status,
110
+ evaluation_reason=evaluation_reason,
111
+ config_rule_name=self.rule_name,
112
+ region=region
113
+ )
114
+
115
+ def _get_rule_remediation_steps(self) -> List[str]:
116
+ """Get remediation steps for data classification tagging."""
117
+ return [
118
+ "1. Tag RDS instance with classification:",
119
+ " aws rds add-tags-to-resource \\",
120
+ " --resource-name <db-arn> \\",
121
+ " --tags Key=DataClassification,Value=confidential \\",
122
+ " Key=Sensitivity,Value=high \\",
123
+ " Key=Compliance,Value=pci-dss",
124
+ "",
125
+ "2. Tag DynamoDB table:",
126
+ " aws dynamodb tag-resource \\",
127
+ " --resource-arn <table-arn> \\",
128
+ " --tags Key=DataClassification,Value=confidential",
129
+ "",
130
+ "3. Recommended classification levels:",
131
+ " - Public: Non-sensitive data",
132
+ " - Internal: Internal use only",
133
+ " - Confidential: Sensitive business data",
134
+ " - Restricted: Highly sensitive (PII, PHI, PCI)",
135
+ "",
136
+ "Priority: HIGH - Critical for data governance",
137
+ "Effort: Low - Simple tagging operation",
138
+ "",
139
+ "AWS Documentation:",
140
+ "https://docs.aws.amazon.com/general/latest/gr/aws_tagging.html"
141
+ ]
142
+
143
+
144
+ class S3BucketClassificationTagsAssessment(BaseConfigRuleAssessment):
145
+ """
146
+ CIS Control 3.12 - Segment Data Processing and Storage Based on Sensitivity
147
+ AWS Config Rule: s3-bucket-classification-tags
148
+
149
+ Ensures S3 buckets have proper data classification tags.
150
+ """
151
+
152
+ def __init__(self):
153
+ super().__init__(
154
+ rule_name="s3-bucket-classification-tags",
155
+ control_id="3.12",
156
+ resource_types=["AWS::S3::Bucket"]
157
+ )
158
+
159
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
160
+ """Get S3 buckets and check for classification tags."""
161
+ if resource_type != "AWS::S3::Bucket":
162
+ return []
163
+
164
+ # S3 is global, only process in us-east-1
165
+ if region != 'us-east-1':
166
+ return []
167
+
168
+ try:
169
+ s3_client = aws_factory.get_client('s3', region)
170
+
171
+ response = s3_client.list_buckets()
172
+ buckets = []
173
+
174
+ for bucket in response.get('Buckets', []):
175
+ bucket_name = bucket.get('Name')
176
+
177
+ try:
178
+ tags_response = s3_client.get_bucket_tagging(Bucket=bucket_name)
179
+ tags = {tag['Key']: tag['Value'] for tag in tags_response.get('TagSet', [])}
180
+ except ClientError as e:
181
+ if e.response.get('Error', {}).get('Code') == 'NoSuchTagSet':
182
+ tags = {}
183
+ else:
184
+ continue
185
+
186
+ has_classification = bool(
187
+ tags.get('DataClassification') or
188
+ tags.get('Sensitivity') or
189
+ tags.get('Compliance')
190
+ )
191
+
192
+ buckets.append({
193
+ 'BucketName': bucket_name,
194
+ 'Tags': tags,
195
+ 'HasClassification': has_classification
196
+ })
197
+
198
+ return buckets
199
+
200
+ except ClientError as e:
201
+ logger.error(f"Error retrieving S3 buckets: {e}")
202
+ return []
203
+
204
+ def _evaluate_resource_compliance(
205
+ self,
206
+ resource: Dict[str, Any],
207
+ aws_factory: AWSClientFactory,
208
+ region: str
209
+ ) -> ComplianceResult:
210
+ """Evaluate if S3 bucket has classification tags."""
211
+ bucket_name = resource.get('BucketName', 'unknown')
212
+ has_classification = resource.get('HasClassification', False)
213
+
214
+ if has_classification:
215
+ evaluation_reason = f"S3 bucket '{bucket_name}' has data classification tags"
216
+ compliance_status = ComplianceStatus.COMPLIANT
217
+ else:
218
+ evaluation_reason = f"S3 bucket '{bucket_name}' missing classification tags (DataClassification, Sensitivity, or Compliance)"
219
+ compliance_status = ComplianceStatus.NON_COMPLIANT
220
+
221
+ return ComplianceResult(
222
+ resource_id=bucket_name,
223
+ resource_type="AWS::S3::Bucket",
224
+ compliance_status=compliance_status,
225
+ evaluation_reason=evaluation_reason,
226
+ config_rule_name=self.rule_name,
227
+ region=region
228
+ )
229
+
230
+ def _get_rule_remediation_steps(self) -> List[str]:
231
+ """Get remediation steps for S3 bucket classification."""
232
+ return [
233
+ "1. Tag S3 bucket with classification:",
234
+ " aws s3api put-bucket-tagging \\",
235
+ " --bucket <bucket-name> \\",
236
+ " --tagging 'TagSet=[{Key=DataClassification,Value=confidential},{Key=Sensitivity,Value=high}]'",
237
+ "",
238
+ "2. Tag multiple buckets (script):",
239
+ " for bucket in $(aws s3api list-buckets --query 'Buckets[].Name' --output text); do",
240
+ " aws s3api put-bucket-tagging --bucket $bucket \\",
241
+ " --tagging 'TagSet=[{Key=DataClassification,Value=internal}]'",
242
+ " done",
243
+ "",
244
+ "3. Classification guidelines:",
245
+ " - Public: Publicly accessible data",
246
+ " - Internal: Internal business data",
247
+ " - Confidential: Sensitive business information",
248
+ " - Restricted: Regulated data (PII, PHI, PCI, etc.)",
249
+ "",
250
+ "Priority: HIGH - Essential for data governance",
251
+ "Effort: Low - Simple tagging",
252
+ "",
253
+ "AWS Documentation:",
254
+ "https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-tagging.html"
255
+ ]
@@ -0,0 +1,279 @@
1
+ """
2
+ CIS Control 3.11 - DynamoDB Table Encryption with KMS
3
+ Ensures DynamoDB tables are encrypted with customer-managed KMS keys for data at rest protection.
4
+ """
5
+
6
+ import logging
7
+ from typing import List, Dict, Any
8
+ from botocore.exceptions import ClientError
9
+
10
+ from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
11
+ from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
12
+ from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class DynamoDBTableEncryptedKMSAssessment(BaseConfigRuleAssessment):
18
+ """
19
+ CIS Control 3.11 - Encrypt Sensitive Data at Rest
20
+ AWS Config Rule: dynamodb-table-encrypted-kms
21
+
22
+ Ensures DynamoDB tables are encrypted with customer-managed KMS keys.
23
+ While DynamoDB encrypts all tables by default with AWS owned keys,
24
+ using customer-managed KMS keys provides better control and auditability.
25
+ """
26
+
27
+ def __init__(self):
28
+ super().__init__(
29
+ rule_name="dynamodb-table-encrypted-kms",
30
+ control_id="3.11",
31
+ resource_types=["AWS::DynamoDB::Table"]
32
+ )
33
+
34
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
35
+ """Get all DynamoDB tables in the region."""
36
+ if resource_type != "AWS::DynamoDB::Table":
37
+ return []
38
+
39
+ try:
40
+ dynamodb_client = aws_factory.get_client('dynamodb', region)
41
+
42
+ tables = []
43
+
44
+ # List all tables
45
+ paginator = dynamodb_client.get_paginator('list_tables')
46
+ table_names = []
47
+
48
+ for page in paginator.paginate():
49
+ table_names.extend(page.get('TableNames', []))
50
+
51
+ # Get details for each table
52
+ for table_name in table_names:
53
+ try:
54
+ response = dynamodb_client.describe_table(TableName=table_name)
55
+ table = response.get('Table', {})
56
+
57
+ # Get SSE (Server-Side Encryption) description
58
+ sse_description = table.get('SSEDescription', {})
59
+ sse_status = sse_description.get('Status', 'DISABLED')
60
+ sse_type = sse_description.get('SSEType', '')
61
+ kms_key_arn = sse_description.get('KMSMasterKeyArn', '')
62
+
63
+ # Determine encryption type
64
+ # SSEType can be: KMS (customer-managed), DEFAULT (AWS owned), or empty
65
+ if sse_status == 'ENABLED' and sse_type == 'KMS':
66
+ encryption_type = 'KMS'
67
+ encrypted_with_kms = True
68
+ elif sse_status == 'ENABLED':
69
+ # Default encryption with AWS owned keys
70
+ encryption_type = 'DEFAULT'
71
+ encrypted_with_kms = False
72
+ else:
73
+ # Older tables might not have SSE explicitly enabled
74
+ # but DynamoDB encrypts all tables by default with AWS owned keys
75
+ encryption_type = 'DEFAULT'
76
+ encrypted_with_kms = False
77
+
78
+ tables.append({
79
+ 'TableName': table_name,
80
+ 'TableArn': table.get('TableArn', ''),
81
+ 'TableStatus': table.get('TableStatus', ''),
82
+ 'SSEStatus': sse_status,
83
+ 'SSEType': sse_type,
84
+ 'KMSMasterKeyArn': kms_key_arn,
85
+ 'EncryptionType': encryption_type,
86
+ 'EncryptedWithKMS': encrypted_with_kms,
87
+ 'ItemCount': table.get('ItemCount', 0),
88
+ 'TableSizeBytes': table.get('TableSizeBytes', 0)
89
+ })
90
+
91
+ except ClientError as e:
92
+ error_code = e.response.get('Error', {}).get('Code', '')
93
+ if error_code == 'ResourceNotFoundException':
94
+ logger.debug(f"Table {table_name} not found in {region} - may have been deleted")
95
+ else:
96
+ logger.warning(f"Error describing table {table_name}: {e}")
97
+ continue
98
+
99
+ logger.debug(f"Found {len(tables)} DynamoDB tables in {region}")
100
+ return tables
101
+
102
+ except ClientError as e:
103
+ error_code = e.response.get('Error', {}).get('Code', '')
104
+ if error_code == 'AccessDeniedException':
105
+ logger.warning(f"Access denied to list DynamoDB tables in {region}")
106
+ else:
107
+ logger.error(f"Error retrieving DynamoDB tables from {region}: {e}")
108
+ return []
109
+ except Exception as e:
110
+ logger.error(f"Unexpected error retrieving DynamoDB tables from {region}: {e}")
111
+ return []
112
+
113
+ def _evaluate_resource_compliance(
114
+ self,
115
+ resource: Dict[str, Any],
116
+ aws_factory: AWSClientFactory,
117
+ region: str
118
+ ) -> ComplianceResult:
119
+ """Evaluate if DynamoDB table is encrypted with customer-managed KMS key."""
120
+ table_name = resource.get('TableName', 'unknown')
121
+ encrypted_with_kms = resource.get('EncryptedWithKMS', False)
122
+ encryption_type = resource.get('EncryptionType', 'UNKNOWN')
123
+ kms_key_arn = resource.get('KMSMasterKeyArn', '')
124
+ table_status = resource.get('TableStatus', '')
125
+
126
+ # Check if table is encrypted with customer-managed KMS key
127
+ is_compliant = encrypted_with_kms
128
+
129
+ if is_compliant:
130
+ evaluation_reason = (
131
+ f"DynamoDB table '{table_name}' is encrypted with customer-managed KMS key: "
132
+ f"{kms_key_arn}"
133
+ )
134
+ compliance_status = ComplianceStatus.COMPLIANT
135
+ else:
136
+ if encryption_type == 'DEFAULT':
137
+ evaluation_reason = (
138
+ f"DynamoDB table '{table_name}' is encrypted with AWS owned keys (default encryption). "
139
+ f"Consider using customer-managed KMS keys for better control and auditability. "
140
+ f"Status: {table_status}"
141
+ )
142
+ else:
143
+ evaluation_reason = (
144
+ f"DynamoDB table '{table_name}' is not encrypted with customer-managed KMS key. "
145
+ f"Encryption type: {encryption_type}. Status: {table_status}"
146
+ )
147
+ compliance_status = ComplianceStatus.NON_COMPLIANT
148
+
149
+ return ComplianceResult(
150
+ resource_id=table_name,
151
+ resource_type="AWS::DynamoDB::Table",
152
+ compliance_status=compliance_status,
153
+ evaluation_reason=evaluation_reason,
154
+ config_rule_name=self.rule_name,
155
+ region=region
156
+ )
157
+
158
+ def _get_rule_remediation_steps(self) -> List[str]:
159
+ """Get remediation steps for enabling DynamoDB KMS encryption."""
160
+ return [
161
+ "1. IMPORTANT: You CAN enable KMS encryption on an existing table.",
162
+ " However, this requires updating the table and may cause brief unavailability.",
163
+ "",
164
+ "2. Enable KMS encryption on an existing table using AWS CLI:",
165
+ " aws dynamodb update-table \\",
166
+ " --table-name <table-name> \\",
167
+ " --sse-specification Enabled=true,SSEType=KMS,KMSMasterKeyId=<kms-key-id> \\",
168
+ " --region <region>",
169
+ "",
170
+ "3. Wait for the table update to complete:",
171
+ " aws dynamodb describe-table \\",
172
+ " --table-name <table-name> \\",
173
+ " --query 'Table.TableStatus' \\",
174
+ " --output text \\",
175
+ " --region <region>",
176
+ "",
177
+ " # Status should be 'ACTIVE' when complete",
178
+ "",
179
+ "4. Verify KMS encryption is enabled:",
180
+ " aws dynamodb describe-table \\",
181
+ " --table-name <table-name> \\",
182
+ " --query 'Table.SSEDescription' \\",
183
+ " --region <region>",
184
+ "",
185
+ "5. For NEW tables, enable KMS encryption at creation:",
186
+ " aws dynamodb create-table \\",
187
+ " --table-name <table-name> \\",
188
+ " --attribute-definitions AttributeName=id,AttributeType=S \\",
189
+ " --key-schema AttributeName=id,KeyType=HASH \\",
190
+ " --billing-mode PAY_PER_REQUEST \\",
191
+ " --sse-specification Enabled=true,SSEType=KMS,KMSMasterKeyId=<kms-key-id> \\",
192
+ " --region <region>",
193
+ "",
194
+ "6. Console method for existing tables:",
195
+ " - Navigate to DynamoDB service",
196
+ " - Select the table",
197
+ " - Click 'Additional settings' tab",
198
+ " - Under 'Encryption at rest', click 'Manage encryption'",
199
+ " - Select 'AWS Key Management Service key (KMS)'",
200
+ " - Choose 'Select from your AWS KMS keys' or 'Enter KMS key ARN'",
201
+ " - Select or enter your KMS key",
202
+ " - Click 'Save changes'",
203
+ "",
204
+ "7. Create a customer-managed KMS key if you don't have one:",
205
+ " aws kms create-key \\",
206
+ " --description 'DynamoDB encryption key' \\",
207
+ " --key-policy file://key-policy.json \\",
208
+ " --region <region>",
209
+ "",
210
+ " # Create an alias for easier reference",
211
+ " aws kms create-alias \\",
212
+ " --alias-name alias/dynamodb-encryption \\",
213
+ " --target-key-id <key-id> \\",
214
+ " --region <region>",
215
+ "",
216
+ "8. Example KMS key policy (key-policy.json):",
217
+ " {",
218
+ ' "Version": "2012-10-17",',
219
+ ' "Statement": [',
220
+ " {",
221
+ ' "Sid": "Enable IAM User Permissions",',
222
+ ' "Effect": "Allow",',
223
+ ' "Principal": {"AWS": "arn:aws:iam::<account-id>:root"},',
224
+ ' "Action": "kms:*",',
225
+ ' "Resource": "*"',
226
+ " },",
227
+ " {",
228
+ ' "Sid": "Allow DynamoDB to use the key",',
229
+ ' "Effect": "Allow",',
230
+ ' "Principal": {"Service": "dynamodb.amazonaws.com"},',
231
+ ' "Action": ["kms:Decrypt", "kms:DescribeKey", "kms:CreateGrant"],',
232
+ ' "Resource": "*",',
233
+ ' "Condition": {',
234
+ ' "StringEquals": {"kms:ViaService": "dynamodb.<region>.amazonaws.com"}',
235
+ " }",
236
+ " }",
237
+ " ]",
238
+ " }",
239
+ "",
240
+ "9. Best practices:",
241
+ " - Use customer-managed KMS keys for all production tables",
242
+ " - Enable automatic key rotation for KMS keys",
243
+ " - Grant appropriate IAM permissions for KMS key usage",
244
+ " - Use separate KMS keys for different environments (dev/staging/prod)",
245
+ " - Document the KMS key used for each table",
246
+ " - Monitor KMS key usage with CloudWatch",
247
+ " - Set up CloudTrail logging for KMS key operations",
248
+ "",
249
+ "10. Important notes:",
250
+ " - Encryption can be changed from AWS owned to KMS without data migration",
251
+ " - Changing encryption type may cause brief table unavailability",
252
+ " - Once KMS encryption is enabled, you can change the KMS key",
253
+ " - You cannot disable encryption (only change the key type)",
254
+ " - Global tables must use the same encryption type across all regions",
255
+ " - Backups inherit encryption from source table",
256
+ " - Point-in-time recovery (PITR) backups use the same encryption",
257
+ " - Performance impact is negligible",
258
+ " - KMS key costs apply (per API call pricing)",
259
+ "",
260
+ "11. Cost considerations:",
261
+ " - AWS owned keys: No additional cost",
262
+ " - Customer-managed KMS keys: $1/month per key + API call costs",
263
+ " - API calls are typically minimal for DynamoDB encryption",
264
+ " - Consider using one KMS key for multiple tables to reduce costs",
265
+ "",
266
+ "12. Verify encryption after update:",
267
+ " # Check SSE status",
268
+ " aws dynamodb describe-table \\",
269
+ " --table-name <table-name> \\",
270
+ " --query 'Table.SSEDescription.{Status:Status,Type:SSEType,Key:KMSMasterKeyArn}' \\",
271
+ " --output table \\",
272
+ " --region <region>",
273
+ "",
274
+ "Priority: MEDIUM - KMS encryption provides better control but AWS owned keys still encrypt data",
275
+ "Effort: Low - Can be enabled with a single API call, minimal downtime",
276
+ "",
277
+ "AWS Documentation:",
278
+ "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/EncryptionAtRest.html"
279
+ ]