aws-cis-controls-assessment 1.0.3__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 (77) hide show
  1. aws_cis_assessment/__init__.py +11 -0
  2. aws_cis_assessment/cli/__init__.py +3 -0
  3. aws_cis_assessment/cli/examples.py +274 -0
  4. aws_cis_assessment/cli/main.py +1259 -0
  5. aws_cis_assessment/cli/utils.py +356 -0
  6. aws_cis_assessment/config/__init__.py +1 -0
  7. aws_cis_assessment/config/config_loader.py +328 -0
  8. aws_cis_assessment/config/rules/cis_controls_ig1.yaml +590 -0
  9. aws_cis_assessment/config/rules/cis_controls_ig2.yaml +412 -0
  10. aws_cis_assessment/config/rules/cis_controls_ig3.yaml +100 -0
  11. aws_cis_assessment/controls/__init__.py +1 -0
  12. aws_cis_assessment/controls/base_control.py +400 -0
  13. aws_cis_assessment/controls/ig1/__init__.py +239 -0
  14. aws_cis_assessment/controls/ig1/control_1_1.py +586 -0
  15. aws_cis_assessment/controls/ig1/control_2_2.py +231 -0
  16. aws_cis_assessment/controls/ig1/control_3_3.py +718 -0
  17. aws_cis_assessment/controls/ig1/control_3_4.py +235 -0
  18. aws_cis_assessment/controls/ig1/control_4_1.py +461 -0
  19. aws_cis_assessment/controls/ig1/control_access_keys.py +310 -0
  20. aws_cis_assessment/controls/ig1/control_advanced_security.py +512 -0
  21. aws_cis_assessment/controls/ig1/control_backup_recovery.py +510 -0
  22. aws_cis_assessment/controls/ig1/control_cloudtrail_logging.py +197 -0
  23. aws_cis_assessment/controls/ig1/control_critical_security.py +422 -0
  24. aws_cis_assessment/controls/ig1/control_data_protection.py +898 -0
  25. aws_cis_assessment/controls/ig1/control_iam_advanced.py +573 -0
  26. aws_cis_assessment/controls/ig1/control_iam_governance.py +493 -0
  27. aws_cis_assessment/controls/ig1/control_iam_policies.py +383 -0
  28. aws_cis_assessment/controls/ig1/control_instance_optimization.py +100 -0
  29. aws_cis_assessment/controls/ig1/control_network_enhancements.py +203 -0
  30. aws_cis_assessment/controls/ig1/control_network_security.py +672 -0
  31. aws_cis_assessment/controls/ig1/control_s3_enhancements.py +173 -0
  32. aws_cis_assessment/controls/ig1/control_s3_security.py +422 -0
  33. aws_cis_assessment/controls/ig1/control_vpc_security.py +235 -0
  34. aws_cis_assessment/controls/ig2/__init__.py +172 -0
  35. aws_cis_assessment/controls/ig2/control_3_10.py +698 -0
  36. aws_cis_assessment/controls/ig2/control_3_11.py +1330 -0
  37. aws_cis_assessment/controls/ig2/control_5_2.py +393 -0
  38. aws_cis_assessment/controls/ig2/control_advanced_encryption.py +355 -0
  39. aws_cis_assessment/controls/ig2/control_codebuild_security.py +263 -0
  40. aws_cis_assessment/controls/ig2/control_encryption_rest.py +382 -0
  41. aws_cis_assessment/controls/ig2/control_encryption_transit.py +382 -0
  42. aws_cis_assessment/controls/ig2/control_network_ha.py +467 -0
  43. aws_cis_assessment/controls/ig2/control_remaining_encryption.py +426 -0
  44. aws_cis_assessment/controls/ig2/control_remaining_rules.py +363 -0
  45. aws_cis_assessment/controls/ig2/control_service_logging.py +402 -0
  46. aws_cis_assessment/controls/ig3/__init__.py +49 -0
  47. aws_cis_assessment/controls/ig3/control_12_8.py +395 -0
  48. aws_cis_assessment/controls/ig3/control_13_1.py +467 -0
  49. aws_cis_assessment/controls/ig3/control_3_14.py +523 -0
  50. aws_cis_assessment/controls/ig3/control_7_1.py +359 -0
  51. aws_cis_assessment/core/__init__.py +1 -0
  52. aws_cis_assessment/core/accuracy_validator.py +425 -0
  53. aws_cis_assessment/core/assessment_engine.py +1266 -0
  54. aws_cis_assessment/core/audit_trail.py +491 -0
  55. aws_cis_assessment/core/aws_client_factory.py +313 -0
  56. aws_cis_assessment/core/error_handler.py +607 -0
  57. aws_cis_assessment/core/models.py +166 -0
  58. aws_cis_assessment/core/scoring_engine.py +459 -0
  59. aws_cis_assessment/reporters/__init__.py +8 -0
  60. aws_cis_assessment/reporters/base_reporter.py +454 -0
  61. aws_cis_assessment/reporters/csv_reporter.py +835 -0
  62. aws_cis_assessment/reporters/html_reporter.py +2162 -0
  63. aws_cis_assessment/reporters/json_reporter.py +561 -0
  64. aws_cis_controls_assessment-1.0.3.dist-info/METADATA +248 -0
  65. aws_cis_controls_assessment-1.0.3.dist-info/RECORD +77 -0
  66. aws_cis_controls_assessment-1.0.3.dist-info/WHEEL +5 -0
  67. aws_cis_controls_assessment-1.0.3.dist-info/entry_points.txt +2 -0
  68. aws_cis_controls_assessment-1.0.3.dist-info/licenses/LICENSE +21 -0
  69. aws_cis_controls_assessment-1.0.3.dist-info/top_level.txt +2 -0
  70. docs/README.md +94 -0
  71. docs/assessment-logic.md +766 -0
  72. docs/cli-reference.md +698 -0
  73. docs/config-rule-mappings.md +393 -0
  74. docs/developer-guide.md +858 -0
  75. docs/installation.md +299 -0
  76. docs/troubleshooting.md +634 -0
  77. docs/user-guide.md +487 -0
