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,382 @@
|
|
|
1
|
+
"""Control 3.11: Encrypt Sensitive Data at Rest assessments."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Any
|
|
4
|
+
import logging
|
|
5
|
+
from botocore.exceptions import ClientError
|
|
6
|
+
|
|
7
|
+
from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
|
|
8
|
+
from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
|
|
9
|
+
from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CloudTrailEncryptionEnabledAssessment(BaseConfigRuleAssessment):
|
|
15
|
+
"""Assessment for cloud-trail-encryption-enabled Config rule."""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
"""Initialize CloudTrail encryption assessment."""
|
|
19
|
+
super().__init__(
|
|
20
|
+
rule_name="cloud-trail-encryption-enabled",
|
|
21
|
+
control_id="3.11",
|
|
22
|
+
resource_types=["AWS::CloudTrail::Trail"]
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
26
|
+
"""Get all CloudTrail trails."""
|
|
27
|
+
if resource_type != "AWS::CloudTrail::Trail":
|
|
28
|
+
return []
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
cloudtrail_client = aws_factory.get_client('cloudtrail', region)
|
|
32
|
+
|
|
33
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
34
|
+
lambda: cloudtrail_client.describe_trails()
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
trails = []
|
|
38
|
+
for trail in response.get('trailList', []):
|
|
39
|
+
trails.append({
|
|
40
|
+
'TrailARN': trail.get('TrailARN'),
|
|
41
|
+
'Name': trail.get('Name'),
|
|
42
|
+
'S3BucketName': trail.get('S3BucketName'),
|
|
43
|
+
'KMSKeyId': trail.get('KMSKeyId'),
|
|
44
|
+
'IsMultiRegionTrail': trail.get('IsMultiRegionTrail', False),
|
|
45
|
+
'HomeRegion': trail.get('HomeRegion')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
logger.debug(f"Found {len(trails)} CloudTrail trails")
|
|
49
|
+
return trails
|
|
50
|
+
|
|
51
|
+
except ClientError as e:
|
|
52
|
+
logger.error(f"Error retrieving CloudTrail trails: {e}")
|
|
53
|
+
raise
|
|
54
|
+
except Exception as e:
|
|
55
|
+
logger.error(f"Unexpected error retrieving CloudTrail trails: {e}")
|
|
56
|
+
raise
|
|
57
|
+
|
|
58
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
59
|
+
"""Evaluate if CloudTrail trail has encryption enabled."""
|
|
60
|
+
trail_arn = resource.get('TrailARN', 'unknown')
|
|
61
|
+
trail_name = resource.get('Name', 'unknown')
|
|
62
|
+
kms_key_id = resource.get('KMSKeyId')
|
|
63
|
+
|
|
64
|
+
if kms_key_id:
|
|
65
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
66
|
+
evaluation_reason = f"CloudTrail {trail_name} has encryption enabled with KMS key: {kms_key_id}"
|
|
67
|
+
else:
|
|
68
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
69
|
+
evaluation_reason = f"CloudTrail {trail_name} does not have encryption enabled"
|
|
70
|
+
|
|
71
|
+
return ComplianceResult(
|
|
72
|
+
resource_id=trail_arn,
|
|
73
|
+
resource_type="AWS::CloudTrail::Trail",
|
|
74
|
+
compliance_status=compliance_status,
|
|
75
|
+
evaluation_reason=evaluation_reason,
|
|
76
|
+
config_rule_name=self.rule_name,
|
|
77
|
+
region=region
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
81
|
+
"""Get specific remediation steps for CloudTrail encryption."""
|
|
82
|
+
return [
|
|
83
|
+
"Identify CloudTrail trails without encryption enabled",
|
|
84
|
+
"For each unencrypted trail:",
|
|
85
|
+
" 1. Create or identify a KMS key for CloudTrail encryption",
|
|
86
|
+
" 2. Update the trail configuration to use the KMS key",
|
|
87
|
+
" 3. Ensure CloudTrail service has permissions to use the key",
|
|
88
|
+
" 4. Test that logs are being encrypted properly",
|
|
89
|
+
" 5. Monitor for any delivery failures",
|
|
90
|
+
"Use AWS CLI: aws cloudtrail put-trail --name <trail> --kms-key-id <key-id>",
|
|
91
|
+
"Ensure KMS key policy allows CloudTrail service access",
|
|
92
|
+
"Consider using separate KMS keys for different environments",
|
|
93
|
+
"Monitor KMS key usage and costs"
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class EFSEncryptedCheckAssessment(BaseConfigRuleAssessment):
|
|
98
|
+
"""Assessment for efs-encrypted-check Config rule."""
|
|
99
|
+
|
|
100
|
+
def __init__(self):
|
|
101
|
+
"""Initialize EFS encryption assessment."""
|
|
102
|
+
super().__init__(
|
|
103
|
+
rule_name="efs-encrypted-check",
|
|
104
|
+
control_id="3.11",
|
|
105
|
+
resource_types=["AWS::EFS::FileSystem"]
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
109
|
+
"""Get all EFS file systems in the region."""
|
|
110
|
+
if resource_type != "AWS::EFS::FileSystem":
|
|
111
|
+
return []
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
efs_client = aws_factory.get_client('efs', region)
|
|
115
|
+
|
|
116
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
117
|
+
lambda: efs_client.describe_file_systems()
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
file_systems = []
|
|
121
|
+
for fs in response.get('FileSystems', []):
|
|
122
|
+
file_systems.append({
|
|
123
|
+
'FileSystemId': fs.get('FileSystemId'),
|
|
124
|
+
'FileSystemArn': fs.get('FileSystemArn'),
|
|
125
|
+
'CreationTime': fs.get('CreationTime'),
|
|
126
|
+
'LifeCycleState': fs.get('LifeCycleState'),
|
|
127
|
+
'Encrypted': fs.get('Encrypted', False),
|
|
128
|
+
'KmsKeyId': fs.get('KmsKeyId'),
|
|
129
|
+
'Name': fs.get('Name', fs.get('FileSystemId'))
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
logger.debug(f"Found {len(file_systems)} EFS file systems in region {region}")
|
|
133
|
+
return file_systems
|
|
134
|
+
|
|
135
|
+
except ClientError as e:
|
|
136
|
+
logger.error(f"Error retrieving EFS file systems in region {region}: {e}")
|
|
137
|
+
raise
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.error(f"Unexpected error retrieving EFS file systems in region {region}: {e}")
|
|
140
|
+
raise
|
|
141
|
+
|
|
142
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
143
|
+
"""Evaluate if EFS file system has encryption enabled."""
|
|
144
|
+
fs_id = resource.get('FileSystemId', 'unknown')
|
|
145
|
+
fs_arn = resource.get('FileSystemArn', 'unknown')
|
|
146
|
+
encrypted = resource.get('Encrypted', False)
|
|
147
|
+
kms_key_id = resource.get('KmsKeyId')
|
|
148
|
+
|
|
149
|
+
if encrypted:
|
|
150
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
151
|
+
evaluation_reason = f"EFS file system {fs_id} has encryption enabled"
|
|
152
|
+
if kms_key_id:
|
|
153
|
+
evaluation_reason += f" with KMS key: {kms_key_id}"
|
|
154
|
+
else:
|
|
155
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
156
|
+
evaluation_reason = f"EFS file system {fs_id} does not have encryption enabled"
|
|
157
|
+
|
|
158
|
+
return ComplianceResult(
|
|
159
|
+
resource_id=fs_arn or fs_id,
|
|
160
|
+
resource_type="AWS::EFS::FileSystem",
|
|
161
|
+
compliance_status=compliance_status,
|
|
162
|
+
evaluation_reason=evaluation_reason,
|
|
163
|
+
config_rule_name=self.rule_name,
|
|
164
|
+
region=region
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
168
|
+
"""Get specific remediation steps for EFS encryption."""
|
|
169
|
+
return [
|
|
170
|
+
"Identify EFS file systems without encryption enabled",
|
|
171
|
+
"For each unencrypted file system:",
|
|
172
|
+
" 1. Create a new encrypted EFS file system",
|
|
173
|
+
" 2. Copy data from the unencrypted file system to the encrypted one",
|
|
174
|
+
" 3. Update applications to use the new encrypted file system",
|
|
175
|
+
" 4. Test functionality with the encrypted file system",
|
|
176
|
+
" 5. Delete the old unencrypted file system",
|
|
177
|
+
"Note: Encryption cannot be enabled on existing EFS file systems",
|
|
178
|
+
"Use AWS CLI: aws efs create-file-system --encrypted --kms-key-id <key-id>",
|
|
179
|
+
"Use AWS DataSync or rsync to migrate data between file systems",
|
|
180
|
+
"Plan for downtime during the migration process"
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class EC2EBSEncryptionByDefaultAssessment(BaseConfigRuleAssessment):
|
|
185
|
+
"""Assessment for ec2-ebs-encryption-by-default Config rule."""
|
|
186
|
+
|
|
187
|
+
def __init__(self):
|
|
188
|
+
"""Initialize EC2 EBS encryption by default assessment."""
|
|
189
|
+
super().__init__(
|
|
190
|
+
rule_name="ec2-ebs-encryption-by-default",
|
|
191
|
+
control_id="3.11",
|
|
192
|
+
resource_types=["AWS::::Account"]
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
196
|
+
"""Get account information for EBS encryption by default check."""
|
|
197
|
+
if resource_type != "AWS::::Account":
|
|
198
|
+
return []
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
# Get account ID
|
|
202
|
+
account_info = aws_factory.get_account_info()
|
|
203
|
+
account_id = account_info.get('account_id', 'unknown')
|
|
204
|
+
|
|
205
|
+
return [{
|
|
206
|
+
'AccountId': account_id,
|
|
207
|
+
'Region': region,
|
|
208
|
+
'Type': 'Account'
|
|
209
|
+
}]
|
|
210
|
+
|
|
211
|
+
except Exception as e:
|
|
212
|
+
logger.error(f"Error getting account information: {e}")
|
|
213
|
+
raise
|
|
214
|
+
|
|
215
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
216
|
+
"""Evaluate if EBS encryption by default is enabled."""
|
|
217
|
+
account_id = resource.get('AccountId', 'unknown')
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
ec2_client = aws_factory.get_client('ec2', region)
|
|
221
|
+
|
|
222
|
+
# Check if EBS encryption by default is enabled
|
|
223
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
224
|
+
lambda: ec2_client.get_ebs_encryption_by_default()
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
encryption_by_default = response.get('EbsEncryptionByDefault', False)
|
|
228
|
+
|
|
229
|
+
if encryption_by_default:
|
|
230
|
+
# Get the default KMS key
|
|
231
|
+
try:
|
|
232
|
+
key_response = aws_factory.aws_api_call_with_retry(
|
|
233
|
+
lambda: ec2_client.get_ebs_default_kms_key_id()
|
|
234
|
+
)
|
|
235
|
+
default_key = key_response.get('KmsKeyId', 'AWS managed key')
|
|
236
|
+
|
|
237
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
238
|
+
evaluation_reason = f"Account {account_id} has EBS encryption by default enabled in region {region} with key: {default_key}"
|
|
239
|
+
except ClientError:
|
|
240
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
241
|
+
evaluation_reason = f"Account {account_id} has EBS encryption by default enabled in region {region}"
|
|
242
|
+
else:
|
|
243
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
244
|
+
evaluation_reason = f"Account {account_id} does not have EBS encryption by default enabled in region {region}"
|
|
245
|
+
|
|
246
|
+
except ClientError as e:
|
|
247
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
248
|
+
if error_code in ['AccessDenied', 'UnauthorizedOperation']:
|
|
249
|
+
compliance_status = ComplianceStatus.ERROR
|
|
250
|
+
evaluation_reason = f"Insufficient permissions to check EBS encryption by default for account {account_id} in region {region}"
|
|
251
|
+
else:
|
|
252
|
+
compliance_status = ComplianceStatus.ERROR
|
|
253
|
+
evaluation_reason = f"Error checking EBS encryption by default for account {account_id} in region {region}: {str(e)}"
|
|
254
|
+
except Exception as e:
|
|
255
|
+
compliance_status = ComplianceStatus.ERROR
|
|
256
|
+
evaluation_reason = f"Unexpected error checking EBS encryption by default for account {account_id} in region {region}: {str(e)}"
|
|
257
|
+
|
|
258
|
+
return ComplianceResult(
|
|
259
|
+
resource_id=f"{account_id}-{region}",
|
|
260
|
+
resource_type="AWS::::Account",
|
|
261
|
+
compliance_status=compliance_status,
|
|
262
|
+
evaluation_reason=evaluation_reason,
|
|
263
|
+
config_rule_name=self.rule_name,
|
|
264
|
+
region=region
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
268
|
+
"""Get specific remediation steps for EBS encryption by default."""
|
|
269
|
+
return [
|
|
270
|
+
"Enable EBS encryption by default in each AWS region",
|
|
271
|
+
"For each region where it's not enabled:",
|
|
272
|
+
" 1. Enable EBS encryption by default",
|
|
273
|
+
" 2. Optionally set a customer-managed KMS key as default",
|
|
274
|
+
" 3. Verify that new volumes are encrypted by default",
|
|
275
|
+
" 4. Update any automation/scripts that create volumes",
|
|
276
|
+
"Use AWS CLI: aws ec2 enable-ebs-encryption-by-default --region <region>",
|
|
277
|
+
"Use AWS CLI: aws ec2 modify-ebs-default-kms-key-id --kms-key-id <key-id> --region <region>",
|
|
278
|
+
"Note: This only affects new volumes; existing volumes remain unchanged",
|
|
279
|
+
"Consider encrypting existing unencrypted volumes as needed"
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class RDSSnapshotEncryptedAssessment(BaseConfigRuleAssessment):
|
|
284
|
+
"""Assessment for rds-snapshot-encrypted Config rule."""
|
|
285
|
+
|
|
286
|
+
def __init__(self):
|
|
287
|
+
"""Initialize RDS snapshot encryption assessment."""
|
|
288
|
+
super().__init__(
|
|
289
|
+
rule_name="rds-snapshot-encrypted",
|
|
290
|
+
control_id="3.11",
|
|
291
|
+
resource_types=["AWS::RDS::DBSnapshot", "AWS::RDS::DBClusterSnapshot"]
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
295
|
+
"""Get all RDS snapshots in the region."""
|
|
296
|
+
resources = []
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
rds_client = aws_factory.get_client('rds', region)
|
|
300
|
+
|
|
301
|
+
if resource_type == "AWS::RDS::DBSnapshot":
|
|
302
|
+
# Get DB snapshots
|
|
303
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
304
|
+
lambda: rds_client.describe_db_snapshots(SnapshotType='manual')
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
for snapshot in response.get('DBSnapshots', []):
|
|
308
|
+
resources.append({
|
|
309
|
+
'SnapshotId': snapshot.get('DBSnapshotIdentifier'),
|
|
310
|
+
'SnapshotArn': snapshot.get('DBSnapshotArn'),
|
|
311
|
+
'Type': 'DBSnapshot',
|
|
312
|
+
'Encrypted': snapshot.get('Encrypted', False),
|
|
313
|
+
'KmsKeyId': snapshot.get('KmsKeyId'),
|
|
314
|
+
'Status': snapshot.get('Status')
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
elif resource_type == "AWS::RDS::DBClusterSnapshot":
|
|
318
|
+
# Get DB cluster snapshots
|
|
319
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
320
|
+
lambda: rds_client.describe_db_cluster_snapshots(SnapshotType='manual')
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
for snapshot in response.get('DBClusterSnapshots', []):
|
|
324
|
+
resources.append({
|
|
325
|
+
'SnapshotId': snapshot.get('DBClusterSnapshotIdentifier'),
|
|
326
|
+
'SnapshotArn': snapshot.get('DBClusterSnapshotArn'),
|
|
327
|
+
'Type': 'DBClusterSnapshot',
|
|
328
|
+
'Encrypted': snapshot.get('StorageEncrypted', False),
|
|
329
|
+
'KmsKeyId': snapshot.get('KmsKeyId'),
|
|
330
|
+
'Status': snapshot.get('Status')
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
logger.debug(f"Found {len(resources)} RDS {resource_type.split('::')[-1]}s in region {region}")
|
|
334
|
+
return resources
|
|
335
|
+
|
|
336
|
+
except ClientError as e:
|
|
337
|
+
logger.error(f"Error retrieving RDS {resource_type} in region {region}: {e}")
|
|
338
|
+
raise
|
|
339
|
+
except Exception as e:
|
|
340
|
+
logger.error(f"Unexpected error retrieving RDS {resource_type} in region {region}: {e}")
|
|
341
|
+
raise
|
|
342
|
+
|
|
343
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
344
|
+
"""Evaluate if RDS snapshot is encrypted."""
|
|
345
|
+
snapshot_id = resource.get('SnapshotId', 'unknown')
|
|
346
|
+
snapshot_arn = resource.get('SnapshotArn', 'unknown')
|
|
347
|
+
snapshot_type = resource.get('Type', 'unknown')
|
|
348
|
+
encrypted = resource.get('Encrypted', False)
|
|
349
|
+
kms_key_id = resource.get('KmsKeyId')
|
|
350
|
+
|
|
351
|
+
if encrypted:
|
|
352
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
353
|
+
evaluation_reason = f"RDS {snapshot_type} {snapshot_id} is encrypted"
|
|
354
|
+
if kms_key_id:
|
|
355
|
+
evaluation_reason += f" with KMS key: {kms_key_id}"
|
|
356
|
+
else:
|
|
357
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
358
|
+
evaluation_reason = f"RDS {snapshot_type} {snapshot_id} is not encrypted"
|
|
359
|
+
|
|
360
|
+
return ComplianceResult(
|
|
361
|
+
resource_id=snapshot_arn or snapshot_id,
|
|
362
|
+
resource_type=f"AWS::RDS::{snapshot_type}",
|
|
363
|
+
compliance_status=compliance_status,
|
|
364
|
+
evaluation_reason=evaluation_reason,
|
|
365
|
+
config_rule_name=self.rule_name,
|
|
366
|
+
region=region
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
370
|
+
"""Get specific remediation steps for RDS snapshot encryption."""
|
|
371
|
+
return [
|
|
372
|
+
"Identify unencrypted RDS snapshots",
|
|
373
|
+
"For each unencrypted snapshot:",
|
|
374
|
+
" 1. Create an encrypted copy of the snapshot",
|
|
375
|
+
" 2. Verify the encrypted copy is complete and functional",
|
|
376
|
+
" 3. Update any references to use the encrypted snapshot",
|
|
377
|
+
" 4. Delete the unencrypted snapshot",
|
|
378
|
+
"Use AWS CLI: aws rds copy-db-snapshot --source-db-snapshot-identifier <source> --target-db-snapshot-identifier <target> --kms-key-id <key-id>",
|
|
379
|
+
"Use AWS CLI: aws rds copy-db-cluster-snapshot --source-db-cluster-snapshot-identifier <source> --target-db-cluster-snapshot-identifier <target> --kms-key-id <key-id>",
|
|
380
|
+
"Ensure source databases have encryption enabled to prevent future unencrypted snapshots",
|
|
381
|
+
"Implement policies to automatically encrypt snapshots"
|
|
382
|
+
]
|