aws-cis-controls-assessment 1.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. aws_cis_assessment/__init__.py +11 -0
  2. aws_cis_assessment/cli/__init__.py +3 -0
  3. aws_cis_assessment/cli/examples.py +274 -0
  4. aws_cis_assessment/cli/main.py +1259 -0
  5. aws_cis_assessment/cli/utils.py +356 -0
  6. aws_cis_assessment/config/__init__.py +1 -0
  7. aws_cis_assessment/config/config_loader.py +328 -0
  8. aws_cis_assessment/config/rules/cis_controls_ig1.yaml +590 -0
  9. aws_cis_assessment/config/rules/cis_controls_ig2.yaml +412 -0
  10. aws_cis_assessment/config/rules/cis_controls_ig3.yaml +100 -0
  11. aws_cis_assessment/controls/__init__.py +1 -0
  12. aws_cis_assessment/controls/base_control.py +400 -0
  13. aws_cis_assessment/controls/ig1/__init__.py +239 -0
  14. aws_cis_assessment/controls/ig1/control_1_1.py +586 -0
  15. aws_cis_assessment/controls/ig1/control_2_2.py +231 -0
  16. aws_cis_assessment/controls/ig1/control_3_3.py +718 -0
  17. aws_cis_assessment/controls/ig1/control_3_4.py +235 -0
  18. aws_cis_assessment/controls/ig1/control_4_1.py +461 -0
  19. aws_cis_assessment/controls/ig1/control_access_keys.py +310 -0
  20. aws_cis_assessment/controls/ig1/control_advanced_security.py +512 -0
  21. aws_cis_assessment/controls/ig1/control_backup_recovery.py +510 -0
  22. aws_cis_assessment/controls/ig1/control_cloudtrail_logging.py +197 -0
  23. aws_cis_assessment/controls/ig1/control_critical_security.py +422 -0
  24. aws_cis_assessment/controls/ig1/control_data_protection.py +898 -0
  25. aws_cis_assessment/controls/ig1/control_iam_advanced.py +573 -0
  26. aws_cis_assessment/controls/ig1/control_iam_governance.py +493 -0
  27. aws_cis_assessment/controls/ig1/control_iam_policies.py +383 -0
  28. aws_cis_assessment/controls/ig1/control_instance_optimization.py +100 -0
  29. aws_cis_assessment/controls/ig1/control_network_enhancements.py +203 -0
  30. aws_cis_assessment/controls/ig1/control_network_security.py +672 -0
  31. aws_cis_assessment/controls/ig1/control_s3_enhancements.py +173 -0
  32. aws_cis_assessment/controls/ig1/control_s3_security.py +422 -0
  33. aws_cis_assessment/controls/ig1/control_vpc_security.py +235 -0
  34. aws_cis_assessment/controls/ig2/__init__.py +172 -0
  35. aws_cis_assessment/controls/ig2/control_3_10.py +698 -0
  36. aws_cis_assessment/controls/ig2/control_3_11.py +1330 -0
  37. aws_cis_assessment/controls/ig2/control_5_2.py +393 -0
  38. aws_cis_assessment/controls/ig2/control_advanced_encryption.py +355 -0
  39. aws_cis_assessment/controls/ig2/control_codebuild_security.py +263 -0
  40. aws_cis_assessment/controls/ig2/control_encryption_rest.py +382 -0
  41. aws_cis_assessment/controls/ig2/control_encryption_transit.py +382 -0
  42. aws_cis_assessment/controls/ig2/control_network_ha.py +467 -0
  43. aws_cis_assessment/controls/ig2/control_remaining_encryption.py +426 -0
  44. aws_cis_assessment/controls/ig2/control_remaining_rules.py +363 -0
  45. aws_cis_assessment/controls/ig2/control_service_logging.py +402 -0
  46. aws_cis_assessment/controls/ig3/__init__.py +49 -0
  47. aws_cis_assessment/controls/ig3/control_12_8.py +395 -0
  48. aws_cis_assessment/controls/ig3/control_13_1.py +467 -0
  49. aws_cis_assessment/controls/ig3/control_3_14.py +523 -0
  50. aws_cis_assessment/controls/ig3/control_7_1.py +359 -0
  51. aws_cis_assessment/core/__init__.py +1 -0
  52. aws_cis_assessment/core/accuracy_validator.py +425 -0
  53. aws_cis_assessment/core/assessment_engine.py +1266 -0
  54. aws_cis_assessment/core/audit_trail.py +491 -0
  55. aws_cis_assessment/core/aws_client_factory.py +313 -0
  56. aws_cis_assessment/core/error_handler.py +607 -0
  57. aws_cis_assessment/core/models.py +166 -0
  58. aws_cis_assessment/core/scoring_engine.py +459 -0
  59. aws_cis_assessment/reporters/__init__.py +8 -0
  60. aws_cis_assessment/reporters/base_reporter.py +454 -0
  61. aws_cis_assessment/reporters/csv_reporter.py +835 -0
  62. aws_cis_assessment/reporters/html_reporter.py +2162 -0
  63. aws_cis_assessment/reporters/json_reporter.py +561 -0
  64. aws_cis_controls_assessment-1.0.3.dist-info/METADATA +248 -0
  65. aws_cis_controls_assessment-1.0.3.dist-info/RECORD +77 -0
  66. aws_cis_controls_assessment-1.0.3.dist-info/WHEEL +5 -0
  67. aws_cis_controls_assessment-1.0.3.dist-info/entry_points.txt +2 -0
  68. aws_cis_controls_assessment-1.0.3.dist-info/licenses/LICENSE +21 -0
  69. aws_cis_controls_assessment-1.0.3.dist-info/top_level.txt +2 -0
  70. docs/README.md +94 -0
  71. docs/assessment-logic.md +766 -0
  72. docs/cli-reference.md +698 -0
  73. docs/config-rule-mappings.md +393 -0
  74. docs/developer-guide.md +858 -0
  75. docs/installation.md +299 -0
  76. docs/troubleshooting.md +634 -0
  77. docs/user-guide.md +487 -0