@@ -0,0 +1,1330 @@
1
+ """Control 3.11: Encrypt Sensitive Data at Rest assessments."""
2
+
3
+ from typing import Dict, List, Any
4
+ import logging
5
+ import json
6
+ from botocore.exceptions import ClientError
7
+
8
+ from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
9
+ from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
10
+ from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class EncryptedVolumesAssessment(BaseConfigRuleAssessment):
16
+ """Assessment for encrypted-volumes Config rule - ensures EBS volumes are encrypted."""
17
+
18
+ def __init__(self):
19
+ """Initialize encrypted volumes assessment."""
20
+ super().__init__(
21
+ rule_name="encrypted-volumes",
22
+ control_id="3.11",
23
+ resource_types=["AWS::EC2::Volume"]
24
+ )
25
+
26
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
27
+ """Get all EBS volumes in the region."""
28
+ if resource_type != "AWS::EC2::Volume":
29
+ return []
30
+
31
+ try:
32
+ ec2_client = aws_factory.get_client('ec2', region)
33
+
34
+ response = aws_factory.aws_api_call_with_retry(
35
+ lambda: ec2_client.describe_volumes()
36
+ )
37
+
38
+ volumes = []
39
+ for volume in response.get('Volumes', []):
40
+ volumes.append({
41
+ 'VolumeId': volume.get('VolumeId'),
42
+ 'VolumeType': volume.get('VolumeType'),
43
+ 'Size': volume.get('Size'),
44
+ 'State': volume.get('State'),
45
+ 'Encrypted': volume.get('Encrypted', False),
46
+ 'KmsKeyId': volume.get('KmsKeyId'),
47
+ 'Attachments': volume.get('Attachments', []),
48
+ 'Tags': volume.get('Tags', [])
49
+ })
50
+
51
+ logger.debug(f"Found {len(volumes)} EBS volumes in region {region}")
52
+ return volumes
53
+
54
+ except ClientError as e:
55
+ logger.error(f"Error retrieving EBS volumes in region {region}: {e}")
56
+ raise
57
+ except Exception as e:
58
+ logger.error(f"Unexpected error retrieving EBS volumes in region {region}: {e}")
59
+ raise
60
+
61
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
62
+ """Evaluate if EBS volume is encrypted."""
63
+ volume_id = resource.get('VolumeId', 'unknown')
64
+ volume_type = resource.get('VolumeType', 'unknown')
65
+ volume_state = resource.get('State', 'unknown')
66
+ is_encrypted = resource.get('Encrypted', False)
67
+ kms_key_id = resource.get('KmsKeyId')
68
+
69
+ # Skip volumes that are being deleted
70
+ if volume_state in ['deleting', 'deleted']:
71
+ return ComplianceResult(
72
+ resource_id=volume_id,
73
+ resource_type="AWS::EC2::Volume",
74
+ compliance_status=ComplianceStatus.NOT_APPLICABLE,
75
+ evaluation_reason=f"EBS volume {volume_id} is in state '{volume_state}'",
76
+ config_rule_name=self.rule_name,
77
+ region=region
78
+ )
79
+
80
+ # Evaluate encryption status
81
+ if is_encrypted:
82
+ compliance_status = ComplianceStatus.COMPLIANT
83
+ if kms_key_id:
84
+ evaluation_reason = f"EBS volume {volume_id} ({volume_type}) is encrypted with KMS key {kms_key_id}"
85
+ else:
86
+ evaluation_reason = f"EBS volume {volume_id} ({volume_type}) is encrypted with default key"
87
+ else:
88
+ compliance_status = ComplianceStatus.NON_COMPLIANT
89
+ evaluation_reason = f"EBS volume {volume_id} ({volume_type}) is not encrypted"
90
+
91
+ return ComplianceResult(
92
+ resource_id=volume_id,
93
+ resource_type="AWS::EC2::Volume",
94
+ compliance_status=compliance_status,
95
+ evaluation_reason=evaluation_reason,
96
+ config_rule_name=self.rule_name,
97
+ region=region
98
+ )
99
+
100
+ def _get_rule_remediation_steps(self) -> List[str]:
101
+ """Get specific remediation steps for EBS volume encryption."""
102
+ return [
103
+ "Identify unencrypted EBS volumes",
104
+ "For each unencrypted volume:",
105
+ " 1. Create a snapshot of the unencrypted volume",
106
+ " 2. Create an encrypted copy of the snapshot",
107
+ " 3. Create a new encrypted volume from the encrypted snapshot",
108
+ " 4. Stop the instance and detach the unencrypted volume",
109
+ " 5. Attach the new encrypted volume to the instance",
110
+ " 6. Start the instance and verify functionality",
111
+ "Use AWS CLI: aws ec2 create-snapshot --volume-id <volume-id> --description 'Snapshot for encryption'",
112
+ "Copy with encryption: aws ec2 copy-snapshot --source-region <region> --source-snapshot-id <snapshot-id> --encrypted --kms-key-id <key-id>",
113
+ "Create encrypted volume: aws ec2 create-volume --snapshot-id <encrypted-snapshot-id> --availability-zone <az>",
114
+ "Enable EBS encryption by default: aws ec2 enable-ebs-encryption-by-default",
115
+ "Set default KMS key: aws ec2 modify-ebs-default-kms-key-id --kms-key-id <key-id>"
116
+ ]
117
+
118
+
119
+ class RDSStorageEncryptedAssessment(BaseConfigRuleAssessment):
120
+ """Assessment for rds-storage-encrypted Config rule - ensures RDS instances have storage encryption."""
121
+
122
+ def __init__(self):
123
+ """Initialize RDS storage encrypted assessment."""
124
+ super().__init__(
125
+ rule_name="rds-storage-encrypted",
126
+ control_id="3.11",
127
+ resource_types=["AWS::RDS::DBInstance"]
128
+ )
129
+
130
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
131
+ """Get all RDS instances in the region."""
132
+ if resource_type != "AWS::RDS::DBInstance":
133
+ return []
134
+
135
+ try:
136
+ rds_client = aws_factory.get_client('rds', region)
137
+
138
+ response = aws_factory.aws_api_call_with_retry(
139
+ lambda: rds_client.describe_db_instances()
140
+ )
141
+
142
+ instances = []
143
+ for instance in response.get('DBInstances', []):
144
+ instances.append({
145
+ 'DBInstanceIdentifier': instance.get('DBInstanceIdentifier'),
146
+ 'DBInstanceClass': instance.get('DBInstanceClass'),
147
+ 'Engine': instance.get('Engine'),
148
+ 'EngineVersion': instance.get('EngineVersion'),
149
+ 'DBInstanceStatus': instance.get('DBInstanceStatus'),
150
+ 'StorageEncrypted': instance.get('StorageEncrypted', False),
151
+ 'KmsKeyId': instance.get('KmsKeyId'),
152
+ 'AllocatedStorage': instance.get('AllocatedStorage'),
153
+ 'StorageType': instance.get('StorageType'),
154
+ 'TagList': instance.get('TagList', [])
155
+ })
156
+
157
+ logger.debug(f"Found {len(instances)} RDS instances in region {region}")
158
+ return instances
159
+
160
+ except ClientError as e:
161
+ logger.error(f"Error retrieving RDS instances in region {region}: {e}")
162
+ raise
163
+ except Exception as e:
164
+ logger.error(f"Unexpected error retrieving RDS instances in region {region}: {e}")
165
+ raise
166
+
167
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
168
+ """Evaluate if RDS instance has storage encryption enabled."""
169
+ db_identifier = resource.get('DBInstanceIdentifier', 'unknown')
170
+ db_class = resource.get('DBInstanceClass', 'unknown')
171
+ engine = resource.get('Engine', 'unknown')
172
+ db_status = resource.get('DBInstanceStatus', 'unknown')
173
+ storage_encrypted = resource.get('StorageEncrypted', False)
174
+ kms_key_id = resource.get('KmsKeyId')
175
+
176
+ # Skip instances that are not available
177
+ if db_status not in ['available', 'backing-up', 'maintenance']:
178
+ return ComplianceResult(
179
+ resource_id=db_identifier,
180
+ resource_type="AWS::RDS::DBInstance",
181
+ compliance_status=ComplianceStatus.NOT_APPLICABLE,
182
+ evaluation_reason=f"RDS instance {db_identifier} is in status '{db_status}'",
183
+ config_rule_name=self.rule_name,
184
+ region=region
185
+ )
186
+
187
+ # Evaluate encryption status
188
+ if storage_encrypted:
189
+ compliance_status = ComplianceStatus.COMPLIANT
190
+ if kms_key_id:
191
+ evaluation_reason = f"RDS instance {db_identifier} ({engine}) has storage encryption enabled with KMS key {kms_key_id}"
192
+ else:
193
+ evaluation_reason = f"RDS instance {db_identifier} ({engine}) has storage encryption enabled with default key"
194
+ else:
195
+ compliance_status = ComplianceStatus.NON_COMPLIANT
196
+ evaluation_reason = f"RDS instance {db_identifier} ({engine}) does not have storage encryption enabled"
197
+
198
+ return ComplianceResult(
199
+ resource_id=db_identifier,
200
+ resource_type="AWS::RDS::DBInstance",
201
+ compliance_status=compliance_status,
202
+ evaluation_reason=evaluation_reason,
203
+ config_rule_name=self.rule_name,
204
+ region=region
205
+ )
206
+
207
+ def _get_rule_remediation_steps(self) -> List[str]:
208
+ """Get specific remediation steps for RDS storage encryption."""
209
+ return [
210
+ "Identify RDS instances without storage encryption",
211
+ "For each unencrypted RDS instance:",
212
+ " 1. Create a snapshot of the unencrypted instance",
213
+ " 2. Create an encrypted copy of the snapshot",
214
+ " 3. Restore a new RDS instance from the encrypted snapshot",
215
+ " 4. Update applications to use the new encrypted instance",
216
+ " 5. Delete the old unencrypted instance after verification",
217
+ "Use AWS CLI: aws rds create-db-snapshot --db-instance-identifier <instance-id> --db-snapshot-identifier <snapshot-id>",
218
+ "Copy with encryption: aws rds copy-db-snapshot --source-db-snapshot-identifier <snapshot-id> --target-db-snapshot-identifier <encrypted-snapshot-id> --kms-key-id <key-id>",
219
+ "Restore encrypted: aws rds restore-db-instance-from-db-snapshot --db-instance-identifier <new-instance-id> --db-snapshot-identifier <encrypted-snapshot-id>",
220
+ "For new instances, enable encryption during creation: --storage-encrypted --kms-key-id <key-id>",
221
+ "Update RDS parameter groups to enforce encryption where applicable"
222
+ ]
223
+
224
+
225
+ class S3DefaultEncryptionKMSAssessment(BaseConfigRuleAssessment):
226
+ """Assessment for s3-default-encryption-kms Config rule - ensures S3 buckets have default KMS encryption."""
227
+
228
+ def __init__(self):
229
+ """Initialize S3 default encryption KMS assessment."""
230
+ super().__init__(
231
+ rule_name="s3-default-encryption-kms",
232
+ control_id="3.11",
233
+ resource_types=["AWS::S3::Bucket"]
234
+ )
235
+
236
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
237
+ """Get all S3 buckets (S3 is global but we'll check from this region)."""
238
+ if resource_type != "AWS::S3::Bucket":
239
+ return []
240
+
241
+ try:
242
+ s3_client = aws_factory.get_client('s3', region)
243
+
244
+ response = aws_factory.aws_api_call_with_retry(
245
+ lambda: s3_client.list_buckets()
246
+ )
247
+
248
+ buckets = []
249
+ for bucket in response.get('Buckets', []):
250
+ bucket_name = bucket.get('Name')
251
+
252
+ # Get bucket location to determine if we should evaluate it from this region
253
+ try:
254
+ location_response = aws_factory.aws_api_call_with_retry(
255
+ lambda: s3_client.get_bucket_location(Bucket=bucket_name)
256
+ )
257
+ bucket_region = location_response.get('LocationConstraint')
258
+
259
+ # Handle special case where us-east-1 returns None
260
+ if bucket_region is None:
261
+ bucket_region = 'us-east-1'
262
+
263
+ # Only include buckets in this region or if we're in us-east-1 (global)
264
+ if bucket_region == region or region == 'us-east-1':
265
+ buckets.append({
266
+ 'Name': bucket_name,
267
+ 'CreationDate': bucket.get('CreationDate'),
268
+ 'Region': bucket_region
269
+ })
270
+
271
+ except ClientError as e:
272
+ # If we can't get bucket location, skip this bucket
273
+ logger.warning(f"Could not get location for bucket {bucket_name}: {e}")
274
+ continue
275
+
276
+ logger.debug(f"Found {len(buckets)} S3 buckets accessible from region {region}")
277
+ return buckets
278
+
279
+ except ClientError as e:
280
+ logger.error(f"Error retrieving S3 buckets from region {region}: {e}")
281
+ raise
282
+ except Exception as e:
283
+ logger.error(f"Unexpected error retrieving S3 buckets from region {region}: {e}")
284
+ raise
285
+
286
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
287
+ """Evaluate if S3 bucket has default KMS encryption enabled."""
288
+ bucket_name = resource.get('Name', 'unknown')
289
+ bucket_region = resource.get('Region', region)
290
+
291
+ try:
292
+ # Use the bucket's region for API calls
293
+ s3_client = aws_factory.get_client('s3', bucket_region)
294
+
295
+ # Get bucket encryption configuration
296
+ try:
297
+ encryption_response = aws_factory.aws_api_call_with_retry(
298
+ lambda: s3_client.get_bucket_encryption(Bucket=bucket_name)
299
+ )
300
+
301
+ server_side_encryption_config = encryption_response.get('ServerSideEncryptionConfiguration', {})
302
+ rules = server_side_encryption_config.get('Rules', [])
303
+
304
+ has_kms_encryption = False
305
+ encryption_details = []
306
+
307
+ for rule in rules:
308
+ apply_server_side_encryption = rule.get('ApplyServerSideEncryptionByDefault', {})
309
+ sse_algorithm = apply_server_side_encryption.get('SSEAlgorithm', '')
310
+ kms_master_key_id = apply_server_side_encryption.get('KMSMasterKeyID', '')
311
+
312
+ encryption_details.append(f"{sse_algorithm}")
313
+
314
+ if sse_algorithm == 'aws:kms':
315
+ has_kms_encryption = True
316
+
317
+ if has_kms_encryption:
318
+ compliance_status = ComplianceStatus.COMPLIANT
319
+ evaluation_reason = f"S3 bucket {bucket_name} has default KMS encryption enabled ({', '.join(encryption_details)})"
320
+ else:
321
+ compliance_status = ComplianceStatus.NON_COMPLIANT
322
+ if encryption_details:
323
+ evaluation_reason = f"S3 bucket {bucket_name} has encryption but not KMS ({', '.join(encryption_details)})"
324
+ else:
325
+ evaluation_reason = f"S3 bucket {bucket_name} has encryption configured but no KMS encryption found"
326
+
327
+ except ClientError as e:
328
+ error_code = e.response.get('Error', {}).get('Code', '')
329
+ if error_code == 'ServerSideEncryptionConfigurationNotFoundError':
330
+ compliance_status = ComplianceStatus.NON_COMPLIANT
331
+ evaluation_reason = f"S3 bucket {bucket_name} has no default encryption configured"
332
+ elif error_code in ['AccessDenied', 'UnauthorizedOperation']:
333
+ compliance_status = ComplianceStatus.ERROR
334
+ evaluation_reason = f"Insufficient permissions to check encryption for bucket {bucket_name}"
335
+ else:
336
+ compliance_status = ComplianceStatus.ERROR
337
+ evaluation_reason = f"Error checking encryption for bucket {bucket_name}: {str(e)}"
338
+
339
+ except Exception as e:
340
+ compliance_status = ComplianceStatus.ERROR
341
+ evaluation_reason = f"Unexpected error checking encryption for bucket {bucket_name}: {str(e)}"
342
+
343
+ return ComplianceResult(
344
+ resource_id=bucket_name,
345
+ resource_type="AWS::S3::Bucket",
346
+ compliance_status=compliance_status,
347
+ evaluation_reason=evaluation_reason,
348
+ config_rule_name=self.rule_name,
349
+ region=bucket_region
350
+ )
351
+
352
+ def _get_rule_remediation_steps(self) -> List[str]:
353
+ """Get specific remediation steps for S3 default KMS encryption."""
354
+ return [
355
+ "Identify S3 buckets without default KMS encryption",
356
+ "For each non-compliant bucket:",
357
+ " 1. Create or identify a KMS key for S3 encryption",
358
+ " 2. Configure default encryption with KMS for the bucket",
359
+ " 3. Verify that new objects are encrypted with KMS",
360
+ "Use AWS CLI: aws s3api put-bucket-encryption --bucket <bucket-name> --server-side-encryption-configuration '{\"Rules\":[{\"ApplyServerSideEncryptionByDefault\":{\"SSEAlgorithm\":\"aws:kms\",\"KMSMasterKeyID\":\"<key-id>\"}}]}'",
361
+ "Create KMS key if needed: aws kms create-key --description 'S3 bucket encryption key'",
362
+ "Set bucket key for cost optimization: --server-side-encryption-configuration '{\"Rules\":[{\"ApplyServerSideEncryptionByDefault\":{\"SSEAlgorithm\":\"aws:kms\",\"KMSMasterKeyID\":\"<key-id>\"},\"BucketKeyEnabled\":true}]}'",
363
+ "Update bucket policies to deny unencrypted uploads",
364
+ "Test encryption by uploading objects and verifying encryption status"
365
+ ]
366
+
367
+
368
+ class DynamoDBTableEncryptedKMSAssessment(BaseConfigRuleAssessment):
369
+ """Assessment for dynamodb-table-encrypted-kms Config rule - ensures DynamoDB tables are encrypted with KMS."""
370
+
371
+ def __init__(self):
372
+ """Initialize DynamoDB table encrypted KMS assessment."""
373
+ super().__init__(
374
+ rule_name="dynamodb-table-encrypted-kms",
375
+ control_id="3.11",
376
+ resource_types=["AWS::DynamoDB::Table"]
377
+ )
378
+
379
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
380
+ """Get all DynamoDB tables in the region."""
381
+ if resource_type != "AWS::DynamoDB::Table":
382
+ return []
383
+
384
+ try:
385
+ dynamodb_client = aws_factory.get_client('dynamodb', region)
386
+
387
+ response = aws_factory.aws_api_call_with_retry(
388
+ lambda: dynamodb_client.list_tables()
389
+ )
390
+
391
+ table_names = response.get('TableNames', [])
392
+ tables = []
393
+
394
+ for table_name in table_names:
395
+ try:
396
+ # Get detailed table information
397
+ table_response = aws_factory.aws_api_call_with_retry(
398
+ lambda: dynamodb_client.describe_table(TableName=table_name)
399
+ )
400
+
401
+ table_info = table_response.get('Table', {})
402
+ tables.append({
403
+ 'TableName': table_info.get('TableName'),
404
+ 'TableStatus': table_info.get('TableStatus'),
405
+ 'CreationDateTime': table_info.get('CreationDateTime'),
406
+ 'SSEDescription': table_info.get('SSEDescription', {}),
407
+ 'BillingModeSummary': table_info.get('BillingModeSummary', {}),
408
+ 'TableSizeBytes': table_info.get('TableSizeBytes', 0)
409
+ })
410
+
411
+ except ClientError as e:
412
+ logger.warning(f"Could not describe table {table_name}: {e}")
413
+ continue
414
+
415
+ logger.debug(f"Found {len(tables)} DynamoDB tables in region {region}")
416
+ return tables
417
+
418
+ except ClientError as e:
419
+ logger.error(f"Error retrieving DynamoDB tables in region {region}: {e}")
420
+ raise
421
+ except Exception as e:
422
+ logger.error(f"Unexpected error retrieving DynamoDB tables in region {region}: {e}")
423
+ raise
424
+
425
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
426
+ """Evaluate if DynamoDB table is encrypted with KMS."""
427
+ table_name = resource.get('TableName', 'unknown')
428
+ table_status = resource.get('TableStatus', 'unknown')
429
+ sse_description = resource.get('SSEDescription', {})
430
+
431
+ # Skip tables that are not active
432
+ if table_status not in ['ACTIVE']:
433
+ return ComplianceResult(
434
+ resource_id=table_name,
435
+ resource_type="AWS::DynamoDB::Table",
436
+ compliance_status=ComplianceStatus.NOT_APPLICABLE,
437
+ evaluation_reason=f"DynamoDB table {table_name} is in status '{table_status}'",
438
+ config_rule_name=self.rule_name,
439
+ region=region
440
+ )
441
+
442
+ # Check encryption status
443
+ sse_status = sse_description.get('Status', 'DISABLED')
444
+ sse_type = sse_description.get('SSEType', '')
445
+ kms_master_key_arn = sse_description.get('KMSMasterKeyArn', '')
446
+
447
+ if sse_status == 'ENABLED':
448
+ if sse_type == 'KMS':
449
+ compliance_status = ComplianceStatus.COMPLIANT
450
+ if kms_master_key_arn:
451
+ evaluation_reason = f"DynamoDB table {table_name} is encrypted with KMS key {kms_master_key_arn}"
452
+ else:
453
+ evaluation_reason = f"DynamoDB table {table_name} is encrypted with KMS (default key)"
454
+ else:
455
+ compliance_status = ComplianceStatus.NON_COMPLIANT
456
+ evaluation_reason = f"DynamoDB table {table_name} is encrypted but not with KMS (type: {sse_type})"
457
+ else:
458
+ compliance_status = ComplianceStatus.NON_COMPLIANT
459
+ evaluation_reason = f"DynamoDB table {table_name} does not have encryption enabled"
460
+
461
+ return ComplianceResult(
462
+ resource_id=table_name,
463
+ resource_type="AWS::DynamoDB::Table",
464
+ compliance_status=compliance_status,
465
+ evaluation_reason=evaluation_reason,
466
+ config_rule_name=self.rule_name,
467
+ region=region
468
+ )
469
+
470
+ def _get_rule_remediation_steps(self) -> List[str]:
471
+ """Get specific remediation steps for DynamoDB KMS encryption."""
472
+ return [
473
+ "Identify DynamoDB tables without KMS encryption",
474
+ "For each non-compliant table:",
475
+ " 1. Create or identify a KMS key for DynamoDB encryption",
476
+ " 2. Enable encryption at rest with KMS for the table",
477
+ " 3. Verify encryption status after enabling",
478
+ "Note: Encryption can only be enabled during table creation for existing tables",
479
+ "For existing unencrypted tables:",
480
+ " 1. Create a backup of the table data",
481
+ " 2. Create a new table with KMS encryption enabled",
482
+ " 3. Migrate data from old table to new encrypted table",
483
+ " 4. Update applications to use the new table",
484
+ " 5. Delete the old unencrypted table after verification",
485
+ "Use AWS CLI for new tables: aws dynamodb create-table --table-name <table-name> --sse-specification Enabled=true,SSEType=KMS,KMSMasterKeyId=<key-id>",
486
+ "Create KMS key if needed: aws kms create-key --description 'DynamoDB table encryption key'",
487
+ "For point-in-time recovery, ensure backups are also encrypted"
488
+ ]
489
+
490
+
491
+ class BackupRecoveryPointEncryptedAssessment(BaseConfigRuleAssessment):
492
+ """Assessment for backup-recovery-point-encrypted Config rule - ensures AWS Backup recovery points are encrypted."""
493
+
494
+ def __init__(self):
495
+ """Initialize backup recovery point encrypted assessment."""
496
+ super().__init__(
497
+ rule_name="backup-recovery-point-encrypted",
498
+ control_id="3.11",
499
+ resource_types=["AWS::Backup::RecoveryPoint"]
500
+ )
501
+
502
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
503
+ """Get all AWS Backup recovery points in the region."""
504
+ if resource_type != "AWS::Backup::RecoveryPoint":
505
+ return []
506
+
507
+ try:
508
+ backup_client = aws_factory.get_client('backup', region)
509
+
510
+ # First get all backup vaults
511
+ vaults_response = aws_factory.aws_api_call_with_retry(
512
+ lambda: backup_client.list_backup_vaults()
513
+ )
514
+
515
+ recovery_points = []
516
+
517
+ for vault in vaults_response.get('BackupVaultList', []):
518
+ vault_name = vault.get('BackupVaultName')
519
+
520
+ try:
521
+ # Get recovery points for each vault
522
+ points_response = aws_factory.aws_api_call_with_retry(
523
+ lambda: backup_client.list_recovery_points_by_backup_vault(
524
+ BackupVaultName=vault_name
525
+ )
526
+ )
527
+
528
+ for point in points_response.get('RecoveryPoints', []):
529
+ recovery_points.append({
530
+ 'RecoveryPointArn': point.get('RecoveryPointArn'),
531
+ 'BackupVaultName': vault_name,
532
+ 'ResourceArn': point.get('ResourceArn'),
533
+ 'ResourceType': point.get('ResourceType'),
534
+ 'CreationDate': point.get('CreationDate'),
535
+ 'Status': point.get('Status'),
536
+ 'IsEncrypted': point.get('IsEncrypted', False),
537
+ 'EncryptionKeyArn': point.get('EncryptionKeyArn'),
538
+ 'BackupSizeInBytes': point.get('BackupSizeInBytes', 0)
539
+ })
540
+
541
+ except ClientError as e:
542
+ logger.warning(f"Could not get recovery points for vault {vault_name}: {e}")
543
+ continue
544
+
545
+ logger.debug(f"Found {len(recovery_points)} backup recovery points in region {region}")
546
+ return recovery_points
547
+
548
+ except ClientError as e:
549
+ logger.error(f"Error retrieving backup recovery points in region {region}: {e}")
550
+ raise
551
+ except Exception as e:
552
+ logger.error(f"Unexpected error retrieving backup recovery points in region {region}: {e}")
553
+ raise
554
+
555
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
556
+ """Evaluate if backup recovery point is encrypted."""
557
+ recovery_point_arn = resource.get('RecoveryPointArn', 'unknown')
558
+ vault_name = resource.get('BackupVaultName', 'unknown')
559
+ resource_type = resource.get('ResourceType', 'unknown')
560
+ status = resource.get('Status', 'unknown')
561
+ is_encrypted = resource.get('IsEncrypted', False)
562
+ encryption_key_arn = resource.get('EncryptionKeyArn')
563
+
564
+ # Skip recovery points that are not completed
565
+ if status not in ['COMPLETED']:
566
+ return ComplianceResult(
567
+ resource_id=recovery_point_arn,
568
+ resource_type="AWS::Backup::RecoveryPoint",
569
+ compliance_status=ComplianceStatus.NOT_APPLICABLE,
570
+ evaluation_reason=f"Recovery point in vault {vault_name} is in status '{status}'",
571
+ config_rule_name=self.rule_name,
572
+ region=region
573
+ )
574
+
575
+ # Evaluate encryption status
576
+ if is_encrypted:
577
+ compliance_status = ComplianceStatus.COMPLIANT
578
+ if encryption_key_arn:
579
+ evaluation_reason = f"Recovery point for {resource_type} in vault {vault_name} is encrypted with key {encryption_key_arn}"
580
+ else:
581
+ evaluation_reason = f"Recovery point for {resource_type} in vault {vault_name} is encrypted"
582
+ else:
583
+ compliance_status = ComplianceStatus.NON_COMPLIANT
584
+ evaluation_reason = f"Recovery point for {resource_type} in vault {vault_name} is not encrypted"
585
+
586
+ return ComplianceResult(
587
+ resource_id=recovery_point_arn,
588
+ resource_type="AWS::Backup::RecoveryPoint",
589
+ compliance_status=compliance_status,
590
+ evaluation_reason=evaluation_reason,
591
+ config_rule_name=self.rule_name,
592
+ region=region
593
+ )
594
+
595
+ def _get_rule_remediation_steps(self) -> List[str]:
596
+ """Get specific remediation steps for backup recovery point encryption."""
597
+ return [
598
+ "Identify backup recovery points that are not encrypted",
599
+ "For each non-compliant recovery point:",
600
+ " 1. Review the backup vault encryption settings",
601
+ " 2. Ensure backup vaults are configured with KMS encryption",
602
+ " 3. Create new backups with encryption enabled",
603
+ " 4. Consider deleting unencrypted recovery points after creating encrypted replacements",
604
+ "Configure backup vault encryption: aws backup put-backup-vault-encryption --backup-vault-name <vault-name> --encryption-key-arn <kms-key-arn>",
605
+ "Create KMS key for backups: aws kms create-key --description 'AWS Backup encryption key'",
606
+ "Update backup plans to use encrypted vaults",
607
+ "For existing unencrypted recovery points, create new encrypted backups and delete old ones",
608
+ "Ensure backup policies require encryption for all new backups"
609
+ ]
610
+
611
+
612
+ class EFSEncryptedCheckAssessment(BaseConfigRuleAssessment):
613
+ """Assessment for efs-encrypted-check Config rule - ensures EFS file systems are encrypted."""
614
+
615
+ def __init__(self):
616
+ """Initialize EFS encrypted check assessment."""
617
+ super().__init__(
618
+ rule_name="efs-encrypted-check",
619
+ control_id="3.11",
620
+ resource_types=["AWS::EFS::FileSystem"]
621
+ )
622
+
623
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
624
+ """Get all EFS file systems in the region."""
625
+ if resource_type != "AWS::EFS::FileSystem":
626
+ return []
627
+
628
+ try:
629
+ efs_client = aws_factory.get_client('efs', region)
630
+
631
+ response = aws_factory.aws_api_call_with_retry(
632
+ lambda: efs_client.describe_file_systems()
633
+ )
634
+
635
+ file_systems = []
636
+ for fs in response.get('FileSystems', []):
637
+ file_systems.append({
638
+ 'FileSystemId': fs.get('FileSystemId'),
639
+ 'FileSystemArn': fs.get('FileSystemArn'),
640
+ 'CreationToken': fs.get('CreationToken'),
641
+ 'LifeCycleState': fs.get('LifeCycleState'),
642
+ 'Encrypted': fs.get('Encrypted', False),
643
+ 'KmsKeyId': fs.get('KmsKeyId'),
644
+ 'PerformanceMode': fs.get('PerformanceMode'),
645
+ 'ThroughputMode': fs.get('ThroughputMode'),
646
+ 'Tags': fs.get('Tags', [])
647
+ })
648
+
649
+ logger.debug(f"Found {len(file_systems)} EFS file systems in region {region}")
650
+ return file_systems
651
+
652
+ except ClientError as e:
653
+ logger.error(f"Error retrieving EFS file systems in region {region}: {e}")
654
+ raise
655
+ except Exception as e:
656
+ logger.error(f"Unexpected error retrieving EFS file systems in region {region}: {e}")
657
+ raise
658
+
659
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
660
+ """Evaluate if EFS file system is encrypted."""
661
+ fs_id = resource.get('FileSystemId', 'unknown')
662
+ lifecycle_state = resource.get('LifeCycleState', 'unknown')
663
+ is_encrypted = resource.get('Encrypted', False)
664
+ kms_key_id = resource.get('KmsKeyId')
665
+
666
+ # Skip file systems that are not available
667
+ if lifecycle_state not in ['available']:
668
+ return ComplianceResult(
669
+ resource_id=fs_id,
670
+ resource_type="AWS::EFS::FileSystem",
671
+ compliance_status=ComplianceStatus.NOT_APPLICABLE,
672
+ evaluation_reason=f"EFS file system {fs_id} is in state '{lifecycle_state}'",
673
+ config_rule_name=self.rule_name,
674
+ region=region
675
+ )
676
+
677
+ # Evaluate encryption status
678
+ if is_encrypted:
679
+ compliance_status = ComplianceStatus.COMPLIANT
680
+ if kms_key_id:
681
+ evaluation_reason = f"EFS file system {fs_id} is encrypted with KMS key {kms_key_id}"
682
+ else:
683
+ evaluation_reason = f"EFS file system {fs_id} is encrypted with default key"
684
+ else:
685
+ compliance_status = ComplianceStatus.NON_COMPLIANT
686
+ evaluation_reason = f"EFS file system {fs_id} is not encrypted"
687
+
688
+ return ComplianceResult(
689
+ resource_id=fs_id,
690
+ resource_type="AWS::EFS::FileSystem",
691
+ compliance_status=compliance_status,
692
+ evaluation_reason=evaluation_reason,
693
+ config_rule_name=self.rule_name,
694
+ region=region
695
+ )
696
+
697
+ def _get_rule_remediation_steps(self) -> List[str]:
698
+ """Get specific remediation steps for EFS encryption."""
699
+ return [
700
+ "Identify EFS file systems that are not encrypted",
701
+ "For each unencrypted file system:",
702
+ " 1. Create a backup of the file system data",
703
+ " 2. Create a new encrypted EFS file system",
704
+ " 3. Copy data from the unencrypted file system to the encrypted one",
705
+ " 4. Update mount targets and applications to use the new encrypted file system",
706
+ " 5. Delete the old unencrypted file system after verification",
707
+ "Note: Encryption can only be enabled during file system creation",
708
+ "Use AWS CLI: aws efs create-file-system --creation-token <token> --encrypted --kms-key-id <key-id>",
709
+ "Create KMS key if needed: aws kms create-key --description 'EFS encryption key'",
710
+ "Use AWS DataSync to copy data between file systems",
711
+ "Update EC2 instances and applications to mount the new encrypted file system",
712
+ "For new file systems, always enable encryption during creation"
713
+ ]
714
+
715
+
716
+ class SecretsManagerUsingKMSKeyAssessment(BaseConfigRuleAssessment):
717
+ """Assessment for secretsmanager-using-cmk Config rule - ensures Secrets Manager secrets use KMS keys."""
718
+
719
+ def __init__(self):
720
+ """Initialize Secrets Manager KMS key assessment."""
721
+ super().__init__(
722
+ rule_name="secretsmanager-using-cmk",
723
+ control_id="3.11",
724
+ resource_types=["AWS::SecretsManager::Secret"]
725
+ )
726
+
727
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
728
+ """Get all Secrets Manager secrets in the region."""
729
+ if resource_type != "AWS::SecretsManager::Secret":
730
+ return []
731
+
732
+ try:
733
+ secretsmanager_client = aws_factory.get_client('secretsmanager', region)
734
+
735
+ response = aws_factory.aws_api_call_with_retry(
736
+ lambda: secretsmanager_client.list_secrets()
737
+ )
738
+
739
+ secrets = []
740
+ for secret in response.get('SecretList', []):
741
+ secrets.append({
742
+ 'ARN': secret.get('ARN'),
743
+ 'Name': secret.get('Name'),
744
+ 'Description': secret.get('Description', ''),
745
+ 'KmsKeyId': secret.get('KmsKeyId'),
746
+ 'CreatedDate': secret.get('CreatedDate'),
747
+ 'LastChangedDate': secret.get('LastChangedDate'),
748
+ 'Tags': secret.get('Tags', [])
749
+ })
750
+
751
+ logger.debug(f"Found {len(secrets)} Secrets Manager secrets in region {region}")
752
+ return secrets
753
+
754
+ except ClientError as e:
755
+ logger.error(f"Error retrieving Secrets Manager secrets in region {region}: {e}")
756
+ raise
757
+ except Exception as e:
758
+ logger.error(f"Unexpected error retrieving Secrets Manager secrets in region {region}: {e}")
759
+ raise
760
+
761
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
762
+ """Evaluate if Secrets Manager secret uses KMS key."""
763
+ secret_arn = resource.get('ARN', 'unknown')
764
+ secret_name = resource.get('Name', 'unknown')
765
+ kms_key_id = resource.get('KmsKeyId')
766
+
767
+ # Check if secret uses KMS key
768
+ if kms_key_id:
769
+ # Check if it's using a customer-managed key (not the default AWS managed key)
770
+ if kms_key_id.startswith('alias/aws/secretsmanager'):
771
+ compliance_status = ComplianceStatus.NON_COMPLIANT
772
+ evaluation_reason = f"Secret {secret_name} uses AWS managed key instead of customer managed key"
773
+ else:
774
+ compliance_status = ComplianceStatus.COMPLIANT
775
+ evaluation_reason = f"Secret {secret_name} uses customer managed KMS key {kms_key_id}"
776
+ else:
777
+ compliance_status = ComplianceStatus.NON_COMPLIANT
778
+ evaluation_reason = f"Secret {secret_name} does not specify a KMS key (using default encryption)"
779
+
780
+ return ComplianceResult(
781
+ resource_id=secret_arn,
782
+ resource_type="AWS::SecretsManager::Secret",
783
+ compliance_status=compliance_status,
784
+ evaluation_reason=evaluation_reason,
785
+ config_rule_name=self.rule_name,
786
+ region=region
787
+ )
788
+
789
+ def _get_rule_remediation_steps(self) -> List[str]:
790
+ """Get specific remediation steps for Secrets Manager KMS encryption."""
791
+ return [
792
+ "Identify Secrets Manager secrets not using customer managed KMS keys",
793
+ "For each non-compliant secret:",
794
+ " 1. Create or identify a customer managed KMS key",
795
+ " 2. Update the secret to use the customer managed KMS key",
796
+ " 3. Verify the secret is encrypted with the new key",
797
+ "Create KMS key: aws kms create-key --description 'Secrets Manager encryption key'",
798
+ "Update secret: aws secretsmanager update-secret --secret-id <secret-name> --kms-key-id <key-id>",
799
+ "For new secrets, specify KMS key during creation: --kms-key-id <key-id>",
800
+ "Ensure proper IAM permissions for the KMS key",
801
+ "Consider key rotation policies for enhanced security"
802
+ ]
803
+
804
+
805
+ class SNSTopicEncryptedKMSAssessment(BaseConfigRuleAssessment):
806
+ """Assessment for sns-encrypted-kms Config rule - ensures SNS topics are encrypted with KMS."""
807
+
808
+ def __init__(self):
809
+ """Initialize SNS topic encrypted KMS assessment."""
810
+ super().__init__(
811
+ rule_name="sns-encrypted-kms",
812
+ control_id="3.11",
813
+ resource_types=["AWS::SNS::Topic"]
814
+ )
815
+
816
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
817
+ """Get all SNS topics in the region."""
818
+ if resource_type != "AWS::SNS::Topic":
819
+ return []
820
+
821
+ try:
822
+ sns_client = aws_factory.get_client('sns', region)
823
+
824
+ response = aws_factory.aws_api_call_with_retry(
825
+ lambda: sns_client.list_topics()
826
+ )
827
+
828
+ topics = []
829
+ for topic in response.get('Topics', []):
830
+ topic_arn = topic.get('TopicArn')
831
+
832
+ try:
833
+ # Get topic attributes to check encryption
834
+ attrs_response = aws_factory.aws_api_call_with_retry(
835
+ lambda: sns_client.get_topic_attributes(TopicArn=topic_arn)
836
+ )
837
+
838
+ attributes = attrs_response.get('Attributes', {})
839
+ topics.append({
840
+ 'TopicArn': topic_arn,
841
+ 'DisplayName': attributes.get('DisplayName', ''),
842
+ 'KmsMasterKeyId': attributes.get('KmsMasterKeyId'),
843
+ 'Policy': attributes.get('Policy', ''),
844
+ 'SubscriptionsConfirmed': attributes.get('SubscriptionsConfirmed', '0'),
845
+ 'SubscriptionsPending': attributes.get('SubscriptionsPending', '0')
846
+ })
847
+
848
+ except ClientError as e:
849
+ logger.warning(f"Could not get attributes for topic {topic_arn}: {e}")
850
+ continue
851
+
852
+ logger.debug(f"Found {len(topics)} SNS topics in region {region}")
853
+ return topics
854
+
855
+ except ClientError as e:
856
+ logger.error(f"Error retrieving SNS topics in region {region}: {e}")
857
+ raise
858
+ except Exception as e:
859
+ logger.error(f"Unexpected error retrieving SNS topics in region {region}: {e}")
860
+ raise
861
+
862
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
863
+ """Evaluate if SNS topic is encrypted with KMS."""
864
+ topic_arn = resource.get('TopicArn', 'unknown')
865
+ display_name = resource.get('DisplayName', '')
866
+ kms_master_key_id = resource.get('KmsMasterKeyId')
867
+
868
+ topic_name = topic_arn.split(':')[-1] if ':' in topic_arn else topic_arn
869
+
870
+ # Check if topic uses KMS encryption
871
+ if kms_master_key_id:
872
+ compliance_status = ComplianceStatus.COMPLIANT
873
+ evaluation_reason = f"SNS topic {topic_name} is encrypted with KMS key {kms_master_key_id}"
874
+ else:
875
+ compliance_status = ComplianceStatus.NON_COMPLIANT
876
+ evaluation_reason = f"SNS topic {topic_name} is not encrypted with KMS"
877
+
878
+ return ComplianceResult(
879
+ resource_id=topic_arn,
880
+ resource_type="AWS::SNS::Topic",
881
+ compliance_status=compliance_status,
882
+ evaluation_reason=evaluation_reason,
883
+ config_rule_name=self.rule_name,
884
+ region=region
885
+ )
886
+
887
+ def _get_rule_remediation_steps(self) -> List[str]:
888
+ """Get specific remediation steps for SNS KMS encryption."""
889
+ return [
890
+ "Identify SNS topics without KMS encryption",
891
+ "For each non-compliant topic:",
892
+ " 1. Create or identify a KMS key for SNS encryption",
893
+ " 2. Set the KmsMasterKeyId attribute for the topic",
894
+ " 3. Verify encryption is enabled",
895
+ "Create KMS key: aws kms create-key --description 'SNS topic encryption key'",
896
+ "Enable encryption: aws sns set-topic-attributes --topic-arn <topic-arn> --attribute-name KmsMasterKeyId --attribute-value <key-id>",
897
+ "For new topics, specify KMS key during creation: --attributes KmsMasterKeyId=<key-id>",
898
+ "Update IAM policies to allow SNS to use the KMS key",
899
+ "Test message publishing and subscription after enabling encryption"
900
+ ]
901
+
902
+
903
+ class SQSQueueEncryptedKMSAssessment(BaseConfigRuleAssessment):
904
+ """Assessment for sqs-queue-encrypted-kms Config rule - ensures SQS queues are encrypted with KMS."""
905
+
906
+ def __init__(self):
907
+ """Initialize SQS queue encrypted KMS assessment."""
908
+ super().__init__(
909
+ rule_name="sqs-queue-encrypted-kms",
910
+ control_id="3.11",
911
+ resource_types=["AWS::SQS::Queue"]
912
+ )
913
+
914
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
915
+ """Get all SQS queues in the region."""
916
+ if resource_type != "AWS::SQS::Queue":
917
+ return []
918
+
919
+ try:
920
+ sqs_client = aws_factory.get_client('sqs', region)
921
+
922
+ response = aws_factory.aws_api_call_with_retry(
923
+ lambda: sqs_client.list_queues()
924
+ )
925
+
926
+ queue_urls = response.get('QueueUrls', [])
927
+ queues = []
928
+
929
+ for queue_url in queue_urls:
930
+ try:
931
+ # Get queue attributes to check encryption
932
+ attrs_response = aws_factory.aws_api_call_with_retry(
933
+ lambda: sqs_client.get_queue_attributes(
934
+ QueueUrl=queue_url,
935
+ AttributeNames=['All']
936
+ )
937
+ )
938
+
939
+ attributes = attrs_response.get('Attributes', {})
940
+ queue_name = queue_url.split('/')[-1]
941
+
942
+ queues.append({
943
+ 'QueueUrl': queue_url,
944
+ 'QueueName': queue_name,
945
+ 'KmsMasterKeyId': attributes.get('KmsMasterKeyId'),
946
+ 'KmsDataKeyReusePeriodSeconds': attributes.get('KmsDataKeyReusePeriodSeconds'),
947
+ 'ApproximateNumberOfMessages': attributes.get('ApproximateNumberOfMessages', '0'),
948
+ 'CreatedTimestamp': attributes.get('CreatedTimestamp')
949
+ })
950
+
951
+ except ClientError as e:
952
+ logger.warning(f"Could not get attributes for queue {queue_url}: {e}")
953
+ continue
954
+
955
+ logger.debug(f"Found {len(queues)} SQS queues in region {region}")
956
+ return queues
957
+
958
+ except ClientError as e:
959
+ logger.error(f"Error retrieving SQS queues in region {region}: {e}")
960
+ raise
961
+ except Exception as e:
962
+ logger.error(f"Unexpected error retrieving SQS queues in region {region}: {e}")
963
+ raise
964
+
965
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
966
+ """Evaluate if SQS queue is encrypted with KMS."""
967
+ queue_url = resource.get('QueueUrl', 'unknown')
968
+ queue_name = resource.get('QueueName', 'unknown')
969
+ kms_master_key_id = resource.get('KmsMasterKeyId')
970
+
971
+ # Check if queue uses KMS encryption
972
+ if kms_master_key_id:
973
+ compliance_status = ComplianceStatus.COMPLIANT
974
+ evaluation_reason = f"SQS queue {queue_name} is encrypted with KMS key {kms_master_key_id}"
975
+ else:
976
+ compliance_status = ComplianceStatus.NON_COMPLIANT
977
+ evaluation_reason = f"SQS queue {queue_name} is not encrypted with KMS"
978
+
979
+ return ComplianceResult(
980
+ resource_id=queue_url,
981
+ resource_type="AWS::SQS::Queue",
982
+ compliance_status=compliance_status,
983
+ evaluation_reason=evaluation_reason,
984
+ config_rule_name=self.rule_name,
985
+ region=region
986
+ )
987
+
988
+ def _get_rule_remediation_steps(self) -> List[str]:
989
+ """Get specific remediation steps for SQS KMS encryption."""
990
+ return [
991
+ "Identify SQS queues without KMS encryption",
992
+ "For each non-compliant queue:",
993
+ " 1. Create or identify a KMS key for SQS encryption",
994
+ " 2. Set the KmsMasterKeyId attribute for the queue",
995
+ " 3. Optionally configure KmsDataKeyReusePeriodSeconds",
996
+ "Create KMS key: aws kms create-key --description 'SQS queue encryption key'",
997
+ "Enable encryption: aws sqs set-queue-attributes --queue-url <queue-url> --attributes KmsMasterKeyId=<key-id>",
998
+ "For new queues, specify KMS key during creation: --attributes KmsMasterKeyId=<key-id>",
999
+ "Update IAM policies to allow SQS to use the KMS key",
1000
+ "Test message sending and receiving after enabling encryption"
1001
+ ]
1002
+
1003
+
1004
+ class CloudWatchLogsEncryptedAssessment(BaseConfigRuleAssessment):
1005
+ """Assessment for cloudwatch-log-group-encrypted Config rule - ensures CloudWatch log groups are encrypted."""
1006
+
1007
+ def __init__(self):
1008
+ """Initialize CloudWatch logs encrypted assessment."""
1009
+ super().__init__(
1010
+ rule_name="cloudwatch-log-group-encrypted",
1011
+ control_id="3.11",
1012
+ resource_types=["AWS::Logs::LogGroup"]
1013
+ )
1014
+
1015
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
1016
+ """Get all CloudWatch log groups in the region."""
1017
+ if resource_type != "AWS::Logs::LogGroup":
1018
+ return []
1019
+
1020
+ try:
1021
+ logs_client = aws_factory.get_client('logs', region)
1022
+
1023
+ log_groups = []
1024
+ next_token = None
1025
+
1026
+ while True:
1027
+ if next_token:
1028
+ response = aws_factory.aws_api_call_with_retry(
1029
+ lambda: logs_client.describe_log_groups(nextToken=next_token)
1030
+ )
1031
+ else:
1032
+ response = aws_factory.aws_api_call_with_retry(
1033
+ lambda: logs_client.describe_log_groups()
1034
+ )
1035
+
1036
+ for log_group in response.get('logGroups', []):
1037
+ log_groups.append({
1038
+ 'logGroupName': log_group.get('logGroupName'),
1039
+ 'logGroupArn': log_group.get('arn'),
1040
+ 'creationTime': log_group.get('creationTime'),
1041
+ 'retentionInDays': log_group.get('retentionInDays'),
1042
+ 'kmsKeyId': log_group.get('kmsKeyId'),
1043
+ 'storedBytes': log_group.get('storedBytes', 0)
1044
+ })
1045
+
1046
+ next_token = response.get('nextToken')
1047
+ if not next_token:
1048
+ break
1049
+
1050
+ logger.debug(f"Found {len(log_groups)} CloudWatch log groups in region {region}")
1051
+ return log_groups
1052
+
1053
+ except ClientError as e:
1054
+ logger.error(f"Error retrieving CloudWatch log groups in region {region}: {e}")
1055
+ raise
1056
+ except Exception as e:
1057
+ logger.error(f"Unexpected error retrieving CloudWatch log groups in region {region}: {e}")
1058
+ raise
1059
+
1060
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
1061
+ """Evaluate if CloudWatch log group is encrypted."""
1062
+ log_group_name = resource.get('logGroupName', 'unknown')
1063
+ log_group_arn = resource.get('logGroupArn', 'unknown')
1064
+ kms_key_id = resource.get('kmsKeyId')
1065
+
1066
+ # Check if log group uses KMS encryption
1067
+ if kms_key_id:
1068
+ compliance_status = ComplianceStatus.COMPLIANT
1069
+ evaluation_reason = f"CloudWatch log group {log_group_name} is encrypted with KMS key {kms_key_id}"
1070
+ else:
1071
+ compliance_status = ComplianceStatus.NON_COMPLIANT
1072
+ evaluation_reason = f"CloudWatch log group {log_group_name} is not encrypted with KMS"
1073
+
1074
+ return ComplianceResult(
1075
+ resource_id=log_group_arn or log_group_name,
1076
+ resource_type="AWS::Logs::LogGroup",
1077
+ compliance_status=compliance_status,
1078
+ evaluation_reason=evaluation_reason,
1079
+ config_rule_name=self.rule_name,
1080
+ region=region
1081
+ )
1082
+
1083
+ def _get_rule_remediation_steps(self) -> List[str]:
1084
+ """Get specific remediation steps for CloudWatch logs KMS encryption."""
1085
+ return [
1086
+ "Identify CloudWatch log groups without KMS encryption",
1087
+ "For each non-compliant log group:",
1088
+ " 1. Create or identify a KMS key for CloudWatch logs encryption",
1089
+ " 2. Associate the KMS key with the log group",
1090
+ " 3. Verify encryption is enabled",
1091
+ "Create KMS key: aws kms create-key --description 'CloudWatch logs encryption key'",
1092
+ "Associate key: aws logs associate-kms-key --log-group-name <log-group-name> --kms-key-id <key-id>",
1093
+ "For new log groups, specify KMS key during creation: --kms-key-id <key-id>",
1094
+ "Update IAM policies to allow CloudWatch Logs to use the KMS key",
1095
+ "Test log ingestion after enabling encryption"
1096
+ ]
1097
+
1098
+
1099
+ class KinesisStreamEncryptedAssessment(BaseConfigRuleAssessment):
1100
+ """Assessment for kinesis-stream-encrypted Config rule - ensures Kinesis streams are encrypted."""
1101
+
1102
+ def __init__(self):
1103
+ """Initialize Kinesis stream encrypted assessment."""
1104
+ super().__init__(
1105
+ rule_name="kinesis-stream-encrypted",
1106
+ control_id="3.11",
1107
+ resource_types=["AWS::Kinesis::Stream"]
1108
+ )
1109
+
1110
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
1111
+ """Get all Kinesis streams in the region."""
1112
+ if resource_type != "AWS::Kinesis::Stream":
1113
+ return []
1114
+
1115
+ try:
1116
+ kinesis_client = aws_factory.get_client('kinesis', region)
1117
+
1118
+ response = aws_factory.aws_api_call_with_retry(
1119
+ lambda: kinesis_client.list_streams()
1120
+ )
1121
+
1122
+ stream_names = response.get('StreamNames', [])
1123
+ streams = []
1124
+
1125
+ for stream_name in stream_names:
1126
+ try:
1127
+ # Get stream details
1128
+ stream_response = aws_factory.aws_api_call_with_retry(
1129
+ lambda: kinesis_client.describe_stream(StreamName=stream_name)
1130
+ )
1131
+
1132
+ stream_description = stream_response.get('StreamDescription', {})
1133
+ streams.append({
1134
+ 'StreamName': stream_description.get('StreamName'),
1135
+ 'StreamARN': stream_description.get('StreamARN'),
1136
+ 'StreamStatus': stream_description.get('StreamStatus'),
1137
+ 'StreamCreationTimestamp': stream_description.get('StreamCreationTimestamp'),
1138
+ 'EncryptionType': stream_description.get('EncryptionType'),
1139
+ 'KeyId': stream_description.get('KeyId'),
1140
+ 'ShardCount': len(stream_description.get('Shards', []))
1141
+ })
1142
+
1143
+ except ClientError as e:
1144
+ logger.warning(f"Could not describe stream {stream_name}: {e}")
1145
+ continue
1146
+
1147
+ logger.debug(f"Found {len(streams)} Kinesis streams in region {region}")
1148
+ return streams
1149
+
1150
+ except ClientError as e:
1151
+ logger.error(f"Error retrieving Kinesis streams in region {region}: {e}")
1152
+ raise
1153
+ except Exception as e:
1154
+ logger.error(f"Unexpected error retrieving Kinesis streams in region {region}: {e}")
1155
+ raise
1156
+
1157
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
1158
+ """Evaluate if Kinesis stream is encrypted."""
1159
+ stream_name = resource.get('StreamName', 'unknown')
1160
+ stream_arn = resource.get('StreamARN', 'unknown')
1161
+ stream_status = resource.get('StreamStatus', 'unknown')
1162
+ encryption_type = resource.get('EncryptionType')
1163
+ key_id = resource.get('KeyId')
1164
+
1165
+ # Skip streams that are not active
1166
+ if stream_status not in ['ACTIVE']:
1167
+ return ComplianceResult(
1168
+ resource_id=stream_arn or stream_name,
1169
+ resource_type="AWS::Kinesis::Stream",
1170
+ compliance_status=ComplianceStatus.NOT_APPLICABLE,
1171
+ evaluation_reason=f"Kinesis stream {stream_name} is in status '{stream_status}'",
1172
+ config_rule_name=self.rule_name,
1173
+ region=region
1174
+ )
1175
+
1176
+ # Check encryption status
1177
+ if encryption_type == 'KMS':
1178
+ compliance_status = ComplianceStatus.COMPLIANT
1179
+ if key_id:
1180
+ evaluation_reason = f"Kinesis stream {stream_name} is encrypted with KMS key {key_id}"
1181
+ else:
1182
+ evaluation_reason = f"Kinesis stream {stream_name} is encrypted with KMS"
1183
+ elif encryption_type == 'NONE' or not encryption_type:
1184
+ compliance_status = ComplianceStatus.NON_COMPLIANT
1185
+ evaluation_reason = f"Kinesis stream {stream_name} is not encrypted"
1186
+ else:
1187
+ compliance_status = ComplianceStatus.NON_COMPLIANT
1188
+ evaluation_reason = f"Kinesis stream {stream_name} uses unsupported encryption type: {encryption_type}"
1189
+
1190
+ return ComplianceResult(
1191
+ resource_id=stream_arn or stream_name,
1192
+ resource_type="AWS::Kinesis::Stream",
1193
+ compliance_status=compliance_status,
1194
+ evaluation_reason=evaluation_reason,
1195
+ config_rule_name=self.rule_name,
1196
+ region=region
1197
+ )
1198
+
1199
+ def _get_rule_remediation_steps(self) -> List[str]:
1200
+ """Get specific remediation steps for Kinesis stream encryption."""
1201
+ return [
1202
+ "Identify Kinesis streams without encryption",
1203
+ "For each non-compliant stream:",
1204
+ " 1. Create or identify a KMS key for Kinesis encryption",
1205
+ " 2. Enable server-side encryption for the stream",
1206
+ " 3. Verify encryption is active",
1207
+ "Create KMS key: aws kms create-key --description 'Kinesis stream encryption key'",
1208
+ "Enable encryption: aws kinesis enable-stream-encryption --stream-name <stream-name> --encryption-type KMS --key-id <key-id>",
1209
+ "For new streams, specify encryption during creation: --encryption-type KMS --key-id <key-id>",
1210
+ "Update IAM policies to allow Kinesis to use the KMS key",
1211
+ "Test data ingestion and consumption after enabling encryption"
1212
+ ]
1213
+
1214
+
1215
+ class ElasticSearchDomainEncryptedAssessment(BaseConfigRuleAssessment):
1216
+ """Assessment for elasticsearch-encrypted-at-rest Config rule - ensures Elasticsearch domains are encrypted at rest."""
1217
+
1218
+ def __init__(self):
1219
+ """Initialize Elasticsearch domain encrypted assessment."""
1220
+ super().__init__(
1221
+ rule_name="elasticsearch-encrypted-at-rest",
1222
+ control_id="3.11",
1223
+ resource_types=["AWS::Elasticsearch::Domain"]
1224
+ )
1225
+
1226
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
1227
+ """Get all Elasticsearch domains in the region."""
1228
+ if resource_type != "AWS::Elasticsearch::Domain":
1229
+ return []
1230
+
1231
+ try:
1232
+ es_client = aws_factory.get_client('es', region)
1233
+
1234
+ response = aws_factory.aws_api_call_with_retry(
1235
+ lambda: es_client.list_domain_names()
1236
+ )
1237
+
1238
+ domain_names = [domain['DomainName'] for domain in response.get('DomainNames', [])]
1239
+ domains = []
1240
+
1241
+ for domain_name in domain_names:
1242
+ try:
1243
+ # Get domain details
1244
+ domain_response = aws_factory.aws_api_call_with_retry(
1245
+ lambda: es_client.describe_elasticsearch_domain(DomainName=domain_name)
1246
+ )
1247
+
1248
+ domain_status = domain_response.get('DomainStatus', {})
1249
+ encryption_at_rest = domain_status.get('EncryptionAtRestOptions', {})
1250
+
1251
+ domains.append({
1252
+ 'DomainName': domain_status.get('DomainName'),
1253
+ 'DomainId': domain_status.get('DomainId'),
1254
+ 'ARN': domain_status.get('ARN'),
1255
+ 'Created': domain_status.get('Created'),
1256
+ 'Processing': domain_status.get('Processing'),
1257
+ 'EncryptionAtRestEnabled': encryption_at_rest.get('Enabled', False),
1258
+ 'KmsKeyId': encryption_at_rest.get('KmsKeyId'),
1259
+ 'ElasticsearchVersion': domain_status.get('ElasticsearchVersion')
1260
+ })
1261
+
1262
+ except ClientError as e:
1263
+ logger.warning(f"Could not describe Elasticsearch domain {domain_name}: {e}")
1264
+ continue
1265
+
1266
+ logger.debug(f"Found {len(domains)} Elasticsearch domains in region {region}")
1267
+ return domains
1268
+
1269
+ except ClientError as e:
1270
+ logger.error(f"Error retrieving Elasticsearch domains in region {region}: {e}")
1271
+ raise
1272
+ except Exception as e:
1273
+ logger.error(f"Unexpected error retrieving Elasticsearch domains in region {region}: {e}")
1274
+ raise
1275
+
1276
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
1277
+ """Evaluate if Elasticsearch domain has encryption at rest enabled."""
1278
+ domain_name = resource.get('DomainName', 'unknown')
1279
+ domain_arn = resource.get('ARN', 'unknown')
1280
+ is_processing = resource.get('Processing', False)
1281
+ encryption_enabled = resource.get('EncryptionAtRestEnabled', False)
1282
+ kms_key_id = resource.get('KmsKeyId')
1283
+
1284
+ # Skip domains that are being processed
1285
+ if is_processing:
1286
+ return ComplianceResult(
1287
+ resource_id=domain_arn or domain_name,
1288
+ resource_type="AWS::Elasticsearch::Domain",
1289
+ compliance_status=ComplianceStatus.NOT_APPLICABLE,
1290
+ evaluation_reason=f"Elasticsearch domain {domain_name} is currently being processed",
1291
+ config_rule_name=self.rule_name,
1292
+ region=region
1293
+ )
1294
+
1295
+ # Check encryption status
1296
+ if encryption_enabled:
1297
+ compliance_status = ComplianceStatus.COMPLIANT
1298
+ if kms_key_id:
1299
+ evaluation_reason = f"Elasticsearch domain {domain_name} has encryption at rest enabled with KMS key {kms_key_id}"
1300
+ else:
1301
+ evaluation_reason = f"Elasticsearch domain {domain_name} has encryption at rest enabled"
1302
+ else:
1303
+ compliance_status = ComplianceStatus.NON_COMPLIANT
1304
+ evaluation_reason = f"Elasticsearch domain {domain_name} does not have encryption at rest enabled"
1305
+
1306
+ return ComplianceResult(
1307
+ resource_id=domain_arn or domain_name,
1308
+ resource_type="AWS::Elasticsearch::Domain",
1309
+ compliance_status=compliance_status,
1310
+ evaluation_reason=evaluation_reason,
1311
+ config_rule_name=self.rule_name,
1312
+ region=region
1313
+ )
1314
+
1315
+ def _get_rule_remediation_steps(self) -> List[str]:
1316
+ """Get specific remediation steps for Elasticsearch encryption at rest."""
1317
+ return [
1318
+ "Identify Elasticsearch domains without encryption at rest",
1319
+ "For each non-compliant domain:",
1320
+ " 1. Create a snapshot of the domain data",
1321
+ " 2. Create a new domain with encryption at rest enabled",
1322
+ " 3. Restore data from snapshot to the new encrypted domain",
1323
+ " 4. Update applications to use the new encrypted domain",
1324
+ " 5. Delete the old unencrypted domain after verification",
1325
+ "Note: Encryption at rest can only be enabled during domain creation",
1326
+ "Create KMS key: aws kms create-key --description 'Elasticsearch encryption key'",
1327
+ "Create encrypted domain: aws es create-elasticsearch-domain --domain-name <domain-name> --encryption-at-rest-options Enabled=true,KmsKeyId=<key-id>",
1328
+ "Use domain migration tools or manual reindexing to move data",
1329
+ "For new domains, always enable encryption at rest during creation"
1330
+ ]