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,393 @@
1
+ """Control 5.2: Use Unique Passwords assessments."""
2
+
3
+ from typing import Dict, List, Any, Optional
4
+ import json
5
+ import logging
6
+ from datetime import datetime, timedelta
7
+ from botocore.exceptions import ClientError
8
+
9
+ from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
10
+ from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
11
+ from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class MFAEnabledForIAMConsoleAccessAssessment(BaseConfigRuleAssessment):
17
+ """Assessment for mfa-enabled-for-iam-console-access Config rule - ensures MFA for console access."""
18
+
19
+ def __init__(self):
20
+ """Initialize MFA enabled for IAM console access assessment."""
21
+ super().__init__(
22
+ rule_name="mfa-enabled-for-iam-console-access",
23
+ control_id="5.2",
24
+ resource_types=["AWS::IAM::User"]
25
+ )
26
+
27
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
28
+ """Get all IAM users with console access."""
29
+ if resource_type != "AWS::IAM::User":
30
+ return []
31
+
32
+ try:
33
+ iam_client = aws_factory.get_client('iam', region)
34
+
35
+ users_with_console = []
36
+ paginator = iam_client.get_paginator('list_users')
37
+
38
+ for page in paginator.paginate():
39
+ for user in page.get('Users', []):
40
+ user_name = user.get('UserName')
41
+
42
+ # Check if user has console access (login profile)
43
+ try:
44
+ aws_factory.aws_api_call_with_retry(
45
+ lambda: iam_client.get_login_profile(UserName=user_name)
46
+ )
47
+ # User has console access
48
+ users_with_console.append({
49
+ 'UserName': user_name,
50
+ 'UserId': user.get('UserId'),
51
+ 'Arn': user.get('Arn'),
52
+ 'CreateDate': user.get('CreateDate'),
53
+ 'PasswordLastUsed': user.get('PasswordLastUsed'),
54
+ 'Tags': user.get('Tags', [])
55
+ })
56
+ except ClientError as e:
57
+ if e.response.get('Error', {}).get('Code') != 'NoSuchEntity':
58
+ logger.warning(f"Error checking login profile for user {user_name}: {e}")
59
+
60
+ logger.debug(f"Found {len(users_with_console)} IAM users with console access")
61
+ return users_with_console
62
+
63
+ except ClientError as e:
64
+ logger.error(f"Error retrieving IAM users: {e}")
65
+ raise
66
+ except Exception as e:
67
+ logger.error(f"Unexpected error retrieving IAM users: {e}")
68
+ raise
69
+
70
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
71
+ """Evaluate if IAM user with console access has MFA enabled."""
72
+ user_name = resource.get('UserName', 'unknown')
73
+
74
+ try:
75
+ iam_client = aws_factory.get_client('iam', region)
76
+
77
+ # Check MFA devices
78
+ response = aws_factory.aws_api_call_with_retry(
79
+ lambda: iam_client.list_mfa_devices(UserName=user_name)
80
+ )
81
+
82
+ mfa_devices = response.get('MFADevices', [])
83
+
84
+ if mfa_devices:
85
+ compliance_status = ComplianceStatus.COMPLIANT
86
+ device_count = len(mfa_devices)
87
+ device_types = [device.get('SerialNumber', '').split('/')[-1] for device in mfa_devices]
88
+ evaluation_reason = f"User {user_name} has {device_count} MFA device(s) configured: {', '.join(device_types)}"
89
+ else:
90
+ compliance_status = ComplianceStatus.NON_COMPLIANT
91
+ evaluation_reason = f"User {user_name} has console access but no MFA devices configured"
92
+
93
+ except ClientError as e:
94
+ error_code = e.response.get('Error', {}).get('Code', '')
95
+ if error_code in ['AccessDenied', 'UnauthorizedOperation']:
96
+ compliance_status = ComplianceStatus.ERROR
97
+ evaluation_reason = f"Insufficient permissions to check MFA for user {user_name}"
98
+ else:
99
+ compliance_status = ComplianceStatus.ERROR
100
+ evaluation_reason = f"Error checking MFA for user {user_name}: {str(e)}"
101
+ except Exception as e:
102
+ compliance_status = ComplianceStatus.ERROR
103
+ evaluation_reason = f"Unexpected error checking MFA for user {user_name}: {str(e)}"
104
+
105
+ return ComplianceResult(
106
+ resource_id=user_name,
107
+ resource_type="AWS::IAM::User",
108
+ compliance_status=compliance_status,
109
+ evaluation_reason=evaluation_reason,
110
+ config_rule_name=self.rule_name,
111
+ region=region
112
+ )
113
+
114
+ def _get_rule_remediation_steps(self) -> List[str]:
115
+ """Get specific remediation steps for IAM console MFA."""
116
+ return [
117
+ "Enable MFA for all IAM users with console access:",
118
+ "1. Identify users with console access but no MFA:",
119
+ " aws iam list-users --query 'Users[?PasswordLastUsed!=null].UserName'",
120
+ "2. For each user, enable MFA using one of these methods:",
121
+ " - Virtual MFA device (recommended): Google Authenticator, Authy, Microsoft Authenticator",
122
+ " - Hardware MFA device: YubiKey, Gemalto token",
123
+ " - SMS MFA (not recommended for high security environments)",
124
+ "3. Enable virtual MFA device:",
125
+ " aws iam create-virtual-mfa-device --virtual-mfa-device-name <device-name> --path /",
126
+ " aws iam enable-mfa-device --user-name <username> --serial-number <device-arn> --authentication-code1 <code1> --authentication-code2 <code2>",
127
+ "4. Use AWS Console: IAM > Users > [User] > Security credentials > Multi-factor authentication",
128
+ "5. Consider enforcing MFA through IAM policies that deny access without MFA",
129
+ "6. Provide MFA setup instructions and training to users",
130
+ "7. Regularly audit MFA compliance using this assessment"
131
+ ]
132
+
133
+
134
+ class RootAccountMFAEnabledAssessment(BaseConfigRuleAssessment):
135
+ """Assessment for root-account-mfa-enabled Config rule - ensures root account has MFA."""
136
+
137
+ def __init__(self):
138
+ """Initialize root account MFA assessment."""
139
+ super().__init__(
140
+ rule_name="root-account-mfa-enabled",
141
+ control_id="5.2",
142
+ resource_types=["AWS::::Account"]
143
+ )
144
+
145
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
146
+ """Get account-level resource for root MFA check."""
147
+ if resource_type != "AWS::::Account":
148
+ return []
149
+
150
+ try:
151
+ account_info = aws_factory.get_account_info()
152
+ return [{
153
+ 'AccountId': account_info.get('account_id', 'unknown'),
154
+ 'ResourceType': 'AWS::::Account'
155
+ }]
156
+ except Exception as e:
157
+ logger.error(f"Error getting account info: {e}")
158
+ return [{
159
+ 'AccountId': 'unknown',
160
+ 'ResourceType': 'AWS::::Account'
161
+ }]
162
+
163
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
164
+ """Evaluate if root account has MFA enabled."""
165
+ account_id = resource.get('AccountId', 'unknown')
166
+
167
+ try:
168
+ iam_client = aws_factory.get_client('iam', region)
169
+
170
+ # Get account summary which includes MFA info
171
+ response = aws_factory.aws_api_call_with_retry(
172
+ lambda: iam_client.get_account_summary()
173
+ )
174
+
175
+ summary = response.get('SummaryMap', {})
176
+
177
+ # Check for root MFA devices
178
+ root_mfa_enabled = summary.get('AccountMFAEnabled', 0)
179
+
180
+ if root_mfa_enabled > 0:
181
+ compliance_status = ComplianceStatus.COMPLIANT
182
+ evaluation_reason = f"Root account has MFA enabled ({root_mfa_enabled} device(s))"
183
+ else:
184
+ compliance_status = ComplianceStatus.NON_COMPLIANT
185
+ evaluation_reason = "Root account does not have MFA enabled - this is a critical security risk"
186
+
187
+ except ClientError as e:
188
+ error_code = e.response.get('Error', {}).get('Code', '')
189
+ if error_code in ['AccessDenied', 'UnauthorizedOperation']:
190
+ compliance_status = ComplianceStatus.ERROR
191
+ evaluation_reason = "Insufficient permissions to check root account MFA status"
192
+ else:
193
+ compliance_status = ComplianceStatus.ERROR
194
+ evaluation_reason = f"Error checking root account MFA: {str(e)}"
195
+ except Exception as e:
196
+ compliance_status = ComplianceStatus.ERROR
197
+ evaluation_reason = f"Unexpected error checking root account MFA: {str(e)}"
198
+
199
+ return ComplianceResult(
200
+ resource_id=account_id,
201
+ resource_type="AWS::::Account",
202
+ compliance_status=compliance_status,
203
+ evaluation_reason=evaluation_reason,
204
+ config_rule_name=self.rule_name,
205
+ region=region
206
+ )
207
+
208
+ def _get_rule_remediation_steps(self) -> List[str]:
209
+ """Get specific remediation steps for root account MFA."""
210
+ return [
211
+ "Enable MFA for the root account immediately:",
212
+ "1. Log in to AWS Console as root user",
213
+ "2. Go to 'My Security Credentials' in the account menu",
214
+ "3. In the 'Multi-factor authentication (MFA)' section, click 'Activate MFA'",
215
+ "4. Choose MFA device type:",
216
+ " - Virtual MFA device (recommended): Use authenticator app",
217
+ " - Hardware MFA device: Use physical token",
218
+ "5. Follow the setup wizard to configure the MFA device",
219
+ "6. Test the MFA device by logging out and back in",
220
+ "7. Store backup codes in a secure location",
221
+ "Additional security measures:",
222
+ " - Use a strong, unique password for the root account",
223
+ " - Store root credentials in a secure password manager",
224
+ " - Limit root account usage to emergency situations only",
225
+ " - Consider using multiple MFA devices for redundancy",
226
+ " - Regularly test root account access procedures"
227
+ ]
228
+
229
+
230
+ class IAMUserUnusedCredentialsAssessment(BaseConfigRuleAssessment):
231
+ """Assessment for iam-user-unused-credentials-check Config rule - identifies unused credentials."""
232
+
233
+ def __init__(self, parameters: Optional[Dict[str, Any]] = None):
234
+ """Initialize IAM user unused credentials assessment."""
235
+ default_params = {
236
+ 'maxCredentialUsageAge': 90 # Days
237
+ }
238
+
239
+ if parameters:
240
+ default_params.update(parameters)
241
+
242
+ super().__init__(
243
+ rule_name="iam-user-unused-credentials-check",
244
+ control_id="5.2",
245
+ resource_types=["AWS::IAM::User"],
246
+ parameters=default_params
247
+ )
248
+
249
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
250
+ """Get all IAM users."""
251
+ if resource_type != "AWS::IAM::User":
252
+ return []
253
+
254
+ try:
255
+ iam_client = aws_factory.get_client('iam', region)
256
+
257
+ users = []
258
+ paginator = iam_client.get_paginator('list_users')
259
+
260
+ for page in paginator.paginate():
261
+ for user in page.get('Users', []):
262
+ users.append({
263
+ 'UserName': user.get('UserName'),
264
+ 'UserId': user.get('UserId'),
265
+ 'Arn': user.get('Arn'),
266
+ 'CreateDate': user.get('CreateDate'),
267
+ 'PasswordLastUsed': user.get('PasswordLastUsed'),
268
+ 'Tags': user.get('Tags', [])
269
+ })
270
+
271
+ logger.debug(f"Found {len(users)} IAM users")
272
+ return users
273
+
274
+ except ClientError as e:
275
+ logger.error(f"Error retrieving IAM users: {e}")
276
+ raise
277
+ except Exception as e:
278
+ logger.error(f"Unexpected error retrieving IAM users: {e}")
279
+ raise
280
+
281
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
282
+ """Evaluate if IAM user has unused credentials."""
283
+ user_name = resource.get('UserName', 'unknown')
284
+ max_age_days = self.parameters.get('maxCredentialUsageAge', 90)
285
+ cutoff_date = datetime.now() - timedelta(days=max_age_days)
286
+
287
+ try:
288
+ iam_client = aws_factory.get_client('iam', region)
289
+
290
+ unused_credentials = []
291
+
292
+ # Check console password usage
293
+ has_console_access = False
294
+ password_last_used = None
295
+ try:
296
+ login_profile = aws_factory.aws_api_call_with_retry(
297
+ lambda: iam_client.get_login_profile(UserName=user_name)
298
+ )
299
+ has_console_access = True
300
+ password_last_used = resource.get('PasswordLastUsed')
301
+
302
+ if password_last_used is None:
303
+ unused_credentials.append("Console password never used")
304
+ elif password_last_used < cutoff_date:
305
+ days_unused = (datetime.now() - password_last_used.replace(tzinfo=None)).days
306
+ unused_credentials.append(f"Console password unused for {days_unused} days")
307
+
308
+ except ClientError as e:
309
+ if e.response.get('Error', {}).get('Code') != 'NoSuchEntity':
310
+ raise
311
+
312
+ # Check access keys
313
+ access_keys_response = aws_factory.aws_api_call_with_retry(
314
+ lambda: iam_client.list_access_keys(UserName=user_name)
315
+ )
316
+
317
+ for access_key in access_keys_response.get('AccessKeyMetadata', []):
318
+ access_key_id = access_key.get('AccessKeyId')
319
+
320
+ # Get access key last used info
321
+ try:
322
+ last_used_response = aws_factory.aws_api_call_with_retry(
323
+ lambda: iam_client.get_access_key_last_used(AccessKeyId=access_key_id)
324
+ )
325
+
326
+ last_used_info = last_used_response.get('AccessKeyLastUsed', {})
327
+ last_used_date = last_used_info.get('LastUsedDate')
328
+
329
+ if last_used_date is None:
330
+ unused_credentials.append(f"Access key {access_key_id} never used")
331
+ elif last_used_date < cutoff_date:
332
+ days_unused = (datetime.now() - last_used_date.replace(tzinfo=None)).days
333
+ unused_credentials.append(f"Access key {access_key_id} unused for {days_unused} days")
334
+
335
+ except ClientError as e:
336
+ logger.warning(f"Error checking access key usage for {access_key_id}: {e}")
337
+
338
+ if unused_credentials:
339
+ compliance_status = ComplianceStatus.NON_COMPLIANT
340
+ evaluation_reason = f"User {user_name} has unused credentials: {'; '.join(unused_credentials)}"
341
+ else:
342
+ if has_console_access or access_keys_response.get('AccessKeyMetadata'):
343
+ compliance_status = ComplianceStatus.COMPLIANT
344
+ evaluation_reason = f"User {user_name} has no unused credentials (all credentials used within {max_age_days} days)"
345
+ else:
346
+ compliance_status = ComplianceStatus.NOT_APPLICABLE
347
+ evaluation_reason = f"User {user_name} has no credentials to evaluate"
348
+
349
+ except ClientError as e:
350
+ error_code = e.response.get('Error', {}).get('Code', '')
351
+ if error_code in ['AccessDenied', 'UnauthorizedOperation']:
352
+ compliance_status = ComplianceStatus.ERROR
353
+ evaluation_reason = f"Insufficient permissions to check credentials for user {user_name}"
354
+ else:
355
+ compliance_status = ComplianceStatus.ERROR
356
+ evaluation_reason = f"Error checking credentials for user {user_name}: {str(e)}"
357
+ except Exception as e:
358
+ compliance_status = ComplianceStatus.ERROR
359
+ evaluation_reason = f"Unexpected error checking credentials for user {user_name}: {str(e)}"
360
+
361
+ return ComplianceResult(
362
+ resource_id=user_name,
363
+ resource_type="AWS::IAM::User",
364
+ compliance_status=compliance_status,
365
+ evaluation_reason=evaluation_reason,
366
+ config_rule_name=self.rule_name,
367
+ region=region
368
+ )
369
+
370
+ def _get_rule_remediation_steps(self) -> List[str]:
371
+ """Get specific remediation steps for unused credentials."""
372
+ max_age_days = self.parameters.get('maxCredentialUsageAge', 90)
373
+ return [
374
+ f"Remove or rotate unused IAM credentials (unused for >{max_age_days} days):",
375
+ "1. Identify users with unused credentials:",
376
+ " aws iam generate-credential-report",
377
+ " aws iam get-credential-report",
378
+ "2. For unused console passwords:",
379
+ " - Contact the user to verify if console access is still needed",
380
+ " - If not needed: aws iam delete-login-profile --user-name <username>",
381
+ " - If needed: Force password reset on next login",
382
+ "3. For unused access keys:",
383
+ " - Verify with the user/application owner if keys are still needed",
384
+ " - If not needed: aws iam delete-access-key --user-name <username> --access-key-id <key-id>",
385
+ " - If needed: Rotate the access keys",
386
+ "4. For users with no activity:",
387
+ " - Consider deactivating or removing the user account",
388
+ " - Document the business justification for keeping inactive accounts",
389
+ "5. Implement automated credential rotation policies",
390
+ "6. Set up CloudWatch alarms for credential usage monitoring",
391
+ f"7. Regularly review credential usage (recommend monthly for >{max_age_days} day threshold)",
392
+ "8. Use AWS IAM Access Analyzer to identify unused access"
393
+ ]