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.
- aws_cis_assessment/__init__.py +11 -0
- aws_cis_assessment/cli/__init__.py +3 -0
- aws_cis_assessment/cli/examples.py +274 -0
- aws_cis_assessment/cli/main.py +1259 -0
- aws_cis_assessment/cli/utils.py +356 -0
- aws_cis_assessment/config/__init__.py +1 -0
- aws_cis_assessment/config/config_loader.py +328 -0
- aws_cis_assessment/config/rules/cis_controls_ig1.yaml +590 -0
- aws_cis_assessment/config/rules/cis_controls_ig2.yaml +412 -0
- aws_cis_assessment/config/rules/cis_controls_ig3.yaml +100 -0
- aws_cis_assessment/controls/__init__.py +1 -0
- aws_cis_assessment/controls/base_control.py +400 -0
- aws_cis_assessment/controls/ig1/__init__.py +239 -0
- aws_cis_assessment/controls/ig1/control_1_1.py +586 -0
- aws_cis_assessment/controls/ig1/control_2_2.py +231 -0
- aws_cis_assessment/controls/ig1/control_3_3.py +718 -0
- aws_cis_assessment/controls/ig1/control_3_4.py +235 -0
- aws_cis_assessment/controls/ig1/control_4_1.py +461 -0
- aws_cis_assessment/controls/ig1/control_access_keys.py +310 -0
- aws_cis_assessment/controls/ig1/control_advanced_security.py +512 -0
- aws_cis_assessment/controls/ig1/control_backup_recovery.py +510 -0
- aws_cis_assessment/controls/ig1/control_cloudtrail_logging.py +197 -0
- aws_cis_assessment/controls/ig1/control_critical_security.py +422 -0
- aws_cis_assessment/controls/ig1/control_data_protection.py +898 -0
- aws_cis_assessment/controls/ig1/control_iam_advanced.py +573 -0
- aws_cis_assessment/controls/ig1/control_iam_governance.py +493 -0
- aws_cis_assessment/controls/ig1/control_iam_policies.py +383 -0
- aws_cis_assessment/controls/ig1/control_instance_optimization.py +100 -0
- aws_cis_assessment/controls/ig1/control_network_enhancements.py +203 -0
- aws_cis_assessment/controls/ig1/control_network_security.py +672 -0
- aws_cis_assessment/controls/ig1/control_s3_enhancements.py +173 -0
- aws_cis_assessment/controls/ig1/control_s3_security.py +422 -0
- aws_cis_assessment/controls/ig1/control_vpc_security.py +235 -0
- aws_cis_assessment/controls/ig2/__init__.py +172 -0
- aws_cis_assessment/controls/ig2/control_3_10.py +698 -0
- aws_cis_assessment/controls/ig2/control_3_11.py +1330 -0
- aws_cis_assessment/controls/ig2/control_5_2.py +393 -0
- aws_cis_assessment/controls/ig2/control_advanced_encryption.py +355 -0
- aws_cis_assessment/controls/ig2/control_codebuild_security.py +263 -0
- aws_cis_assessment/controls/ig2/control_encryption_rest.py +382 -0
- aws_cis_assessment/controls/ig2/control_encryption_transit.py +382 -0
- aws_cis_assessment/controls/ig2/control_network_ha.py +467 -0
- aws_cis_assessment/controls/ig2/control_remaining_encryption.py +426 -0
- aws_cis_assessment/controls/ig2/control_remaining_rules.py +363 -0
- aws_cis_assessment/controls/ig2/control_service_logging.py +402 -0
- aws_cis_assessment/controls/ig3/__init__.py +49 -0
- aws_cis_assessment/controls/ig3/control_12_8.py +395 -0
- aws_cis_assessment/controls/ig3/control_13_1.py +467 -0
- aws_cis_assessment/controls/ig3/control_3_14.py +523 -0
- aws_cis_assessment/controls/ig3/control_7_1.py +359 -0
- aws_cis_assessment/core/__init__.py +1 -0
- aws_cis_assessment/core/accuracy_validator.py +425 -0
- aws_cis_assessment/core/assessment_engine.py +1266 -0
- aws_cis_assessment/core/audit_trail.py +491 -0
- aws_cis_assessment/core/aws_client_factory.py +313 -0
- aws_cis_assessment/core/error_handler.py +607 -0
- aws_cis_assessment/core/models.py +166 -0
- aws_cis_assessment/core/scoring_engine.py +459 -0
- aws_cis_assessment/reporters/__init__.py +8 -0
- aws_cis_assessment/reporters/base_reporter.py +454 -0
- aws_cis_assessment/reporters/csv_reporter.py +835 -0
- aws_cis_assessment/reporters/html_reporter.py +2162 -0
- aws_cis_assessment/reporters/json_reporter.py +561 -0
- aws_cis_controls_assessment-1.0.3.dist-info/METADATA +248 -0
- aws_cis_controls_assessment-1.0.3.dist-info/RECORD +77 -0
- aws_cis_controls_assessment-1.0.3.dist-info/WHEEL +5 -0
- aws_cis_controls_assessment-1.0.3.dist-info/entry_points.txt +2 -0
- aws_cis_controls_assessment-1.0.3.dist-info/licenses/LICENSE +21 -0
- aws_cis_controls_assessment-1.0.3.dist-info/top_level.txt +2 -0
- docs/README.md +94 -0
- docs/assessment-logic.md +766 -0
- docs/cli-reference.md +698 -0
- docs/config-rule-mappings.md +393 -0
- docs/developer-guide.md +858 -0
- docs/installation.md +299 -0
- docs/troubleshooting.md +634 -0
- 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
|
+
]
|