aws-cis-controls-assessment 1.1.4__py3-none-any.whl → 1.2.2__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 +4 -4
- aws_cis_assessment/config/rules/cis_controls_ig1.yaml +365 -2
- aws_cis_assessment/controls/base_control.py +106 -24
- aws_cis_assessment/controls/ig1/__init__.py +144 -15
- aws_cis_assessment/controls/ig1/control_4_1.py +4 -4
- aws_cis_assessment/controls/ig1/control_access_analyzer.py +198 -0
- aws_cis_assessment/controls/ig1/control_access_asset_mgmt.py +360 -0
- aws_cis_assessment/controls/ig1/control_access_control.py +323 -0
- aws_cis_assessment/controls/ig1/control_backup_security.py +579 -0
- aws_cis_assessment/controls/ig1/control_cloudfront_logging.py +215 -0
- aws_cis_assessment/controls/ig1/control_configuration_mgmt.py +407 -0
- aws_cis_assessment/controls/ig1/control_data_classification.py +255 -0
- aws_cis_assessment/controls/ig1/control_dynamodb_encryption.py +279 -0
- aws_cis_assessment/controls/ig1/control_ebs_encryption.py +177 -0
- aws_cis_assessment/controls/ig1/control_efs_encryption.py +243 -0
- aws_cis_assessment/controls/ig1/control_elb_logging.py +195 -0
- aws_cis_assessment/controls/ig1/control_guardduty.py +156 -0
- aws_cis_assessment/controls/ig1/control_inspector.py +184 -0
- aws_cis_assessment/controls/ig1/control_inventory.py +511 -0
- aws_cis_assessment/controls/ig1/control_macie.py +165 -0
- aws_cis_assessment/controls/ig1/control_messaging_encryption.py +419 -0
- aws_cis_assessment/controls/ig1/control_mfa.py +485 -0
- aws_cis_assessment/controls/ig1/control_network_security.py +194 -619
- aws_cis_assessment/controls/ig1/control_patch_management.py +626 -0
- aws_cis_assessment/controls/ig1/control_rds_encryption.py +228 -0
- aws_cis_assessment/controls/ig1/control_s3_encryption.py +383 -0
- aws_cis_assessment/controls/ig1/control_tls_ssl.py +556 -0
- aws_cis_assessment/controls/ig1/control_version_mgmt.py +337 -0
- aws_cis_assessment/controls/ig1/control_vpc_flow_logs.py +205 -0
- aws_cis_assessment/controls/ig1/control_waf_logging.py +226 -0
- aws_cis_assessment/core/assessment_engine.py +160 -11
- aws_cis_assessment/core/aws_client_factory.py +17 -5
- aws_cis_assessment/core/models.py +20 -1
- aws_cis_assessment/core/scoring_engine.py +102 -1
- aws_cis_assessment/reporters/base_reporter.py +58 -13
- aws_cis_assessment/reporters/html_reporter.py +186 -9
- aws_cis_controls_assessment-1.2.2.dist-info/METADATA +320 -0
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/RECORD +44 -20
- docs/developer-guide.md +204 -5
- docs/user-guide.md +137 -4
- aws_cis_controls_assessment-1.1.4.dist-info/METADATA +0 -404
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/WHEEL +0 -0
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/entry_points.txt +0 -0
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/licenses/LICENSE +0 -0
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CIS Control 11.3, 11.4 - Backup Security
|
|
3
|
+
Ensures proper backup security and protection measures.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import List, Dict, Any
|
|
8
|
+
from botocore.exceptions import ClientError
|
|
9
|
+
|
|
10
|
+
from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
|
|
11
|
+
from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
|
|
12
|
+
from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BackupVaultEncryptionEnabledAssessment(BaseConfigRuleAssessment):
|
|
18
|
+
"""
|
|
19
|
+
CIS Control 11.3 - Protect Recovery Data
|
|
20
|
+
AWS Config Rule: backup-vault-encryption-enabled
|
|
21
|
+
|
|
22
|
+
Ensures AWS Backup vaults are encrypted with KMS.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
super().__init__(
|
|
27
|
+
rule_name="backup-vault-encryption-enabled",
|
|
28
|
+
control_id="11.3",
|
|
29
|
+
resource_types=["AWS::Backup::BackupVault"]
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
33
|
+
"""Get backup vaults and check encryption."""
|
|
34
|
+
if resource_type != "AWS::Backup::BackupVault":
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
backup_client = aws_factory.get_client('backup', region)
|
|
39
|
+
|
|
40
|
+
response = backup_client.list_backup_vaults()
|
|
41
|
+
vaults = []
|
|
42
|
+
|
|
43
|
+
for vault in response.get('BackupVaultList', []):
|
|
44
|
+
vault_name = vault.get('BackupVaultName')
|
|
45
|
+
encryption_key_arn = vault.get('EncryptionKeyArn')
|
|
46
|
+
|
|
47
|
+
# Check if encrypted with KMS
|
|
48
|
+
is_encrypted = bool(encryption_key_arn)
|
|
49
|
+
|
|
50
|
+
vaults.append({
|
|
51
|
+
'VaultName': vault_name,
|
|
52
|
+
'VaultArn': vault.get('BackupVaultArn'),
|
|
53
|
+
'EncryptionKeyArn': encryption_key_arn,
|
|
54
|
+
'IsEncrypted': is_encrypted
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
return vaults
|
|
58
|
+
|
|
59
|
+
except ClientError as e:
|
|
60
|
+
logger.error(f"Error retrieving backup vaults in {region}: {e}")
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
def _evaluate_resource_compliance(
|
|
64
|
+
self,
|
|
65
|
+
resource: Dict[str, Any],
|
|
66
|
+
aws_factory: AWSClientFactory,
|
|
67
|
+
region: str
|
|
68
|
+
) -> ComplianceResult:
|
|
69
|
+
"""Evaluate if backup vault is encrypted."""
|
|
70
|
+
vault_name = resource.get('VaultName', 'unknown')
|
|
71
|
+
is_encrypted = resource.get('IsEncrypted', False)
|
|
72
|
+
encryption_key = resource.get('EncryptionKeyArn', '')
|
|
73
|
+
|
|
74
|
+
if is_encrypted:
|
|
75
|
+
evaluation_reason = f"Backup vault '{vault_name}' is encrypted with KMS key: {encryption_key}"
|
|
76
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
77
|
+
else:
|
|
78
|
+
evaluation_reason = f"Backup vault '{vault_name}' is not encrypted with KMS"
|
|
79
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
80
|
+
|
|
81
|
+
return ComplianceResult(
|
|
82
|
+
resource_id=resource.get('VaultArn', vault_name),
|
|
83
|
+
resource_type="AWS::Backup::BackupVault",
|
|
84
|
+
compliance_status=compliance_status,
|
|
85
|
+
evaluation_reason=evaluation_reason,
|
|
86
|
+
config_rule_name=self.rule_name,
|
|
87
|
+
region=region
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
91
|
+
"""Get remediation steps for backup vault encryption."""
|
|
92
|
+
return [
|
|
93
|
+
"1. Create encrypted backup vault:",
|
|
94
|
+
" aws backup create-backup-vault \\",
|
|
95
|
+
" --backup-vault-name encrypted-vault \\",
|
|
96
|
+
" --encryption-key-arn <kms-key-arn>",
|
|
97
|
+
"",
|
|
98
|
+
"2. Migrate backups to encrypted vault:",
|
|
99
|
+
" - Create new encrypted vault",
|
|
100
|
+
" - Update backup plans to use new vault",
|
|
101
|
+
" - Copy existing backups to new vault",
|
|
102
|
+
" - Delete old unencrypted vault",
|
|
103
|
+
"",
|
|
104
|
+
"3. Console method:",
|
|
105
|
+
" - Navigate to AWS Backup",
|
|
106
|
+
" - Click 'Backup vaults'",
|
|
107
|
+
" - Click 'Create backup vault'",
|
|
108
|
+
" - Enter vault name",
|
|
109
|
+
" - Select KMS key for encryption",
|
|
110
|
+
" - Click 'Create backup vault'",
|
|
111
|
+
"",
|
|
112
|
+
"Priority: HIGH - Protects backup data",
|
|
113
|
+
"Effort: Low - Simple vault creation",
|
|
114
|
+
"",
|
|
115
|
+
"AWS Documentation:",
|
|
116
|
+
"https://docs.aws.amazon.com/aws-backup/latest/devguide/vaults.html"
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class BackupCrossRegionCopyEnabledAssessment(BaseConfigRuleAssessment):
|
|
121
|
+
"""
|
|
122
|
+
CIS Control 11.4 - Establish and Maintain an Isolated Instance of Recovery Data
|
|
123
|
+
AWS Config Rule: backup-cross-region-copy-enabled
|
|
124
|
+
|
|
125
|
+
Ensures backup plans include cross-region copy for disaster recovery.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def __init__(self):
|
|
129
|
+
super().__init__(
|
|
130
|
+
rule_name="backup-cross-region-copy-enabled",
|
|
131
|
+
control_id="11.4",
|
|
132
|
+
resource_types=["AWS::Backup::BackupPlan"]
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
136
|
+
"""Get backup plans and check for cross-region copy."""
|
|
137
|
+
if resource_type != "AWS::Backup::BackupPlan":
|
|
138
|
+
return []
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
backup_client = aws_factory.get_client('backup', region)
|
|
142
|
+
|
|
143
|
+
response = backup_client.list_backup_plans()
|
|
144
|
+
plans = []
|
|
145
|
+
|
|
146
|
+
for plan_summary in response.get('BackupPlansList', []):
|
|
147
|
+
plan_id = plan_summary.get('BackupPlanId')
|
|
148
|
+
plan_name = plan_summary.get('BackupPlanName')
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
# Get plan details
|
|
152
|
+
plan_response = backup_client.get_backup_plan(BackupPlanId=plan_id)
|
|
153
|
+
plan = plan_response.get('BackupPlan', {})
|
|
154
|
+
|
|
155
|
+
# Check if any rule has cross-region copy
|
|
156
|
+
has_cross_region = False
|
|
157
|
+
for rule in plan.get('Rules', []):
|
|
158
|
+
copy_actions = rule.get('CopyActions', [])
|
|
159
|
+
if copy_actions:
|
|
160
|
+
has_cross_region = True
|
|
161
|
+
break
|
|
162
|
+
|
|
163
|
+
plans.append({
|
|
164
|
+
'PlanId': plan_id,
|
|
165
|
+
'PlanName': plan_name,
|
|
166
|
+
'PlanArn': plan_summary.get('BackupPlanArn'),
|
|
167
|
+
'HasCrossRegionCopy': has_cross_region
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
except ClientError:
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
return plans
|
|
174
|
+
|
|
175
|
+
except ClientError as e:
|
|
176
|
+
logger.error(f"Error retrieving backup plans in {region}: {e}")
|
|
177
|
+
return []
|
|
178
|
+
|
|
179
|
+
def _evaluate_resource_compliance(
|
|
180
|
+
self,
|
|
181
|
+
resource: Dict[str, Any],
|
|
182
|
+
aws_factory: AWSClientFactory,
|
|
183
|
+
region: str
|
|
184
|
+
) -> ComplianceResult:
|
|
185
|
+
"""Evaluate if backup plan has cross-region copy."""
|
|
186
|
+
plan_name = resource.get('PlanName', 'unknown')
|
|
187
|
+
has_cross_region = resource.get('HasCrossRegionCopy', False)
|
|
188
|
+
|
|
189
|
+
if has_cross_region:
|
|
190
|
+
evaluation_reason = f"Backup plan '{plan_name}' includes cross-region copy"
|
|
191
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
192
|
+
else:
|
|
193
|
+
evaluation_reason = f"Backup plan '{plan_name}' does not include cross-region copy"
|
|
194
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
195
|
+
|
|
196
|
+
return ComplianceResult(
|
|
197
|
+
resource_id=resource.get('PlanArn', plan_name),
|
|
198
|
+
resource_type="AWS::Backup::BackupPlan",
|
|
199
|
+
compliance_status=compliance_status,
|
|
200
|
+
evaluation_reason=evaluation_reason,
|
|
201
|
+
config_rule_name=self.rule_name,
|
|
202
|
+
region=region
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
206
|
+
"""Get remediation steps for cross-region backup copy."""
|
|
207
|
+
return [
|
|
208
|
+
"1. Update backup plan to include cross-region copy:",
|
|
209
|
+
" aws backup update-backup-plan \\",
|
|
210
|
+
" --backup-plan-id <plan-id> \\",
|
|
211
|
+
" --backup-plan '{",
|
|
212
|
+
' "Rules": [{',
|
|
213
|
+
' "RuleName": "daily-backup",',
|
|
214
|
+
' "TargetBackupVaultName": "primary-vault",',
|
|
215
|
+
' "ScheduleExpression": "cron(0 5 * * ? *)",',
|
|
216
|
+
' "CopyActions": [{',
|
|
217
|
+
' "DestinationBackupVaultArn": "arn:aws:backup:us-west-2:...:backup-vault:dr-vault",',
|
|
218
|
+
' "Lifecycle": {"DeleteAfterDays": 90}',
|
|
219
|
+
" }]",
|
|
220
|
+
" }]",
|
|
221
|
+
" }'",
|
|
222
|
+
"",
|
|
223
|
+
"2. Console method:",
|
|
224
|
+
" - Navigate to AWS Backup",
|
|
225
|
+
" - Select backup plan",
|
|
226
|
+
" - Click 'Edit'",
|
|
227
|
+
" - In backup rule, expand 'Copy to destination'",
|
|
228
|
+
" - Select destination region",
|
|
229
|
+
" - Select destination vault",
|
|
230
|
+
" - Configure lifecycle",
|
|
231
|
+
" - Click 'Save plan'",
|
|
232
|
+
"",
|
|
233
|
+
"3. Best practices:",
|
|
234
|
+
" - Copy to geographically distant region",
|
|
235
|
+
" - Use separate AWS account for DR",
|
|
236
|
+
" - Encrypt copies with different KMS key",
|
|
237
|
+
" - Test restore from DR region regularly",
|
|
238
|
+
"",
|
|
239
|
+
"Priority: HIGH - Critical for disaster recovery",
|
|
240
|
+
"Effort: Medium - Requires DR planning",
|
|
241
|
+
"",
|
|
242
|
+
"AWS Documentation:",
|
|
243
|
+
"https://docs.aws.amazon.com/aws-backup/latest/devguide/cross-region-backup.html"
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class BackupVaultLockEnabledAssessment(BaseConfigRuleAssessment):
|
|
248
|
+
"""
|
|
249
|
+
CIS Control 11.3 - Protect Recovery Data
|
|
250
|
+
AWS Config Rule: backup-vault-lock-enabled
|
|
251
|
+
|
|
252
|
+
Ensures backup vaults have vault lock enabled to prevent deletion.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
def __init__(self):
|
|
256
|
+
super().__init__(
|
|
257
|
+
rule_name="backup-vault-lock-enabled",
|
|
258
|
+
control_id="11.3",
|
|
259
|
+
resource_types=["AWS::Backup::BackupVault"]
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
263
|
+
"""Get backup vaults and check for vault lock."""
|
|
264
|
+
if resource_type != "AWS::Backup::BackupVault":
|
|
265
|
+
return []
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
backup_client = aws_factory.get_client('backup', region)
|
|
269
|
+
|
|
270
|
+
response = backup_client.list_backup_vaults()
|
|
271
|
+
vaults = []
|
|
272
|
+
|
|
273
|
+
for vault in response.get('BackupVaultList', []):
|
|
274
|
+
vault_name = vault.get('BackupVaultName')
|
|
275
|
+
|
|
276
|
+
# Check for vault lock configuration
|
|
277
|
+
try:
|
|
278
|
+
lock_response = backup_client.describe_backup_vault(BackupVaultName=vault_name)
|
|
279
|
+
min_retention_days = lock_response.get('MinRetentionDays')
|
|
280
|
+
max_retention_days = lock_response.get('MaxRetentionDays')
|
|
281
|
+
locked = lock_response.get('Locked', False)
|
|
282
|
+
|
|
283
|
+
has_lock = bool(locked or min_retention_days or max_retention_days)
|
|
284
|
+
except ClientError:
|
|
285
|
+
has_lock = False
|
|
286
|
+
|
|
287
|
+
vaults.append({
|
|
288
|
+
'VaultName': vault_name,
|
|
289
|
+
'VaultArn': vault.get('BackupVaultArn'),
|
|
290
|
+
'HasLock': has_lock
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
return vaults
|
|
294
|
+
|
|
295
|
+
except ClientError as e:
|
|
296
|
+
logger.error(f"Error retrieving backup vaults in {region}: {e}")
|
|
297
|
+
return []
|
|
298
|
+
|
|
299
|
+
def _evaluate_resource_compliance(
|
|
300
|
+
self,
|
|
301
|
+
resource: Dict[str, Any],
|
|
302
|
+
aws_factory: AWSClientFactory,
|
|
303
|
+
region: str
|
|
304
|
+
) -> ComplianceResult:
|
|
305
|
+
"""Evaluate if backup vault has lock enabled."""
|
|
306
|
+
vault_name = resource.get('VaultName', 'unknown')
|
|
307
|
+
has_lock = resource.get('HasLock', False)
|
|
308
|
+
|
|
309
|
+
if has_lock:
|
|
310
|
+
evaluation_reason = f"Backup vault '{vault_name}' has vault lock enabled"
|
|
311
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
312
|
+
else:
|
|
313
|
+
evaluation_reason = f"Backup vault '{vault_name}' does not have vault lock enabled"
|
|
314
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
315
|
+
|
|
316
|
+
return ComplianceResult(
|
|
317
|
+
resource_id=resource.get('VaultArn', vault_name),
|
|
318
|
+
resource_type="AWS::Backup::BackupVault",
|
|
319
|
+
compliance_status=compliance_status,
|
|
320
|
+
evaluation_reason=evaluation_reason,
|
|
321
|
+
config_rule_name=self.rule_name,
|
|
322
|
+
region=region
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
326
|
+
"""Get remediation steps for backup vault lock."""
|
|
327
|
+
return [
|
|
328
|
+
"1. Enable vault lock on backup vault:",
|
|
329
|
+
" aws backup put-backup-vault-lock-configuration \\",
|
|
330
|
+
" --backup-vault-name <vault-name> \\",
|
|
331
|
+
" --min-retention-days 7 \\",
|
|
332
|
+
" --max-retention-days 365",
|
|
333
|
+
"",
|
|
334
|
+
"2. Console method:",
|
|
335
|
+
" - Navigate to AWS Backup",
|
|
336
|
+
" - Select backup vault",
|
|
337
|
+
" - Click 'Vault lock configuration'",
|
|
338
|
+
" - Enable vault lock",
|
|
339
|
+
" - Set minimum retention days",
|
|
340
|
+
" - Set maximum retention days (optional)",
|
|
341
|
+
" - Click 'Save'",
|
|
342
|
+
"",
|
|
343
|
+
"3. Important notes:",
|
|
344
|
+
" - Vault lock is IRREVERSIBLE once enabled",
|
|
345
|
+
" - Test with non-production vault first",
|
|
346
|
+
" - Prevents deletion of backups within retention period",
|
|
347
|
+
" - Protects against ransomware and insider threats",
|
|
348
|
+
"",
|
|
349
|
+
"Priority: HIGH - Protects against backup deletion",
|
|
350
|
+
"Effort: Low - Simple configuration (but irreversible!)",
|
|
351
|
+
"",
|
|
352
|
+
"AWS Documentation:",
|
|
353
|
+
"https://docs.aws.amazon.com/aws-backup/latest/devguide/vault-lock.html"
|
|
354
|
+
]
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class Route53QueryLoggingEnabledAssessment(BaseConfigRuleAssessment):
|
|
358
|
+
"""
|
|
359
|
+
CIS Control 8.2 - Collect Audit Logs
|
|
360
|
+
AWS Config Rule: route53-query-logging-enabled
|
|
361
|
+
|
|
362
|
+
Ensures Route 53 hosted zones have query logging enabled.
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
def __init__(self):
|
|
366
|
+
super().__init__(
|
|
367
|
+
rule_name="route53-query-logging-enabled",
|
|
368
|
+
control_id="8.2",
|
|
369
|
+
resource_types=["AWS::Route53::HostedZone"]
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
373
|
+
"""Get Route 53 hosted zones and check for query logging."""
|
|
374
|
+
if resource_type != "AWS::Route53::HostedZone":
|
|
375
|
+
return []
|
|
376
|
+
|
|
377
|
+
# Route 53 is global, only process in us-east-1
|
|
378
|
+
if region != 'us-east-1':
|
|
379
|
+
return []
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
route53_client = aws_factory.get_client('route53', region)
|
|
383
|
+
|
|
384
|
+
response = route53_client.list_hosted_zones()
|
|
385
|
+
zones = []
|
|
386
|
+
|
|
387
|
+
for zone in response.get('HostedZones', []):
|
|
388
|
+
zone_id = zone.get('Id').split('/')[-1] # Extract ID from full path
|
|
389
|
+
zone_name = zone.get('Name')
|
|
390
|
+
|
|
391
|
+
# Check for query logging configuration
|
|
392
|
+
try:
|
|
393
|
+
logging_response = route53_client.list_query_logging_configs(
|
|
394
|
+
HostedZoneId=zone_id
|
|
395
|
+
)
|
|
396
|
+
has_logging = len(logging_response.get('QueryLoggingConfigs', [])) > 0
|
|
397
|
+
except ClientError:
|
|
398
|
+
has_logging = False
|
|
399
|
+
|
|
400
|
+
zones.append({
|
|
401
|
+
'ZoneId': zone_id,
|
|
402
|
+
'ZoneName': zone_name,
|
|
403
|
+
'HasQueryLogging': has_logging,
|
|
404
|
+
'IsPrivate': zone.get('Config', {}).get('PrivateZone', False)
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
return zones
|
|
408
|
+
|
|
409
|
+
except ClientError as e:
|
|
410
|
+
logger.error(f"Error retrieving Route 53 hosted zones: {e}")
|
|
411
|
+
return []
|
|
412
|
+
|
|
413
|
+
def _evaluate_resource_compliance(
|
|
414
|
+
self,
|
|
415
|
+
resource: Dict[str, Any],
|
|
416
|
+
aws_factory: AWSClientFactory,
|
|
417
|
+
region: str
|
|
418
|
+
) -> ComplianceResult:
|
|
419
|
+
"""Evaluate if hosted zone has query logging enabled."""
|
|
420
|
+
zone_id = resource.get('ZoneId', 'unknown')
|
|
421
|
+
zone_name = resource.get('ZoneName', 'unknown')
|
|
422
|
+
has_logging = resource.get('HasQueryLogging', False)
|
|
423
|
+
is_private = resource.get('IsPrivate', False)
|
|
424
|
+
|
|
425
|
+
if has_logging:
|
|
426
|
+
evaluation_reason = f"Route 53 hosted zone '{zone_name}' has query logging enabled"
|
|
427
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
428
|
+
else:
|
|
429
|
+
evaluation_reason = f"Route 53 hosted zone '{zone_name}' does not have query logging enabled"
|
|
430
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
431
|
+
|
|
432
|
+
return ComplianceResult(
|
|
433
|
+
resource_id=zone_id,
|
|
434
|
+
resource_type="AWS::Route53::HostedZone",
|
|
435
|
+
compliance_status=compliance_status,
|
|
436
|
+
evaluation_reason=evaluation_reason,
|
|
437
|
+
config_rule_name=self.rule_name,
|
|
438
|
+
region=region
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
442
|
+
"""Get remediation steps for Route 53 query logging."""
|
|
443
|
+
return [
|
|
444
|
+
"1. Create CloudWatch log group for query logs:",
|
|
445
|
+
" aws logs create-log-group \\",
|
|
446
|
+
" --log-group-name /aws/route53/queries",
|
|
447
|
+
"",
|
|
448
|
+
"2. Create query logging configuration:",
|
|
449
|
+
" aws route53 create-query-logging-config \\",
|
|
450
|
+
" --hosted-zone-id <zone-id> \\",
|
|
451
|
+
" --cloudwatch-logs-log-group-arn <log-group-arn>",
|
|
452
|
+
"",
|
|
453
|
+
"3. Console method:",
|
|
454
|
+
" - Navigate to Route 53",
|
|
455
|
+
" - Select hosted zone",
|
|
456
|
+
" - Click 'Configure query logging'",
|
|
457
|
+
" - Select or create CloudWatch log group",
|
|
458
|
+
" - Click 'Create'",
|
|
459
|
+
"",
|
|
460
|
+
"4. Best practices:",
|
|
461
|
+
" - Enable for all public hosted zones",
|
|
462
|
+
" - Consider for private zones with sensitive data",
|
|
463
|
+
" - Set appropriate log retention period",
|
|
464
|
+
" - Monitor logs for suspicious queries",
|
|
465
|
+
"",
|
|
466
|
+
"Priority: MEDIUM - DNS query monitoring",
|
|
467
|
+
"Effort: Low - Simple configuration",
|
|
468
|
+
"",
|
|
469
|
+
"AWS Documentation:",
|
|
470
|
+
"https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/query-logs.html"
|
|
471
|
+
]
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
class RDSBackupRetentionCheckAssessment(BaseConfigRuleAssessment):
|
|
475
|
+
"""
|
|
476
|
+
CIS Control 11.2 - Perform Automated Backups
|
|
477
|
+
AWS Config Rule: rds-backup-retention-check
|
|
478
|
+
|
|
479
|
+
Ensures RDS instances have adequate backup retention periods.
|
|
480
|
+
"""
|
|
481
|
+
|
|
482
|
+
def __init__(self):
|
|
483
|
+
super().__init__(
|
|
484
|
+
rule_name="rds-backup-retention-check",
|
|
485
|
+
control_id="11.2",
|
|
486
|
+
resource_types=["AWS::RDS::DBInstance"]
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
490
|
+
"""Get RDS instances and check backup retention."""
|
|
491
|
+
if resource_type != "AWS::RDS::DBInstance":
|
|
492
|
+
return []
|
|
493
|
+
|
|
494
|
+
try:
|
|
495
|
+
rds_client = aws_factory.get_client('rds', region)
|
|
496
|
+
|
|
497
|
+
response = rds_client.describe_db_instances()
|
|
498
|
+
instances = []
|
|
499
|
+
|
|
500
|
+
for db in response.get('DBInstances', []):
|
|
501
|
+
retention_period = db.get('BackupRetentionPeriod', 0)
|
|
502
|
+
|
|
503
|
+
# Minimum recommended retention is 7 days
|
|
504
|
+
meets_minimum = retention_period >= 7
|
|
505
|
+
|
|
506
|
+
instances.append({
|
|
507
|
+
'DBInstanceIdentifier': db.get('DBInstanceIdentifier'),
|
|
508
|
+
'BackupRetentionPeriod': retention_period,
|
|
509
|
+
'MeetsMinimum': meets_minimum
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
return instances
|
|
513
|
+
|
|
514
|
+
except ClientError as e:
|
|
515
|
+
logger.error(f"Error retrieving RDS instances in {region}: {e}")
|
|
516
|
+
return []
|
|
517
|
+
|
|
518
|
+
def _evaluate_resource_compliance(
|
|
519
|
+
self,
|
|
520
|
+
resource: Dict[str, Any],
|
|
521
|
+
aws_factory: AWSClientFactory,
|
|
522
|
+
region: str
|
|
523
|
+
) -> ComplianceResult:
|
|
524
|
+
"""Evaluate if RDS instance has adequate backup retention."""
|
|
525
|
+
db_id = resource.get('DBInstanceIdentifier', 'unknown')
|
|
526
|
+
retention_period = resource.get('BackupRetentionPeriod', 0)
|
|
527
|
+
meets_minimum = resource.get('MeetsMinimum', False)
|
|
528
|
+
|
|
529
|
+
if meets_minimum:
|
|
530
|
+
evaluation_reason = f"RDS instance '{db_id}' has {retention_period} days backup retention (meets 7-day minimum)"
|
|
531
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
532
|
+
else:
|
|
533
|
+
evaluation_reason = f"RDS instance '{db_id}' has {retention_period} days backup retention (below 7-day minimum)"
|
|
534
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
535
|
+
|
|
536
|
+
return ComplianceResult(
|
|
537
|
+
resource_id=db_id,
|
|
538
|
+
resource_type="AWS::RDS::DBInstance",
|
|
539
|
+
compliance_status=compliance_status,
|
|
540
|
+
evaluation_reason=evaluation_reason,
|
|
541
|
+
config_rule_name=self.rule_name,
|
|
542
|
+
region=region
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
546
|
+
"""Get remediation steps for RDS backup retention."""
|
|
547
|
+
return [
|
|
548
|
+
"1. Update RDS backup retention period:",
|
|
549
|
+
" aws rds modify-db-instance \\",
|
|
550
|
+
" --db-instance-identifier <db-id> \\",
|
|
551
|
+
" --backup-retention-period 7 \\",
|
|
552
|
+
" --apply-immediately",
|
|
553
|
+
"",
|
|
554
|
+
"2. Set retention for new instances:",
|
|
555
|
+
" aws rds create-db-instance \\",
|
|
556
|
+
" --db-instance-identifier <db-id> \\",
|
|
557
|
+
" --backup-retention-period 30 \\",
|
|
558
|
+
" ...",
|
|
559
|
+
"",
|
|
560
|
+
"3. Console method:",
|
|
561
|
+
" - Navigate to RDS",
|
|
562
|
+
" - Select database instance",
|
|
563
|
+
" - Click 'Modify'",
|
|
564
|
+
" - Under 'Backup', set retention period",
|
|
565
|
+
" - Click 'Continue'",
|
|
566
|
+
" - Select 'Apply immediately'",
|
|
567
|
+
" - Click 'Modify DB instance'",
|
|
568
|
+
"",
|
|
569
|
+
"4. Recommended retention periods:",
|
|
570
|
+
" - Development: 7 days minimum",
|
|
571
|
+
" - Production: 30 days recommended",
|
|
572
|
+
" - Compliance: 90+ days as required",
|
|
573
|
+
"",
|
|
574
|
+
"Priority: HIGH - Essential for data recovery",
|
|
575
|
+
"Effort: Low - Simple configuration change",
|
|
576
|
+
"",
|
|
577
|
+
"AWS Documentation:",
|
|
578
|
+
"https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithAutomatedBackups.html"
|
|
579
|
+
]
|