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,718 @@
1
+ """Control 3.3: Configure Data Access Control Lists assessments."""
2
+
3
+ from typing import Dict, List, Any, Optional
4
+ import json
5
+ import logging
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 IAMPasswordPolicyAssessment(BaseConfigRuleAssessment):
16
+ """Assessment for iam-password-policy Config rule - ensures strong password policy."""
17
+
18
+ def __init__(self, parameters: Optional[Dict[str, Any]] = None):
19
+ """Initialize IAM password policy assessment."""
20
+ default_params = {
21
+ 'MinimumPasswordLength': 14,
22
+ 'RequireSymbols': True,
23
+ 'RequireNumbers': True,
24
+ 'RequireUppercaseCharacters': True,
25
+ 'RequireLowercaseCharacters': True,
26
+ 'MaxPasswordAge': 90,
27
+ 'PasswordReusePrevention': 24,
28
+ 'AllowUsersToChangePassword': True,
29
+ 'HardExpiry': False
30
+ }
31
+
32
+ if parameters:
33
+ # Validate parameter types and ranges
34
+ validated_params = self._validate_parameters(parameters)
35
+ default_params.update(validated_params)
36
+
37
+ super().__init__(
38
+ rule_name="iam-password-policy",
39
+ control_id="5.2", # Updated to reflect Control 5.2 for password management
40
+ resource_types=["AWS::::Account"],
41
+ parameters=default_params
42
+ )
43
+
44
+ def _validate_parameters(self, parameters: Dict[str, Any]) -> Dict[str, Any]:
45
+ """Validate and sanitize input parameters."""
46
+ validated = {}
47
+
48
+ # Validate MinimumPasswordLength
49
+ if 'MinimumPasswordLength' in parameters:
50
+ min_length = parameters['MinimumPasswordLength']
51
+ if isinstance(min_length, int) and 6 <= min_length <= 128:
52
+ validated['MinimumPasswordLength'] = min_length
53
+ else:
54
+ logger.warning(f"Invalid MinimumPasswordLength: {min_length}. Must be integer between 6-128. Using default: 14")
55
+
56
+ # Validate MaxPasswordAge
57
+ if 'MaxPasswordAge' in parameters:
58
+ max_age = parameters['MaxPasswordAge']
59
+ if isinstance(max_age, int) and 1 <= max_age <= 1095:
60
+ validated['MaxPasswordAge'] = max_age
61
+ else:
62
+ logger.warning(f"Invalid MaxPasswordAge: {max_age}. Must be integer between 1-1095 days. Using default: 90")
63
+
64
+ # Validate PasswordReusePrevention
65
+ if 'PasswordReusePrevention' in parameters:
66
+ reuse_prevention = parameters['PasswordReusePrevention']
67
+ if isinstance(reuse_prevention, int) and 1 <= reuse_prevention <= 24:
68
+ validated['PasswordReusePrevention'] = reuse_prevention
69
+ else:
70
+ logger.warning(f"Invalid PasswordReusePrevention: {reuse_prevention}. Must be integer between 1-24. Using default: 24")
71
+
72
+ # Validate boolean parameters
73
+ boolean_params = [
74
+ 'RequireSymbols', 'RequireNumbers', 'RequireUppercaseCharacters',
75
+ 'RequireLowercaseCharacters', 'AllowUsersToChangePassword', 'HardExpiry'
76
+ ]
77
+
78
+ for param in boolean_params:
79
+ if param in parameters:
80
+ value = parameters[param]
81
+ if isinstance(value, bool):
82
+ validated[param] = value
83
+ elif isinstance(value, str) and value.lower() in ['true', '1', 'yes', 'on', 'false', '0', 'no', 'off']:
84
+ validated[param] = value.lower() in ['true', '1', 'yes', 'on']
85
+ else:
86
+ logger.warning(f"Invalid {param}: {value}. Must be boolean. Using default.")
87
+ # Don't add to validated, will use default
88
+
89
+ return validated
90
+
91
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
92
+ """Get account-level resource for password policy check."""
93
+ if resource_type != "AWS::::Account":
94
+ return []
95
+
96
+ # Account-level resource - return single item representing the account
97
+ try:
98
+ account_info = aws_factory.get_account_info()
99
+ return [{
100
+ 'AccountId': account_info.get('account_id', 'unknown'),
101
+ 'ResourceType': 'AWS::::Account'
102
+ }]
103
+ except Exception as e:
104
+ logger.error(f"Error getting account info: {e}")
105
+ return [{
106
+ 'AccountId': 'unknown',
107
+ 'ResourceType': 'AWS::::Account'
108
+ }]
109
+
110
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
111
+ """Evaluate IAM password policy compliance."""
112
+ account_id = resource.get('AccountId', 'unknown')
113
+
114
+ try:
115
+ iam_client = aws_factory.get_client('iam', region)
116
+
117
+ # Get password policy
118
+ response = aws_factory.aws_api_call_with_retry(
119
+ lambda: iam_client.get_account_password_policy()
120
+ )
121
+
122
+ policy = response.get('PasswordPolicy', {})
123
+
124
+ # Check each requirement
125
+ violations = []
126
+
127
+ # Minimum password length
128
+ min_length = policy.get('MinimumPasswordLength', 0)
129
+ required_min_length = self.parameters.get('MinimumPasswordLength', 14)
130
+ if min_length < required_min_length:
131
+ violations.append(f"Minimum password length is {min_length}, required: {required_min_length}")
132
+
133
+ # Character requirements
134
+ if self.parameters.get('RequireSymbols', True) and not policy.get('RequireSymbols', False):
135
+ violations.append("Password policy does not require symbols")
136
+
137
+ if self.parameters.get('RequireNumbers', True) and not policy.get('RequireNumbers', False):
138
+ violations.append("Password policy does not require numbers")
139
+
140
+ if self.parameters.get('RequireUppercaseCharacters', True) and not policy.get('RequireUppercaseCharacters', False):
141
+ violations.append("Password policy does not require uppercase characters")
142
+
143
+ if self.parameters.get('RequireLowercaseCharacters', True) and not policy.get('RequireLowercaseCharacters', False):
144
+ violations.append("Password policy does not require lowercase characters")
145
+
146
+ # Password age
147
+ max_age = policy.get('MaxPasswordAge')
148
+ required_max_age = self.parameters.get('MaxPasswordAge', 90)
149
+ if max_age is None or max_age > required_max_age:
150
+ violations.append(f"Maximum password age is {max_age or 'unlimited'}, required: {required_max_age} days")
151
+
152
+ # Password reuse prevention
153
+ reuse_prevention = policy.get('PasswordReusePrevention', 0)
154
+ required_reuse_prevention = self.parameters.get('PasswordReusePrevention', 24)
155
+ if reuse_prevention < required_reuse_prevention:
156
+ violations.append(f"Password reuse prevention is {reuse_prevention}, required: {required_reuse_prevention}")
157
+
158
+ # Allow users to change password
159
+ allow_change = policy.get('AllowUsersToChangePassword', False)
160
+ required_allow_change = self.parameters.get('AllowUsersToChangePassword', True)
161
+ if required_allow_change and not allow_change:
162
+ violations.append("Password policy does not allow users to change their own passwords")
163
+ elif not required_allow_change and allow_change:
164
+ violations.append("Password policy allows users to change passwords when it should not")
165
+
166
+ # Hard expiry
167
+ hard_expiry = policy.get('HardExpiry', False)
168
+ required_hard_expiry = self.parameters.get('HardExpiry', False)
169
+ if required_hard_expiry and not hard_expiry:
170
+ violations.append("Password policy does not enforce hard expiry")
171
+ elif not required_hard_expiry and hard_expiry:
172
+ violations.append("Password policy enforces hard expiry when it should not")
173
+
174
+ if violations:
175
+ compliance_status = ComplianceStatus.NON_COMPLIANT
176
+ evaluation_reason = f"IAM password policy violations: {'; '.join(violations)}"
177
+ else:
178
+ compliance_status = ComplianceStatus.COMPLIANT
179
+ evaluation_reason = "IAM password policy meets all security requirements"
180
+
181
+ except ClientError as e:
182
+ error_code = e.response.get('Error', {}).get('Code', '')
183
+ if error_code == 'NoSuchEntity':
184
+ compliance_status = ComplianceStatus.NON_COMPLIANT
185
+ evaluation_reason = "No IAM password policy is configured"
186
+ elif error_code in ['AccessDenied', 'UnauthorizedOperation']:
187
+ compliance_status = ComplianceStatus.ERROR
188
+ evaluation_reason = "Insufficient permissions to check IAM password policy"
189
+ else:
190
+ compliance_status = ComplianceStatus.ERROR
191
+ evaluation_reason = f"Error checking IAM password policy: {str(e)}"
192
+ except Exception as e:
193
+ compliance_status = ComplianceStatus.ERROR
194
+ evaluation_reason = f"Unexpected error checking IAM password policy: {str(e)}"
195
+
196
+ return ComplianceResult(
197
+ resource_id=account_id,
198
+ resource_type="AWS::::Account",
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 specific remediation steps for IAM password policy."""
207
+ return [
208
+ "Update the IAM password policy to meet security requirements:",
209
+ f" - Set minimum password length to {self.parameters.get('MinimumPasswordLength', 14)} characters",
210
+ " - Require symbols, numbers, uppercase and lowercase characters",
211
+ f" - Set maximum password age to {self.parameters.get('MaxPasswordAge', 90)} days",
212
+ f" - Set password reuse prevention to {self.parameters.get('PasswordReusePrevention', 24)} passwords",
213
+ f" - {'Allow' if self.parameters.get('AllowUsersToChangePassword', True) else 'Disallow'} users to change their own passwords",
214
+ f" - {'Enable' if self.parameters.get('HardExpiry', False) else 'Disable'} hard expiry for passwords",
215
+ "Use AWS CLI command:",
216
+ f" aws iam update-account-password-policy \\",
217
+ f" --minimum-password-length {self.parameters.get('MinimumPasswordLength', 14)} \\",
218
+ f" {'--require-symbols' if self.parameters.get('RequireSymbols', True) else '--no-require-symbols'} \\",
219
+ f" {'--require-numbers' if self.parameters.get('RequireNumbers', True) else '--no-require-numbers'} \\",
220
+ f" {'--require-uppercase-characters' if self.parameters.get('RequireUppercaseCharacters', True) else '--no-require-uppercase-characters'} \\",
221
+ f" {'--require-lowercase-characters' if self.parameters.get('RequireLowercaseCharacters', True) else '--no-require-lowercase-characters'} \\",
222
+ f" --max-password-age {self.parameters.get('MaxPasswordAge', 90)} \\",
223
+ f" --password-reuse-prevention {self.parameters.get('PasswordReusePrevention', 24)} \\",
224
+ f" {'--allow-users-to-change-password' if self.parameters.get('AllowUsersToChangePassword', True) else '--no-allow-users-to-change-password'}" +
225
+ (f" \\\n {'--hard-expiry' if self.parameters.get('HardExpiry', False) else '--no-hard-expiry'}" if 'HardExpiry' in self.parameters else ""),
226
+ "Or use AWS Console: IAM > Account settings > Password policy",
227
+ "Additional recommendations:",
228
+ " - Communicate password policy changes to all users in advance",
229
+ " - Provide password manager recommendations to users",
230
+ " - Consider implementing password complexity training",
231
+ " - Monitor password policy compliance regularly"
232
+ ]
233
+
234
+
235
+ class IAMUserMFAEnabledAssessment(BaseConfigRuleAssessment):
236
+ """Assessment for iam-user-mfa-enabled Config rule - ensures IAM users have MFA."""
237
+
238
+ def __init__(self):
239
+ """Initialize IAM user MFA assessment."""
240
+ super().__init__(
241
+ rule_name="iam-user-mfa-enabled",
242
+ control_id="3.3",
243
+ resource_types=["AWS::IAM::User"]
244
+ )
245
+
246
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
247
+ """Get all IAM users."""
248
+ if resource_type != "AWS::IAM::User":
249
+ return []
250
+
251
+ try:
252
+ iam_client = aws_factory.get_client('iam', region)
253
+
254
+ users = []
255
+ paginator = iam_client.get_paginator('list_users')
256
+
257
+ for page in paginator.paginate():
258
+ for user in page.get('Users', []):
259
+ users.append({
260
+ 'UserName': user.get('UserName'),
261
+ 'UserId': user.get('UserId'),
262
+ 'Arn': user.get('Arn'),
263
+ 'CreateDate': user.get('CreateDate'),
264
+ 'PasswordLastUsed': user.get('PasswordLastUsed'),
265
+ 'Tags': user.get('Tags', [])
266
+ })
267
+
268
+ logger.debug(f"Found {len(users)} IAM users")
269
+ return users
270
+
271
+ except ClientError as e:
272
+ logger.error(f"Error retrieving IAM users: {e}")
273
+ raise
274
+ except Exception as e:
275
+ logger.error(f"Unexpected error retrieving IAM users: {e}")
276
+ raise
277
+
278
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
279
+ """Evaluate if IAM user has MFA enabled."""
280
+ user_name = resource.get('UserName', 'unknown')
281
+
282
+ try:
283
+ iam_client = aws_factory.get_client('iam', region)
284
+
285
+ # Check if user has console access (login profile)
286
+ has_console_access = False
287
+ try:
288
+ aws_factory.aws_api_call_with_retry(
289
+ lambda: iam_client.get_login_profile(UserName=user_name)
290
+ )
291
+ has_console_access = True
292
+ except ClientError as e:
293
+ if e.response.get('Error', {}).get('Code') != 'NoSuchEntity':
294
+ raise
295
+
296
+ # If no console access, user doesn't need MFA
297
+ if not has_console_access:
298
+ return ComplianceResult(
299
+ resource_id=user_name,
300
+ resource_type="AWS::IAM::User",
301
+ compliance_status=ComplianceStatus.NOT_APPLICABLE,
302
+ evaluation_reason=f"User {user_name} does not have console access",
303
+ config_rule_name=self.rule_name,
304
+ region=region
305
+ )
306
+
307
+ # Check MFA devices
308
+ response = aws_factory.aws_api_call_with_retry(
309
+ lambda: iam_client.list_mfa_devices(UserName=user_name)
310
+ )
311
+
312
+ mfa_devices = response.get('MFADevices', [])
313
+
314
+ if mfa_devices:
315
+ compliance_status = ComplianceStatus.COMPLIANT
316
+ device_count = len(mfa_devices)
317
+ evaluation_reason = f"User {user_name} has {device_count} MFA device(s) configured"
318
+ else:
319
+ compliance_status = ComplianceStatus.NON_COMPLIANT
320
+ evaluation_reason = f"User {user_name} has console access but no MFA devices configured"
321
+
322
+ except ClientError as e:
323
+ error_code = e.response.get('Error', {}).get('Code', '')
324
+ if error_code in ['AccessDenied', 'UnauthorizedOperation']:
325
+ compliance_status = ComplianceStatus.ERROR
326
+ evaluation_reason = f"Insufficient permissions to check MFA for user {user_name}"
327
+ else:
328
+ compliance_status = ComplianceStatus.ERROR
329
+ evaluation_reason = f"Error checking MFA for user {user_name}: {str(e)}"
330
+ except Exception as e:
331
+ compliance_status = ComplianceStatus.ERROR
332
+ evaluation_reason = f"Unexpected error checking MFA for user {user_name}: {str(e)}"
333
+
334
+ return ComplianceResult(
335
+ resource_id=user_name,
336
+ resource_type="AWS::IAM::User",
337
+ compliance_status=compliance_status,
338
+ evaluation_reason=evaluation_reason,
339
+ config_rule_name=self.rule_name,
340
+ region=region
341
+ )
342
+
343
+ def _get_rule_remediation_steps(self) -> List[str]:
344
+ """Get specific remediation steps for IAM user MFA."""
345
+ return [
346
+ "Enable MFA for all IAM users with console access:",
347
+ "1. Identify users without MFA who have console access",
348
+ "2. For each user, enable MFA using one of these methods:",
349
+ " - Virtual MFA device (Google Authenticator, Authy, etc.)",
350
+ " - Hardware MFA device (YubiKey, etc.)",
351
+ " - SMS MFA (not recommended for high security)",
352
+ "Use AWS CLI: aws iam enable-mfa-device --user-name <username> --serial-number <device-arn> --authentication-code1 <code1> --authentication-code2 <code2>",
353
+ "Or use AWS Console: IAM > Users > [User] > Security credentials > Multi-factor authentication",
354
+ "Consider enforcing MFA through IAM policies",
355
+ "Provide MFA setup instructions to users"
356
+ ]
357
+
358
+
359
+ class IAMRootAccessKeyAssessment(BaseConfigRuleAssessment):
360
+ """Assessment for iam-root-access-key-check Config rule - ensures root has no access keys."""
361
+
362
+ def __init__(self):
363
+ """Initialize IAM root access key assessment."""
364
+ super().__init__(
365
+ rule_name="iam-root-access-key-check",
366
+ control_id="3.3",
367
+ resource_types=["AWS::::Account"]
368
+ )
369
+
370
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
371
+ """Get account-level resource for root access key check."""
372
+ if resource_type != "AWS::::Account":
373
+ return []
374
+
375
+ try:
376
+ account_info = aws_factory.get_account_info()
377
+ return [{
378
+ 'AccountId': account_info.get('account_id', 'unknown'),
379
+ 'ResourceType': 'AWS::::Account'
380
+ }]
381
+ except Exception as e:
382
+ logger.error(f"Error getting account info: {e}")
383
+ return [{
384
+ 'AccountId': 'unknown',
385
+ 'ResourceType': 'AWS::::Account'
386
+ }]
387
+
388
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
389
+ """Evaluate if root user has access keys."""
390
+ account_id = resource.get('AccountId', 'unknown')
391
+
392
+ try:
393
+ iam_client = aws_factory.get_client('iam', region)
394
+
395
+ # Get account summary which includes root access key info
396
+ response = aws_factory.aws_api_call_with_retry(
397
+ lambda: iam_client.get_account_summary()
398
+ )
399
+
400
+ summary = response.get('SummaryMap', {})
401
+
402
+ # Check for root access keys
403
+ root_access_keys_present = summary.get('AccountAccessKeysPresent', 0)
404
+
405
+ if root_access_keys_present > 0:
406
+ compliance_status = ComplianceStatus.NON_COMPLIANT
407
+ evaluation_reason = f"Root user has {root_access_keys_present} access key(s) - this is a security risk"
408
+ else:
409
+ compliance_status = ComplianceStatus.COMPLIANT
410
+ evaluation_reason = "Root user has no access keys configured"
411
+
412
+ except ClientError as e:
413
+ error_code = e.response.get('Error', {}).get('Code', '')
414
+ if error_code in ['AccessDenied', 'UnauthorizedOperation']:
415
+ compliance_status = ComplianceStatus.ERROR
416
+ evaluation_reason = "Insufficient permissions to check root access keys"
417
+ else:
418
+ compliance_status = ComplianceStatus.ERROR
419
+ evaluation_reason = f"Error checking root access keys: {str(e)}"
420
+ except Exception as e:
421
+ compliance_status = ComplianceStatus.ERROR
422
+ evaluation_reason = f"Unexpected error checking root access keys: {str(e)}"
423
+
424
+ return ComplianceResult(
425
+ resource_id=account_id,
426
+ resource_type="AWS::::Account",
427
+ compliance_status=compliance_status,
428
+ evaluation_reason=evaluation_reason,
429
+ config_rule_name=self.rule_name,
430
+ region=region
431
+ )
432
+
433
+ def _get_rule_remediation_steps(self) -> List[str]:
434
+ """Get specific remediation steps for root access keys."""
435
+ return [
436
+ "Remove root user access keys immediately:",
437
+ "1. Log in to AWS Console as root user",
438
+ "2. Go to 'My Security Credentials' in the account menu",
439
+ "3. In the 'Access keys' section, delete any existing access keys",
440
+ "4. Create IAM users with appropriate permissions instead",
441
+ "5. Use IAM roles for applications and services",
442
+ "Alternative using AWS CLI (if you have the keys):",
443
+ " aws iam delete-access-key --access-key-id <access-key-id>",
444
+ "Best practices:",
445
+ " - Never use root credentials for daily operations",
446
+ " - Enable MFA on the root account",
447
+ " - Use IAM users and roles for all programmatic access"
448
+ ]
449
+
450
+
451
+ class S3BucketPublicReadProhibitedAssessment(BaseConfigRuleAssessment):
452
+ """Assessment for s3-bucket-public-read-prohibited Config rule."""
453
+
454
+ def __init__(self):
455
+ """Initialize S3 bucket public read assessment."""
456
+ super().__init__(
457
+ rule_name="s3-bucket-public-read-prohibited",
458
+ control_id="3.3",
459
+ resource_types=["AWS::S3::Bucket"]
460
+ )
461
+
462
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
463
+ """Get all S3 buckets."""
464
+ if resource_type != "AWS::S3::Bucket":
465
+ return []
466
+
467
+ try:
468
+ s3_client = aws_factory.get_client('s3', region)
469
+
470
+ response = aws_factory.aws_api_call_with_retry(
471
+ lambda: s3_client.list_buckets()
472
+ )
473
+
474
+ buckets = []
475
+ for bucket in response.get('Buckets', []):
476
+ buckets.append({
477
+ 'Name': bucket.get('Name'),
478
+ 'CreationDate': bucket.get('CreationDate')
479
+ })
480
+
481
+ logger.debug(f"Found {len(buckets)} S3 buckets")
482
+ return buckets
483
+
484
+ except ClientError as e:
485
+ logger.error(f"Error retrieving S3 buckets: {e}")
486
+ raise
487
+ except Exception as e:
488
+ logger.error(f"Unexpected error retrieving S3 buckets: {e}")
489
+ raise
490
+
491
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
492
+ """Evaluate if S3 bucket allows public read access."""
493
+ bucket_name = resource.get('Name', 'unknown')
494
+
495
+ try:
496
+ s3_client = aws_factory.get_client('s3', region)
497
+
498
+ public_access_issues = []
499
+
500
+ # Check bucket ACL
501
+ try:
502
+ acl_response = aws_factory.aws_api_call_with_retry(
503
+ lambda: s3_client.get_bucket_acl(Bucket=bucket_name)
504
+ )
505
+
506
+ for grant in acl_response.get('Grants', []):
507
+ grantee = grant.get('Grantee', {})
508
+ permission = grant.get('Permission', '')
509
+
510
+ # Check for public read permissions
511
+ if grantee.get('Type') == 'Group':
512
+ uri = grantee.get('URI', '')
513
+ if 'AllUsers' in uri and permission in ['READ', 'FULL_CONTROL']:
514
+ public_access_issues.append(f"Bucket ACL grants {permission} to AllUsers")
515
+ elif 'AuthenticatedUsers' in uri and permission in ['READ', 'FULL_CONTROL']:
516
+ public_access_issues.append(f"Bucket ACL grants {permission} to AuthenticatedUsers")
517
+
518
+ except ClientError as e:
519
+ if e.response.get('Error', {}).get('Code') not in ['AccessDenied', 'NoSuchBucket']:
520
+ raise
521
+
522
+ # Check bucket policy
523
+ try:
524
+ policy_response = aws_factory.aws_api_call_with_retry(
525
+ lambda: s3_client.get_bucket_policy(Bucket=bucket_name)
526
+ )
527
+
528
+ policy_doc = json.loads(policy_response.get('Policy', '{}'))
529
+
530
+ for statement in policy_doc.get('Statement', []):
531
+ effect = statement.get('Effect', '')
532
+ principal = statement.get('Principal', {})
533
+ action = statement.get('Action', [])
534
+
535
+ if effect == 'Allow':
536
+ # Check for public principals
537
+ if principal == '*' or (isinstance(principal, dict) and principal.get('AWS') == '*'):
538
+ # Check for read actions
539
+ actions = action if isinstance(action, list) else [action]
540
+ for act in actions:
541
+ if any(read_action in act for read_action in ['s3:GetObject', 's3:ListBucket', 's3:*']):
542
+ public_access_issues.append(f"Bucket policy allows public {act}")
543
+
544
+ except ClientError as e:
545
+ error_code = e.response.get('Error', {}).get('Code', '')
546
+ if error_code not in ['NoSuchBucketPolicy', 'AccessDenied', 'NoSuchBucket']:
547
+ raise
548
+
549
+ # Check public access block settings
550
+ try:
551
+ pab_response = aws_factory.aws_api_call_with_retry(
552
+ lambda: s3_client.get_public_access_block(Bucket=bucket_name)
553
+ )
554
+
555
+ pab_config = pab_response.get('PublicAccessBlockConfiguration', {})
556
+
557
+ if not pab_config.get('BlockPublicAcls', False):
558
+ public_access_issues.append("Public Access Block does not block public ACLs")
559
+ if not pab_config.get('IgnorePublicAcls', False):
560
+ public_access_issues.append("Public Access Block does not ignore public ACLs")
561
+ if not pab_config.get('BlockPublicPolicy', False):
562
+ public_access_issues.append("Public Access Block does not block public policies")
563
+ if not pab_config.get('RestrictPublicBuckets', False):
564
+ public_access_issues.append("Public Access Block does not restrict public buckets")
565
+
566
+ except ClientError as e:
567
+ error_code = e.response.get('Error', {}).get('Code', '')
568
+ if error_code == 'NoSuchPublicAccessBlockConfiguration':
569
+ public_access_issues.append("No Public Access Block configuration found")
570
+ elif error_code not in ['AccessDenied', 'NoSuchBucket']:
571
+ raise
572
+
573
+ if public_access_issues:
574
+ compliance_status = ComplianceStatus.NON_COMPLIANT
575
+ evaluation_reason = f"Bucket {bucket_name} has public read access issues: {'; '.join(public_access_issues)}"
576
+ else:
577
+ compliance_status = ComplianceStatus.COMPLIANT
578
+ evaluation_reason = f"Bucket {bucket_name} does not allow public read access"
579
+
580
+ except ClientError as e:
581
+ error_code = e.response.get('Error', {}).get('Code', '')
582
+ if error_code == 'NoSuchBucket':
583
+ compliance_status = ComplianceStatus.NOT_APPLICABLE
584
+ evaluation_reason = f"Bucket {bucket_name} does not exist"
585
+ elif error_code in ['AccessDenied', 'UnauthorizedOperation']:
586
+ compliance_status = ComplianceStatus.ERROR
587
+ evaluation_reason = f"Insufficient permissions to check bucket {bucket_name}"
588
+ else:
589
+ compliance_status = ComplianceStatus.ERROR
590
+ evaluation_reason = f"Error checking bucket {bucket_name}: {str(e)}"
591
+ except Exception as e:
592
+ compliance_status = ComplianceStatus.ERROR
593
+ evaluation_reason = f"Unexpected error checking bucket {bucket_name}: {str(e)}"
594
+
595
+ return ComplianceResult(
596
+ resource_id=bucket_name,
597
+ resource_type="AWS::S3::Bucket",
598
+ compliance_status=compliance_status,
599
+ evaluation_reason=evaluation_reason,
600
+ config_rule_name=self.rule_name,
601
+ region=region
602
+ )
603
+
604
+ def _get_rule_remediation_steps(self) -> List[str]:
605
+ """Get specific remediation steps for S3 public read access."""
606
+ return [
607
+ "Remove public read access from S3 buckets:",
608
+ "1. Enable S3 Public Access Block at bucket level:",
609
+ " aws s3api put-public-access-block --bucket <bucket-name> --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true",
610
+ "2. Review and remove public permissions from bucket ACLs:",
611
+ " aws s3api get-bucket-acl --bucket <bucket-name>",
612
+ " aws s3api put-bucket-acl --bucket <bucket-name> --acl private",
613
+ "3. Review and update bucket policies to remove public access:",
614
+ " aws s3api get-bucket-policy --bucket <bucket-name>",
615
+ " aws s3api delete-bucket-policy --bucket <bucket-name> # if policy grants public access",
616
+ "4. Use CloudFront or signed URLs for legitimate public content access",
617
+ "5. Regularly audit bucket permissions using AWS Config or Trusted Advisor"
618
+ ]
619
+
620
+
621
+ class EC2InstanceNoPublicIPAssessment(BaseConfigRuleAssessment):
622
+ """Assessment for ec2-instance-no-public-ip Config rule."""
623
+
624
+ def __init__(self):
625
+ """Initialize EC2 instance no public IP assessment."""
626
+ super().__init__(
627
+ rule_name="ec2-instance-no-public-ip",
628
+ control_id="3.3",
629
+ resource_types=["AWS::EC2::Instance"]
630
+ )
631
+
632
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
633
+ """Get all EC2 instances."""
634
+ if resource_type != "AWS::EC2::Instance":
635
+ return []
636
+
637
+ try:
638
+ ec2_client = aws_factory.get_client('ec2', region)
639
+
640
+ response = aws_factory.aws_api_call_with_retry(
641
+ lambda: ec2_client.describe_instances()
642
+ )
643
+
644
+ instances = []
645
+ for reservation in response.get('Reservations', []):
646
+ for instance in reservation.get('Instances', []):
647
+ instances.append({
648
+ 'InstanceId': instance.get('InstanceId'),
649
+ 'State': instance.get('State', {}),
650
+ 'PublicIpAddress': instance.get('PublicIpAddress'),
651
+ 'PrivateIpAddress': instance.get('PrivateIpAddress'),
652
+ 'SubnetId': instance.get('SubnetId'),
653
+ 'VpcId': instance.get('VpcId'),
654
+ 'Tags': instance.get('Tags', [])
655
+ })
656
+
657
+ logger.debug(f"Found {len(instances)} EC2 instances in region {region}")
658
+ return instances
659
+
660
+ except ClientError as e:
661
+ logger.error(f"Error retrieving EC2 instances in region {region}: {e}")
662
+ raise
663
+ except Exception as e:
664
+ logger.error(f"Unexpected error retrieving EC2 instances in region {region}: {e}")
665
+ raise
666
+
667
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
668
+ """Evaluate if EC2 instance has a public IP address."""
669
+ instance_id = resource.get('InstanceId', 'unknown')
670
+ state = resource.get('State', {})
671
+ state_name = state.get('Name', 'unknown')
672
+ public_ip = resource.get('PublicIpAddress')
673
+
674
+ # Only evaluate running instances
675
+ if state_name not in ['running', 'stopped']:
676
+ return ComplianceResult(
677
+ resource_id=instance_id,
678
+ resource_type="AWS::EC2::Instance",
679
+ compliance_status=ComplianceStatus.NOT_APPLICABLE,
680
+ evaluation_reason=f"Instance {instance_id} is in state '{state_name}'",
681
+ config_rule_name=self.rule_name,
682
+ region=region
683
+ )
684
+
685
+ if public_ip:
686
+ compliance_status = ComplianceStatus.NON_COMPLIANT
687
+ evaluation_reason = f"Instance {instance_id} has public IP address {public_ip}"
688
+ else:
689
+ compliance_status = ComplianceStatus.COMPLIANT
690
+ evaluation_reason = f"Instance {instance_id} does not have a public IP address"
691
+
692
+ return ComplianceResult(
693
+ resource_id=instance_id,
694
+ resource_type="AWS::EC2::Instance",
695
+ compliance_status=compliance_status,
696
+ evaluation_reason=evaluation_reason,
697
+ config_rule_name=self.rule_name,
698
+ region=region
699
+ )
700
+
701
+ def _get_rule_remediation_steps(self) -> List[str]:
702
+ """Get specific remediation steps for EC2 instances with public IPs."""
703
+ return [
704
+ "Remove public IP addresses from EC2 instances:",
705
+ "1. For existing instances with public IPs:",
706
+ " - Stop the instance if it has a dynamic public IP",
707
+ " - Disassociate Elastic IP if attached: aws ec2 disassociate-address --association-id <assoc-id>",
708
+ " - Start the instance without public IP assignment",
709
+ "2. For new instances, launch in private subnets:",
710
+ " - Use subnets that don't auto-assign public IPs",
711
+ " - Set 'Auto-assign Public IP' to 'Disable'",
712
+ "3. Set up internet access through NAT Gateway or NAT Instance:",
713
+ " - Create NAT Gateway in public subnet",
714
+ " - Route private subnet traffic through NAT Gateway",
715
+ "4. Use Application Load Balancer or CloudFront for web applications",
716
+ "5. Use VPN or Direct Connect for administrative access",
717
+ "6. Consider using AWS Systems Manager Session Manager for secure shell access"
718
+ ]