@@ -0,0 +1,898 @@
1
+ """
2
+ CIS Control 3.3 - Data Protection Controls
3
+ Critical data protection rules to prevent public exposure of sensitive data.
4
+ """
5
+
6
+ import logging
7
+ from typing import List, Dict, Any, Optional
8
+ import boto3
9
+ import json
10
+ from botocore.exceptions import ClientError, NoCredentialsError
11
+
12
+ from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
13
+ from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
14
+ from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class EBSSnapshotPublicRestorableCheckAssessment(BaseConfigRuleAssessment):
20
+ """
21
+ CIS Control 3.3 - Configure Data Access Control Lists
22
+ AWS Config Rule: ebs-snapshot-public-restorable-check
23
+
24
+ Ensures EBS snapshots are not publicly restorable to prevent data exposure.
25
+ """
26
+
27
+ def __init__(self):
28
+ super().__init__(
29
+ rule_name="ebs-snapshot-public-restorable-check",
30
+ control_id="3.3",
31
+ resource_types=["AWS::EC2::Snapshot"]
32
+ )
33
+
34
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
35
+ """Get all EBS snapshots owned by the account."""
36
+ if resource_type != "AWS::EC2::Snapshot":
37
+ return []
38
+
39
+ try:
40
+ ec2_client = aws_factory.get_client('ec2', region)
41
+
42
+ # Get all EBS snapshots owned by the account
43
+ paginator = ec2_client.get_paginator('describe_snapshots')
44
+ page_iterator = paginator.paginate(OwnerIds=['self'])
45
+
46
+ snapshots = []
47
+ for page in page_iterator:
48
+ for snapshot in page['Snapshots']:
49
+ snapshots.append({
50
+ 'SnapshotId': snapshot['SnapshotId'],
51
+ 'Description': snapshot.get('Description', ''),
52
+ 'VolumeId': snapshot.get('VolumeId', ''),
53
+ 'VolumeSize': snapshot.get('VolumeSize', 0),
54
+ 'Encrypted': snapshot.get('Encrypted', False),
55
+ 'State': snapshot.get('State', ''),
56
+ 'StartTime': snapshot.get('StartTime')
57
+ })
58
+
59
+ logger.debug(f"Found {len(snapshots)} EBS snapshots in {region}")
60
+ return snapshots
61
+
62
+ except ClientError as e:
63
+ logger.error(f"Error retrieving EBS snapshots in {region}: {e}")
64
+ raise
65
+ except Exception as e:
66
+ logger.error(f"Unexpected error retrieving EBS snapshots in {region}: {e}")
67
+ raise
68
+
69
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
70
+ """Evaluate if EBS snapshot is publicly restorable."""
71
+ snapshot_id = resource.get('SnapshotId', 'unknown')
72
+
73
+ try:
74
+ ec2_client = aws_factory.get_client('ec2', region)
75
+
76
+ # Check if snapshot has public restore permissions
77
+ response = ec2_client.describe_snapshot_attribute(
78
+ SnapshotId=snapshot_id,
79
+ Attribute='createVolumePermission'
80
+ )
81
+
82
+ create_volume_permissions = response.get('CreateVolumePermissions', [])
83
+ is_public = any(
84
+ perm.get('Group') == 'all'
85
+ for perm in create_volume_permissions
86
+ )
87
+
88
+ if is_public:
89
+ return ComplianceResult(
90
+ resource_id=snapshot_id,
91
+ resource_type="AWS::EC2::Snapshot",
92
+ compliance_status=ComplianceStatus.NON_COMPLIANT,
93
+ evaluation_reason="EBS snapshot allows public restore access",
94
+ config_rule_name=self.rule_name,
95
+ region=region
96
+ )
97
+ else:
98
+ return ComplianceResult(
99
+ resource_id=snapshot_id,
100
+ resource_type="AWS::EC2::Snapshot",
101
+ compliance_status=ComplianceStatus.COMPLIANT,
102
+ evaluation_reason="EBS snapshot does not allow public restore access",
103
+ config_rule_name=self.rule_name,
104
+ region=region
105
+ )
106
+
107
+ except ClientError as e:
108
+ error_code = e.response.get('Error', {}).get('Code', '')
109
+ if error_code == 'InvalidSnapshot.NotFound':
110
+ return ComplianceResult(
111
+ resource_id=snapshot_id,
112
+ resource_type="AWS::EC2::Snapshot",
113
+ compliance_status=ComplianceStatus.NOT_APPLICABLE,
114
+ evaluation_reason="Snapshot no longer exists",
115
+ config_rule_name=self.rule_name,
116
+ region=region
117
+ )
118
+ elif error_code in ['UnauthorizedOperation', 'AccessDenied']:
119
+ return ComplianceResult(
120
+ resource_id=snapshot_id,
121
+ resource_type="AWS::EC2::Snapshot",
122
+ compliance_status=ComplianceStatus.NOT_APPLICABLE,
123
+ evaluation_reason=f"Insufficient permissions to check snapshot attributes: {error_code}",
124
+ config_rule_name=self.rule_name,
125
+ region=region
126
+ )
127
+ else:
128
+ logger.warning(f"Error checking snapshot {snapshot_id} in {region}: {e}")
129
+ return ComplianceResult(
130
+ resource_id=snapshot_id,
131
+ resource_type="AWS::EC2::Snapshot",
132
+ compliance_status=ComplianceStatus.ERROR,
133
+ evaluation_reason=f"Error checking snapshot attributes: {str(e)}",
134
+ config_rule_name=self.rule_name,
135
+ region=region
136
+ )
137
+
138
+ except Exception as e:
139
+ logger.error(f"Unexpected error checking snapshot {snapshot_id} in {region}: {e}")
140
+ return ComplianceResult(
141
+ resource_id=snapshot_id,
142
+ resource_type="AWS::EC2::Snapshot",
143
+ compliance_status=ComplianceStatus.ERROR,
144
+ evaluation_reason=f"Unexpected error: {str(e)}",
145
+ config_rule_name=self.rule_name,
146
+ region=region
147
+ )
148
+
149
+
150
+ class RDSSnapshotsPublicProhibitedAssessment(BaseConfigRuleAssessment):
151
+ """
152
+ CIS Control 3.3 - Configure Data Access Control Lists
153
+ AWS Config Rule: rds-snapshots-public-prohibited
154
+
155
+ Ensures RDS snapshots are not publicly accessible to prevent database data exposure.
156
+ """
157
+
158
+ def __init__(self):
159
+ super().__init__(
160
+ rule_name="rds-snapshots-public-prohibited",
161
+ control_id="3.3",
162
+ resource_types=["AWS::RDS::DBSnapshot", "AWS::RDS::DBClusterSnapshot"]
163
+ )
164
+
165
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
166
+ """Get all RDS snapshots (DB and cluster snapshots)."""
167
+ resources = []
168
+
169
+ try:
170
+ rds_client = aws_factory.get_client('rds', region)
171
+
172
+ if resource_type == "AWS::RDS::DBSnapshot":
173
+ # Get DB snapshots
174
+ paginator = rds_client.get_paginator('describe_db_snapshots')
175
+ for page in paginator.paginate(SnapshotType='manual'):
176
+ for snapshot in page['DBSnapshots']:
177
+ resources.append({
178
+ 'Type': 'DBSnapshot',
179
+ 'SnapshotId': snapshot['DBSnapshotIdentifier'],
180
+ 'DBInstanceIdentifier': snapshot.get('DBInstanceIdentifier', ''),
181
+ 'Engine': snapshot.get('Engine', ''),
182
+ 'Encrypted': snapshot.get('Encrypted', False),
183
+ 'SnapshotType': snapshot.get('SnapshotType', '')
184
+ })
185
+
186
+ elif resource_type == "AWS::RDS::DBClusterSnapshot":
187
+ # Get DB cluster snapshots
188
+ paginator = rds_client.get_paginator('describe_db_cluster_snapshots')
189
+ for page in paginator.paginate(SnapshotType='manual'):
190
+ for snapshot in page['DBClusterSnapshots']:
191
+ resources.append({
192
+ 'Type': 'DBClusterSnapshot',
193
+ 'SnapshotId': snapshot['DBClusterSnapshotIdentifier'],
194
+ 'DBClusterIdentifier': snapshot.get('DBClusterIdentifier', ''),
195
+ 'Engine': snapshot.get('Engine', ''),
196
+ 'Encrypted': snapshot.get('StorageEncrypted', False)
197
+ })
198
+
199
+ logger.debug(f"Found {len(resources)} RDS snapshots of type {resource_type} in {region}")
200
+ return resources
201
+
202
+ except ClientError as e:
203
+ logger.error(f"Error retrieving RDS snapshots in {region}: {e}")
204
+ raise
205
+ except Exception as e:
206
+ logger.error(f"Unexpected error retrieving RDS snapshots in {region}: {e}")
207
+ raise
208
+
209
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
210
+ """Evaluate if RDS snapshot is publicly accessible."""
211
+ snapshot_id = resource.get('SnapshotId', 'unknown')
212
+ snapshot_type = resource.get('Type', 'Unknown')
213
+
214
+ try:
215
+ rds_client = aws_factory.get_client('rds', region)
216
+
217
+ is_public = False
218
+
219
+ if snapshot_type == 'DBSnapshot':
220
+ # Check DB snapshot attributes
221
+ response = rds_client.describe_db_snapshot_attributes(
222
+ DBSnapshotIdentifier=snapshot_id
223
+ )
224
+ attributes = response.get('DBSnapshotAttributesResult', {}).get('DBSnapshotAttributes', [])
225
+
226
+ for attr in attributes:
227
+ if attr['AttributeName'] == 'restore' and 'all' in attr.get('AttributeValues', []):
228
+ is_public = True
229
+ break
230
+
231
+ resource_type = "AWS::RDS::DBSnapshot"
232
+
233
+ elif snapshot_type == 'DBClusterSnapshot':
234
+ # Check cluster snapshot attributes
235
+ response = rds_client.describe_db_cluster_snapshot_attributes(
236
+ DBClusterSnapshotIdentifier=snapshot_id
237
+ )
238
+ attributes = response.get('DBClusterSnapshotAttributesResult', {}).get('DBClusterSnapshotAttributes', [])
239
+
240
+ for attr in attributes:
241
+ if attr['AttributeName'] == 'restore' and 'all' in attr.get('AttributeValues', []):
242
+ is_public = True
243
+ break
244
+
245
+ resource_type = "AWS::RDS::DBClusterSnapshot"
246
+
247
+ else:
248
+ return ComplianceResult(
249
+ resource_id=snapshot_id,
250
+ resource_type="AWS::RDS::DBSnapshot",
251
+ compliance_status=ComplianceStatus.ERROR,
252
+ evaluation_reason=f"Unknown snapshot type: {snapshot_type}",
253
+ config_rule_name=self.rule_name,
254
+ region=region
255
+ )
256
+
257
+ if is_public:
258
+ return ComplianceResult(
259
+ resource_id=snapshot_id,
260
+ resource_type=resource_type,
261
+ compliance_status=ComplianceStatus.NON_COMPLIANT,
262
+ evaluation_reason=f"RDS {snapshot_type.lower()} allows public restore access",
263
+ config_rule_name=self.rule_name,
264
+ region=region
265
+ )
266
+ else:
267
+ return ComplianceResult(
268
+ resource_id=snapshot_id,
269
+ resource_type=resource_type,
270
+ compliance_status=ComplianceStatus.COMPLIANT,
271
+ evaluation_reason=f"RDS {snapshot_type.lower()} does not allow public restore access",
272
+ config_rule_name=self.rule_name,
273
+ region=region
274
+ )
275
+
276
+ except ClientError as e:
277
+ error_code = e.response.get('Error', {}).get('Code', '')
278
+ if error_code in ['DBSnapshotNotFoundFault', 'DBClusterSnapshotNotFoundFault']:
279
+ return ComplianceResult(
280
+ resource_id=snapshot_id,
281
+ resource_type=f"AWS::RDS::{snapshot_type}",
282
+ compliance_status=ComplianceStatus.NOT_APPLICABLE,
283
+ evaluation_reason="Snapshot no longer exists",
284
+ config_rule_name=self.rule_name,
285
+ region=region
286
+ )
287
+ elif error_code in ['UnauthorizedOperation', 'AccessDenied']:
288
+ return ComplianceResult(
289
+ resource_id=snapshot_id,
290
+ resource_type=f"AWS::RDS::{snapshot_type}",
291
+ compliance_status=ComplianceStatus.NOT_APPLICABLE,
292
+ evaluation_reason=f"Insufficient permissions to check snapshot attributes: {error_code}",
293
+ config_rule_name=self.rule_name,
294
+ region=region
295
+ )
296
+ else:
297
+ logger.warning(f"Error checking RDS snapshot {snapshot_id} in {region}: {e}")
298
+ return ComplianceResult(
299
+ resource_id=snapshot_id,
300
+ resource_type=f"AWS::RDS::{snapshot_type}",
301
+ compliance_status=ComplianceStatus.ERROR,
302
+ evaluation_reason=f"Error checking snapshot attributes: {str(e)}",
303
+ config_rule_name=self.rule_name,
304
+ region=region
305
+ )
306
+
307
+ except Exception as e:
308
+ logger.error(f"Unexpected error checking RDS snapshot {snapshot_id} in {region}: {e}")
309
+ return ComplianceResult(
310
+ resource_id=snapshot_id,
311
+ resource_type=f"AWS::RDS::{snapshot_type}",
312
+ compliance_status=ComplianceStatus.ERROR,
313
+ evaluation_reason=f"Unexpected error: {str(e)}",
314
+ config_rule_name=self.rule_name,
315
+ region=region
316
+ )
317
+ """
318
+ Evaluate RDS snapshot public access compliance.
319
+
320
+ Args:
321
+ aws_factory: AWS client factory
322
+ region: AWS region to evaluate
323
+
324
+ Returns:
325
+ List of ComplianceResult objects
326
+ """
327
+ results = []
328
+
329
+ try:
330
+ rds_client = aws_factory.get_client('rds', region)
331
+
332
+ # Check DB snapshots
333
+ db_snapshots_paginator = rds_client.get_paginator('describe_db_snapshots')
334
+ db_snapshot_count = 0
335
+
336
+ for page in db_snapshots_paginator.paginate(SnapshotType='manual'):
337
+ for snapshot in page['DBSnapshots']:
338
+ db_snapshot_count += 1
339
+ snapshot_id = snapshot['DBSnapshotIdentifier']
340
+
341
+ try:
342
+ # Check snapshot attributes for public access
343
+ response = rds_client.describe_db_snapshot_attributes(
344
+ DBSnapshotIdentifier=snapshot_id
345
+ )
346
+
347
+ attributes = response.get('DBSnapshotAttributesResult', {}).get('DBSnapshotAttributes', [])
348
+ is_public = False
349
+
350
+ for attr in attributes:
351
+ if attr['AttributeName'] == 'restore' and 'all' in attr.get('AttributeValues', []):
352
+ is_public = True
353
+ break
354
+
355
+ if is_public:
356
+ results.append(ComplianceResult(
357
+ resource_id=snapshot_id,
358
+ resource_type="AWS::RDS::DBSnapshot",
359
+ compliance_status=ComplianceStatus.NON_COMPLIANT,
360
+ evaluation_reason="RDS DB snapshot allows public restore access",
361
+ config_rule_name=self.rule_name,
362
+ region=region,
363
+ resource_details={
364
+ 'snapshot_id': snapshot_id,
365
+ 'db_instance_identifier': snapshot.get('DBInstanceIdentifier', ''),
366
+ 'engine': snapshot.get('Engine', ''),
367
+ 'encrypted': snapshot.get('Encrypted', False),
368
+ 'snapshot_type': snapshot.get('SnapshotType', ''),
369
+ 'public_attributes': attributes
370
+ },
371
+ remediation_guidance={
372
+ 'description': 'Remove public restore permissions from RDS snapshot',
373
+ 'cli_command': f'aws rds modify-db-snapshot-attribute --db-snapshot-identifier {snapshot_id} --attribute-name restore --values-to-remove all --region {region}',
374
+ 'console_url': f'https://{region}.console.aws.amazon.com/rds/home?region={region}#snapshot:id={snapshot_id}',
375
+ 'additional_info': 'Ensure only specific AWS accounts have restore permissions if sharing is required'
376
+ }
377
+ ))
378
+ else:
379
+ results.append(ComplianceResult(
380
+ resource_id=snapshot_id,
381
+ resource_type="AWS::RDS::DBSnapshot",
382
+ compliance_status=ComplianceStatus.COMPLIANT,
383
+ evaluation_reason="RDS DB snapshot does not allow public restore access",
384
+ config_rule_name=self.rule_name,
385
+ region=region,
386
+ resource_details={
387
+ 'snapshot_id': snapshot_id,
388
+ 'db_instance_identifier': snapshot.get('DBInstanceIdentifier', ''),
389
+ 'encrypted': snapshot.get('Encrypted', False)
390
+ }
391
+ ))
392
+
393
+ except ClientError as e:
394
+ error_code = e.response.get('Error', {}).get('Code', '')
395
+ if error_code == 'DBSnapshotNotFoundFault':
396
+ continue
397
+ elif error_code in ['UnauthorizedOperation', 'AccessDenied']:
398
+ results.append(ComplianceResult(
399
+ resource_id=snapshot_id,
400
+ resource_type="AWS::RDS::DBSnapshot",
401
+ compliance_status=ComplianceStatus.NOT_APPLICABLE,
402
+ evaluation_reason=f"Insufficient permissions to check snapshot attributes: {error_code}",
403
+ config_rule_name=self.rule_name,
404
+ region=region
405
+ ))
406
+ else:
407
+ logger.warning(f"Error checking RDS snapshot {snapshot_id} in {region}: {e}")
408
+
409
+ # Check DB cluster snapshots
410
+ cluster_snapshots_paginator = rds_client.get_paginator('describe_db_cluster_snapshots')
411
+ cluster_snapshot_count = 0
412
+
413
+ for page in cluster_snapshots_paginator.paginate(SnapshotType='manual'):
414
+ for snapshot in page['DBClusterSnapshots']:
415
+ cluster_snapshot_count += 1
416
+ snapshot_id = snapshot['DBClusterSnapshotIdentifier']
417
+
418
+ try:
419
+ # Check cluster snapshot attributes for public access
420
+ response = rds_client.describe_db_cluster_snapshot_attributes(
421
+ DBClusterSnapshotIdentifier=snapshot_id
422
+ )
423
+
424
+ attributes = response.get('DBClusterSnapshotAttributesResult', {}).get('DBClusterSnapshotAttributes', [])
425
+ is_public = False
426
+
427
+ for attr in attributes:
428
+ if attr['AttributeName'] == 'restore' and 'all' in attr.get('AttributeValues', []):
429
+ is_public = True
430
+ break
431
+
432
+ if is_public:
433
+ results.append(ComplianceResult(
434
+ resource_id=snapshot_id,
435
+ resource_type="AWS::RDS::DBClusterSnapshot",
436
+ compliance_status=ComplianceStatus.NON_COMPLIANT,
437
+ evaluation_reason="RDS cluster snapshot allows public restore access",
438
+ config_rule_name=self.rule_name,
439
+ region=region,
440
+ resource_details={
441
+ 'snapshot_id': snapshot_id,
442
+ 'db_cluster_identifier': snapshot.get('DBClusterIdentifier', ''),
443
+ 'engine': snapshot.get('Engine', ''),
444
+ 'encrypted': snapshot.get('StorageEncrypted', False)
445
+ },
446
+ remediation_guidance={
447
+ 'description': 'Remove public restore permissions from RDS cluster snapshot',
448
+ 'cli_command': f'aws rds modify-db-cluster-snapshot-attribute --db-cluster-snapshot-identifier {snapshot_id} --attribute-name restore --values-to-remove all --region {region}',
449
+ 'console_url': f'https://{region}.console.aws.amazon.com/rds/home?region={region}#cluster-snapshot:id={snapshot_id}',
450
+ 'additional_info': 'Ensure only specific AWS accounts have restore permissions if sharing is required'
451
+ }
452
+ ))
453
+ else:
454
+ results.append(ComplianceResult(
455
+ resource_id=snapshot_id,
456
+ resource_type="AWS::RDS::DBClusterSnapshot",
457
+ compliance_status=ComplianceStatus.COMPLIANT,
458
+ evaluation_reason="RDS cluster snapshot does not allow public restore access",
459
+ config_rule_name=self.rule_name,
460
+ region=region,
461
+ resource_details={
462
+ 'snapshot_id': snapshot_id,
463
+ 'db_cluster_identifier': snapshot.get('DBClusterIdentifier', ''),
464
+ 'encrypted': snapshot.get('StorageEncrypted', False)
465
+ }
466
+ ))
467
+
468
+ except ClientError as e:
469
+ error_code = e.response.get('Error', {}).get('Code', '')
470
+ if error_code == 'DBClusterSnapshotNotFoundFault':
471
+ continue
472
+ elif error_code in ['UnauthorizedOperation', 'AccessDenied']:
473
+ results.append(ComplianceResult(
474
+ resource_id=snapshot_id,
475
+ resource_type="AWS::RDS::DBClusterSnapshot",
476
+ compliance_status=ComplianceStatus.NOT_APPLICABLE,
477
+ evaluation_reason=f"Insufficient permissions to check cluster snapshot attributes: {error_code}",
478
+ config_rule_name=self.rule_name,
479
+ region=region
480
+ ))
481
+ else:
482
+ logger.warning(f"Error checking RDS cluster snapshot {snapshot_id} in {region}: {e}")
483
+
484
+ # If no snapshots found, return informational result
485
+ total_snapshots = db_snapshot_count + cluster_snapshot_count
486
+ if total_snapshots == 0:
487
+ results.append(ComplianceResult(
488
+ resource_id=f"no-rds-snapshots-{region}",
489
+ resource_type="AWS::RDS::DBSnapshot",
490
+ compliance_status=ComplianceStatus.NOT_APPLICABLE,
491
+ evaluation_reason="No RDS snapshots found in this region",
492
+ config_rule_name=self.rule_name,
493
+ region=region
494
+ ))
495
+
496
+ logger.info(f"Evaluated {total_snapshots} RDS snapshots in {region} ({db_snapshot_count} DB + {cluster_snapshot_count} cluster)")
497
+
498
+ except ClientError as e:
499
+ error_code = e.response.get('Error', {}).get('Code', '')
500
+ if error_code in ['UnauthorizedOperation', 'AccessDenied']:
501
+ results.append(ComplianceResult(
502
+ resource_id=f"access-denied-{region}",
503
+ resource_type="AWS::RDS::DBSnapshot",
504
+ compliance_status=ComplianceStatus.NOT_APPLICABLE,
505
+ evaluation_reason=f"Insufficient permissions to describe RDS snapshots: {error_code}",
506
+ config_rule_name=self.rule_name,
507
+ region=region
508
+ ))
509
+ else:
510
+ logger.error(f"Error evaluating RDS snapshots in {region}: {e}")
511
+ results.append(ComplianceResult(
512
+ resource_id=f"error-{region}",
513
+ resource_type="AWS::RDS::DBSnapshot",
514
+ compliance_status=ComplianceStatus.ERROR,
515
+ evaluation_reason=f"Error evaluating RDS snapshots: {str(e)}",
516
+ config_rule_name=self.rule_name,
517
+ region=region
518
+ ))
519
+
520
+ except Exception as e:
521
+ logger.error(f"Unexpected error evaluating RDS snapshots in {region}: {e}")
522
+ results.append(ComplianceResult(
523
+ resource_id=f"error-{region}",
524
+ resource_type="AWS::RDS::DBSnapshot",
525
+ compliance_status=ComplianceStatus.ERROR,
526
+ evaluation_reason=f"Unexpected error: {str(e)}",
527
+ config_rule_name=self.rule_name,
528
+ region=region
529
+ ))
530
+
531
+ return results
532
+
533
+
534
+ class RDSInstancePublicAccessCheckAssessment(BaseConfigRuleAssessment):
535
+ """
536
+ CIS Control 3.3 - Configure Data Access Control Lists
537
+ AWS Config Rule: rds-instance-public-access-check
538
+
539
+ Ensures RDS instances are not publicly accessible to prevent database exposure.
540
+ """
541
+
542
+ def __init__(self):
543
+ super().__init__(
544
+ rule_name="rds-instance-public-access-check",
545
+ control_id="3.3",
546
+ resource_types=["AWS::RDS::DBInstance", "AWS::RDS::DBCluster"]
547
+ )
548
+
549
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
550
+ """Get all RDS instances and clusters in the region."""
551
+ resources = []
552
+
553
+ try:
554
+ rds_client = aws_factory.get_client('rds', region)
555
+
556
+ if resource_type == "AWS::RDS::DBInstance":
557
+ # Get DB instances
558
+ paginator = rds_client.get_paginator('describe_db_instances')
559
+ for page in paginator.paginate():
560
+ for instance in page['DBInstances']:
561
+ resources.append({
562
+ 'Type': 'DBInstance',
563
+ 'InstanceId': instance['DBInstanceIdentifier'],
564
+ 'Engine': instance.get('Engine', ''),
565
+ 'InstanceClass': instance.get('DBInstanceClass', ''),
566
+ 'PubliclyAccessible': instance.get('PubliclyAccessible', False),
567
+ 'VpcSecurityGroups': [sg['VpcSecurityGroupId'] for sg in instance.get('VpcSecurityGroups', [])],
568
+ 'DBSubnetGroup': instance.get('DBSubnetGroup', {}).get('DBSubnetGroupName', ''),
569
+ 'Endpoint': instance.get('Endpoint', {}).get('Address', '')
570
+ })
571
+
572
+ elif resource_type == "AWS::RDS::DBCluster":
573
+ # Get DB clusters
574
+ paginator = rds_client.get_paginator('describe_db_clusters')
575
+ for page in paginator.paginate():
576
+ for cluster in page['DBClusters']:
577
+ # Check if any member instances are publicly accessible
578
+ public_members = []
579
+ for member in cluster.get('DBClusterMembers', []):
580
+ member_id = member['DBInstanceIdentifier']
581
+ try:
582
+ member_response = rds_client.describe_db_instances(DBInstanceIdentifier=member_id)
583
+ member_instance = member_response['DBInstances'][0]
584
+ if member_instance.get('PubliclyAccessible', False):
585
+ public_members.append(member_id)
586
+ except ClientError:
587
+ continue
588
+
589
+ resources.append({
590
+ 'Type': 'DBCluster',
591
+ 'ClusterId': cluster['DBClusterIdentifier'],
592
+ 'Engine': cluster.get('Engine', ''),
593
+ 'PublicMembers': public_members,
594
+ 'VpcSecurityGroups': [sg['VpcSecurityGroupId'] for sg in cluster.get('VpcSecurityGroups', [])],
595
+ 'DBSubnetGroup': cluster.get('DBSubnetGroup', ''),
596
+ 'Endpoint': cluster.get('Endpoint', '')
597
+ })
598
+
599
+ logger.debug(f"Found {len(resources)} RDS resources of type {resource_type} in {region}")
600
+ return resources
601
+
602
+ except ClientError as e:
603
+ logger.error(f"Error retrieving RDS resources in {region}: {e}")
604
+ raise
605
+ except Exception as e:
606
+ logger.error(f"Unexpected error retrieving RDS resources in {region}: {e}")
607
+ raise
608
+
609
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
610
+ """Evaluate if RDS instance or cluster is publicly accessible."""
611
+ resource_type = resource.get('Type', 'Unknown')
612
+
613
+ if resource_type == 'DBInstance':
614
+ instance_id = resource.get('InstanceId', 'unknown')
615
+ is_public = resource.get('PubliclyAccessible', False)
616
+
617
+ if is_public:
618
+ return ComplianceResult(
619
+ resource_id=instance_id,
620
+ resource_type="AWS::RDS::DBInstance",
621
+ compliance_status=ComplianceStatus.NON_COMPLIANT,
622
+ evaluation_reason="RDS instance is publicly accessible",
623
+ config_rule_name=self.rule_name,
624
+ region=region
625
+ )
626
+ else:
627
+ return ComplianceResult(
628
+ resource_id=instance_id,
629
+ resource_type="AWS::RDS::DBInstance",
630
+ compliance_status=ComplianceStatus.COMPLIANT,
631
+ evaluation_reason="RDS instance is not publicly accessible",
632
+ config_rule_name=self.rule_name,
633
+ region=region
634
+ )
635
+
636
+ elif resource_type == 'DBCluster':
637
+ cluster_id = resource.get('ClusterId', 'unknown')
638
+ public_members = resource.get('PublicMembers', [])
639
+
640
+ if public_members:
641
+ return ComplianceResult(
642
+ resource_id=cluster_id,
643
+ resource_type="AWS::RDS::DBCluster",
644
+ compliance_status=ComplianceStatus.NON_COMPLIANT,
645
+ evaluation_reason=f"RDS cluster has publicly accessible member instances: {', '.join(public_members)}",
646
+ config_rule_name=self.rule_name,
647
+ region=region
648
+ )
649
+ else:
650
+ return ComplianceResult(
651
+ resource_id=cluster_id,
652
+ resource_type="AWS::RDS::DBCluster",
653
+ compliance_status=ComplianceStatus.COMPLIANT,
654
+ evaluation_reason="RDS cluster has no publicly accessible member instances",
655
+ config_rule_name=self.rule_name,
656
+ region=region
657
+ )
658
+
659
+ else:
660
+ return ComplianceResult(
661
+ resource_id="unknown",
662
+ resource_type="AWS::RDS::DBInstance",
663
+ compliance_status=ComplianceStatus.ERROR,
664
+ evaluation_reason=f"Unknown RDS resource type: {resource_type}",
665
+ config_rule_name=self.rule_name,
666
+ region=region
667
+ )
668
+
669
+
670
+ class RedshiftClusterPublicAccessCheckAssessment(BaseConfigRuleAssessment):
671
+ """
672
+ CIS Control 3.3 - Configure Data Access Control Lists
673
+ AWS Config Rule: redshift-cluster-public-access-check
674
+
675
+ Ensures Redshift clusters are not publicly accessible to prevent data warehouse exposure.
676
+ """
677
+
678
+ def __init__(self):
679
+ super().__init__(
680
+ rule_name="redshift-cluster-public-access-check",
681
+ control_id="3.3",
682
+ resource_types=["AWS::Redshift::Cluster"]
683
+ )
684
+
685
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
686
+ """Get all Redshift clusters in the region."""
687
+ if resource_type != "AWS::Redshift::Cluster":
688
+ return []
689
+
690
+ try:
691
+ redshift_client = aws_factory.get_client('redshift', region)
692
+
693
+ # Get all Redshift clusters
694
+ paginator = redshift_client.get_paginator('describe_clusters')
695
+ clusters = []
696
+
697
+ for page in paginator.paginate():
698
+ for cluster in page['Clusters']:
699
+ clusters.append({
700
+ 'ClusterId': cluster['ClusterIdentifier'],
701
+ 'NodeType': cluster.get('NodeType', ''),
702
+ 'NumberOfNodes': cluster.get('NumberOfNodes', 0),
703
+ 'PubliclyAccessible': cluster.get('PubliclyAccessible', False),
704
+ 'VpcId': cluster.get('VpcId', ''),
705
+ 'VpcSecurityGroups': [sg['VpcSecurityGroupId'] for sg in cluster.get('VpcSecurityGroups', [])],
706
+ 'ClusterSubnetGroup': cluster.get('ClusterSubnetGroupName', ''),
707
+ 'Endpoint': cluster.get('Endpoint', {}).get('Address', '') if cluster.get('Endpoint') else '',
708
+ 'Encrypted': cluster.get('Encrypted', False)
709
+ })
710
+
711
+ logger.debug(f"Found {len(clusters)} Redshift clusters in {region}")
712
+ return clusters
713
+
714
+ except ClientError as e:
715
+ logger.error(f"Error retrieving Redshift clusters in {region}: {e}")
716
+ raise
717
+ except Exception as e:
718
+ logger.error(f"Unexpected error retrieving Redshift clusters in {region}: {e}")
719
+ raise
720
+
721
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
722
+ """Evaluate if Redshift cluster is publicly accessible."""
723
+ cluster_id = resource.get('ClusterId', 'unknown')
724
+ is_public = resource.get('PubliclyAccessible', False)
725
+
726
+ if is_public:
727
+ return ComplianceResult(
728
+ resource_id=cluster_id,
729
+ resource_type="AWS::Redshift::Cluster",
730
+ compliance_status=ComplianceStatus.NON_COMPLIANT,
731
+ evaluation_reason="Redshift cluster is publicly accessible",
732
+ config_rule_name=self.rule_name,
733
+ region=region
734
+ )
735
+ else:
736
+ return ComplianceResult(
737
+ resource_id=cluster_id,
738
+ resource_type="AWS::Redshift::Cluster",
739
+ compliance_status=ComplianceStatus.COMPLIANT,
740
+ evaluation_reason="Redshift cluster is not publicly accessible",
741
+ config_rule_name=self.rule_name,
742
+ region=region
743
+ )
744
+
745
+
746
+ class S3BucketLevelPublicAccessProhibitedAssessment(BaseConfigRuleAssessment):
747
+ """
748
+ CIS Control 3.3 - Configure Data Access Control Lists
749
+ AWS Config Rule: s3-bucket-level-public-access-prohibited
750
+
751
+ Ensures S3 buckets do not allow public access at the bucket level to prevent data exposure.
752
+ """
753
+
754
+ def __init__(self):
755
+ super().__init__(
756
+ rule_name="s3-bucket-level-public-access-prohibited",
757
+ control_id="3.3",
758
+ resource_types=["AWS::S3::Bucket"]
759
+ )
760
+
761
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
762
+ """Get all S3 buckets (only from us-east-1 to avoid duplicates)."""
763
+ if resource_type != "AWS::S3::Bucket":
764
+ return []
765
+
766
+ # S3 is global, only check from us-east-1 to avoid duplicate checks
767
+ if region != 'us-east-1':
768
+ return []
769
+
770
+ try:
771
+ s3_client = aws_factory.get_client('s3', region)
772
+
773
+ response = s3_client.list_buckets()
774
+ buckets = []
775
+
776
+ for bucket in response.get('Buckets', []):
777
+ bucket_name = bucket['Name']
778
+
779
+ try:
780
+ # Get bucket public access block configuration
781
+ pab_config = {}
782
+ try:
783
+ pab_response = s3_client.get_public_access_block(Bucket=bucket_name)
784
+ pab_config = pab_response.get('PublicAccessBlockConfiguration', {})
785
+ except ClientError as e:
786
+ if e.response.get('Error', {}).get('Code') != 'NoSuchPublicAccessBlockConfiguration':
787
+ raise e
788
+
789
+ # Check bucket ACL for public permissions
790
+ acl_response = s3_client.get_bucket_acl(Bucket=bucket_name)
791
+ grants = acl_response.get('Grants', [])
792
+
793
+ public_acl = False
794
+ for grant in grants:
795
+ grantee = grant.get('Grantee', {})
796
+ if grantee.get('Type') == 'Group':
797
+ uri = grantee.get('URI', '')
798
+ if 'AllUsers' in uri or 'AuthenticatedUsers' in uri:
799
+ public_acl = True
800
+ break
801
+
802
+ # Check bucket policy for public permissions
803
+ public_policy = False
804
+ policy_statements = []
805
+ try:
806
+ policy_response = s3_client.get_bucket_policy(Bucket=bucket_name)
807
+ policy_doc = json.loads(policy_response['Policy'])
808
+ statements = policy_doc.get('Statement', [])
809
+
810
+ for statement in statements:
811
+ if isinstance(statement, dict):
812
+ effect = statement.get('Effect', '')
813
+ principal = statement.get('Principal', {})
814
+
815
+ if effect == 'Allow':
816
+ if principal == '*' or (isinstance(principal, dict) and principal.get('AWS') == '*'):
817
+ public_policy = True
818
+ policy_statements.append(statement)
819
+
820
+ except ClientError as e:
821
+ if e.response.get('Error', {}).get('Code') != 'NoSuchBucketPolicy':
822
+ raise e
823
+
824
+ buckets.append({
825
+ 'BucketName': bucket_name,
826
+ 'PublicAccessBlock': pab_config,
827
+ 'PublicACL': public_acl,
828
+ 'PublicPolicy': public_policy,
829
+ 'PolicyStatements': policy_statements
830
+ })
831
+
832
+ except ClientError as e:
833
+ error_code = e.response.get('Error', {}).get('Code', '')
834
+ if error_code in ['NoSuchBucket', 'BucketNotEmpty', 'AccessDenied']:
835
+ continue
836
+ else:
837
+ logger.warning(f"Error checking S3 bucket {bucket_name}: {e}")
838
+ continue
839
+
840
+ logger.debug(f"Found {len(buckets)} S3 buckets from {region}")
841
+ return buckets
842
+
843
+ except ClientError as e:
844
+ logger.error(f"Error retrieving S3 buckets from {region}: {e}")
845
+ raise
846
+ except Exception as e:
847
+ logger.error(f"Unexpected error retrieving S3 buckets from {region}: {e}")
848
+ raise
849
+
850
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
851
+ """Evaluate if S3 bucket allows public access."""
852
+ bucket_name = resource.get('BucketName', 'unknown')
853
+ pab_config = resource.get('PublicAccessBlock', {})
854
+ public_acl = resource.get('PublicACL', False)
855
+ public_policy = resource.get('PublicPolicy', False)
856
+
857
+ # Check if all public access is blocked
858
+ block_public_acls = pab_config.get('BlockPublicAcls', False)
859
+ ignore_public_acls = pab_config.get('IgnorePublicAcls', False)
860
+ block_public_policy = pab_config.get('BlockPublicPolicy', False)
861
+ restrict_public_buckets = pab_config.get('RestrictPublicBuckets', False)
862
+
863
+ all_blocked = all([
864
+ block_public_acls,
865
+ ignore_public_acls,
866
+ block_public_policy,
867
+ restrict_public_buckets
868
+ ])
869
+
870
+ # Determine compliance
871
+ has_public_access = public_acl or public_policy or not all_blocked
872
+
873
+ if has_public_access:
874
+ issues = []
875
+ if not all_blocked:
876
+ issues.append("Public Access Block not fully configured")
877
+ if public_acl:
878
+ issues.append("Bucket ACL allows public access")
879
+ if public_policy:
880
+ issues.append("Bucket policy allows public access")
881
+
882
+ return ComplianceResult(
883
+ resource_id=bucket_name,
884
+ resource_type="AWS::S3::Bucket",
885
+ compliance_status=ComplianceStatus.NON_COMPLIANT,
886
+ evaluation_reason=f"S3 bucket allows public access: {', '.join(issues)}",
887
+ config_rule_name=self.rule_name,
888
+ region=region
889
+ )
890
+ else:
891
+ return ComplianceResult(
892
+ resource_id=bucket_name,
893
+ resource_type="AWS::S3::Bucket",
894
+ compliance_status=ComplianceStatus.COMPLIANT,
895
+ evaluation_reason="S3 bucket blocks all public access",
896
+ config_rule_name=self.rule_name,
897
+ region=region
898
+ )