complio 0.1.1__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 (79) hide show
  1. CHANGELOG.md +208 -0
  2. README.md +343 -0
  3. complio/__init__.py +48 -0
  4. complio/cli/__init__.py +0 -0
  5. complio/cli/banner.py +87 -0
  6. complio/cli/commands/__init__.py +0 -0
  7. complio/cli/commands/history.py +439 -0
  8. complio/cli/commands/scan.py +700 -0
  9. complio/cli/main.py +115 -0
  10. complio/cli/output.py +338 -0
  11. complio/config/__init__.py +17 -0
  12. complio/config/settings.py +333 -0
  13. complio/connectors/__init__.py +9 -0
  14. complio/connectors/aws/__init__.py +0 -0
  15. complio/connectors/aws/client.py +342 -0
  16. complio/connectors/base.py +135 -0
  17. complio/core/__init__.py +10 -0
  18. complio/core/registry.py +228 -0
  19. complio/core/runner.py +351 -0
  20. complio/py.typed +0 -0
  21. complio/reporters/__init__.py +7 -0
  22. complio/reporters/generator.py +417 -0
  23. complio/tests_library/__init__.py +0 -0
  24. complio/tests_library/base.py +492 -0
  25. complio/tests_library/identity/__init__.py +0 -0
  26. complio/tests_library/identity/access_key_rotation.py +302 -0
  27. complio/tests_library/identity/mfa_enforcement.py +327 -0
  28. complio/tests_library/identity/root_account_protection.py +470 -0
  29. complio/tests_library/infrastructure/__init__.py +0 -0
  30. complio/tests_library/infrastructure/cloudtrail_encryption.py +286 -0
  31. complio/tests_library/infrastructure/cloudtrail_log_validation.py +274 -0
  32. complio/tests_library/infrastructure/cloudtrail_logging.py +400 -0
  33. complio/tests_library/infrastructure/ebs_encryption.py +244 -0
  34. complio/tests_library/infrastructure/ec2_security_groups.py +321 -0
  35. complio/tests_library/infrastructure/iam_password_policy.py +460 -0
  36. complio/tests_library/infrastructure/nacl_security.py +356 -0
  37. complio/tests_library/infrastructure/rds_encryption.py +252 -0
  38. complio/tests_library/infrastructure/s3_encryption.py +301 -0
  39. complio/tests_library/infrastructure/s3_public_access.py +369 -0
  40. complio/tests_library/infrastructure/secrets_manager_encryption.py +248 -0
  41. complio/tests_library/infrastructure/vpc_flow_logs.py +287 -0
  42. complio/tests_library/logging/__init__.py +0 -0
  43. complio/tests_library/logging/cloudwatch_alarms.py +354 -0
  44. complio/tests_library/logging/cloudwatch_logs_encryption.py +281 -0
  45. complio/tests_library/logging/cloudwatch_retention.py +252 -0
  46. complio/tests_library/logging/config_enabled.py +393 -0
  47. complio/tests_library/logging/eventbridge_rules.py +460 -0
  48. complio/tests_library/logging/guardduty_enabled.py +436 -0
  49. complio/tests_library/logging/security_hub_enabled.py +416 -0
  50. complio/tests_library/logging/sns_encryption.py +273 -0
  51. complio/tests_library/network/__init__.py +0 -0
  52. complio/tests_library/network/alb_nlb_security.py +421 -0
  53. complio/tests_library/network/api_gateway_security.py +452 -0
  54. complio/tests_library/network/cloudfront_https.py +332 -0
  55. complio/tests_library/network/direct_connect_security.py +343 -0
  56. complio/tests_library/network/nacl_configuration.py +367 -0
  57. complio/tests_library/network/network_firewall.py +355 -0
  58. complio/tests_library/network/transit_gateway_security.py +318 -0
  59. complio/tests_library/network/vpc_endpoints_security.py +339 -0
  60. complio/tests_library/network/vpn_security.py +333 -0
  61. complio/tests_library/network/waf_configuration.py +428 -0
  62. complio/tests_library/security/__init__.py +0 -0
  63. complio/tests_library/security/kms_key_rotation.py +314 -0
  64. complio/tests_library/storage/__init__.py +0 -0
  65. complio/tests_library/storage/backup_encryption.py +288 -0
  66. complio/tests_library/storage/dynamodb_encryption.py +280 -0
  67. complio/tests_library/storage/efs_encryption.py +257 -0
  68. complio/tests_library/storage/elasticache_encryption.py +370 -0
  69. complio/tests_library/storage/redshift_encryption.py +252 -0
  70. complio/tests_library/storage/s3_versioning.py +264 -0
  71. complio/utils/__init__.py +26 -0
  72. complio/utils/errors.py +179 -0
  73. complio/utils/exceptions.py +151 -0
  74. complio/utils/history.py +243 -0
  75. complio/utils/logger.py +391 -0
  76. complio-0.1.1.dist-info/METADATA +385 -0
  77. complio-0.1.1.dist-info/RECORD +79 -0
  78. complio-0.1.1.dist-info/WHEEL +4 -0
  79. complio-0.1.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,302 @@
1
+ """
2
+ IAM access key rotation compliance test.
3
+
4
+ Checks that all IAM access keys are rotated regularly (< 90 days).
5
+
6
+ ISO 27001 Control: A.8.5 - Access control
7
+ Requirement: Access credentials must be rotated regularly
8
+
9
+ Example:
10
+ >>> from complio.connectors.aws.client import AWSConnector
11
+ >>> from complio.tests_library.identity.access_key_rotation import AccessKeyRotationTest
12
+ >>>
13
+ >>> connector = AWSConnector("production", "us-east-1")
14
+ >>> connector.connect()
15
+ >>>
16
+ >>> test = AccessKeyRotationTest(connector)
17
+ >>> result = test.run()
18
+ >>> print(f"Passed: {result.passed}, Score: {result.score}")
19
+ """
20
+
21
+ from datetime import datetime, timezone
22
+ from typing import Any, Dict
23
+
24
+ from botocore.exceptions import ClientError
25
+
26
+ from complio.connectors.aws.client import AWSConnector
27
+ from complio.tests_library.base import (
28
+ ComplianceTest,
29
+ Severity,
30
+ TestResult,
31
+ TestStatus,
32
+ )
33
+
34
+
35
+ class AccessKeyRotationTest(ComplianceTest):
36
+ """Test for IAM access key rotation compliance.
37
+
38
+ Verifies that all IAM access keys are rotated regularly.
39
+ Keys older than 90 days are flagged.
40
+
41
+ Compliance Requirements:
42
+ - Access keys should be < 90 days old (COMPLIANT)
43
+ - Access keys 90-180 days old (MEDIUM severity)
44
+ - Access keys > 180 days old (HIGH severity)
45
+
46
+ Scoring:
47
+ - 100% if all keys are < 90 days old
48
+ - Proportional score based on compliant/total ratio
49
+ - 0% if no keys are compliant
50
+
51
+ Example:
52
+ >>> test = AccessKeyRotationTest(connector)
53
+ >>> result = test.execute()
54
+ >>> for finding in result.findings:
55
+ ... print(f"{finding.resource_id}: {finding.title}")
56
+ """
57
+
58
+ def __init__(self, connector: AWSConnector) -> None:
59
+ """Initialize access key rotation test.
60
+
61
+ Args:
62
+ connector: AWS connector instance
63
+ """
64
+ super().__init__(
65
+ test_id="access_key_rotation",
66
+ test_name="IAM Access Key Rotation Check",
67
+ description="Verify all IAM access keys are rotated regularly (< 90 days)",
68
+ control_id="A.8.5",
69
+ connector=connector,
70
+ scope="global",
71
+ )
72
+
73
+ def execute(self) -> TestResult:
74
+ """Execute access key rotation compliance test.
75
+
76
+ Returns:
77
+ TestResult with findings for old access keys
78
+
79
+ Example:
80
+ >>> test = AccessKeyRotationTest(connector)
81
+ >>> result = test.execute()
82
+ >>> print(result.score)
83
+ 85.0
84
+ """
85
+ result = TestResult(
86
+ test_id=self.test_id,
87
+ test_name=self.test_name,
88
+ status=TestStatus.PASSED,
89
+ passed=True,
90
+ score=100.0,
91
+ )
92
+
93
+ try:
94
+ # Get IAM client
95
+ iam_client = self.connector.get_client("iam")
96
+
97
+ # List all users
98
+ self.logger.info("listing_iam_users")
99
+ users = []
100
+
101
+ paginator = iam_client.get_paginator("list_users")
102
+ for page in paginator.paginate():
103
+ users.extend(page.get("Users", []))
104
+
105
+ if not users:
106
+ self.logger.info("no_iam_users_found")
107
+ result.metadata["message"] = "No IAM users found in account"
108
+ return result
109
+
110
+ self.logger.info("iam_users_found", count=len(users))
111
+
112
+ # Check access keys for each user
113
+ total_keys = 0
114
+ compliant_keys = 0
115
+ now = datetime.now(timezone.utc)
116
+
117
+ for user in users:
118
+ user_name = user["UserName"]
119
+
120
+ # List access keys for user
121
+ try:
122
+ keys_response = iam_client.list_access_keys(UserName=user_name)
123
+ access_keys = keys_response.get("AccessKeyMetadata", [])
124
+
125
+ for access_key in access_keys:
126
+ access_key_id = access_key["AccessKeyId"]
127
+ create_date = access_key["CreateDate"]
128
+ status = access_key["Status"]
129
+
130
+ # Skip inactive keys
131
+ if status != "Active":
132
+ self.logger.debug("skipping_inactive_key", access_key_id=access_key_id, user=user_name)
133
+ continue
134
+
135
+ total_keys += 1
136
+ result.resources_scanned += 1
137
+
138
+ # Calculate age
139
+ age_delta = now - create_date
140
+ age_days = age_delta.days
141
+
142
+ # Determine severity based on age
143
+ if age_days <= 90:
144
+ compliant = True
145
+ severity = None
146
+ elif age_days <= 180:
147
+ compliant = False
148
+ severity = Severity.MEDIUM
149
+ else:
150
+ compliant = False
151
+ severity = Severity.HIGH
152
+
153
+ # Create evidence
154
+ evidence = self.create_evidence(
155
+ resource_id=access_key_id,
156
+ resource_type="iam_access_key",
157
+ data={
158
+ "access_key_id": access_key_id,
159
+ "user_name": user_name,
160
+ "create_date": create_date.isoformat(),
161
+ "age_days": age_days,
162
+ "status": status,
163
+ "compliant": compliant,
164
+ }
165
+ )
166
+ result.add_evidence(evidence)
167
+
168
+ if compliant:
169
+ compliant_keys += 1
170
+ self.logger.debug(
171
+ "access_key_compliant",
172
+ access_key_id=access_key_id,
173
+ user=user_name,
174
+ age_days=age_days
175
+ )
176
+ else:
177
+ # Create finding for old access key
178
+ finding = self.create_finding(
179
+ resource_id=access_key_id,
180
+ resource_type="iam_access_key",
181
+ severity=severity,
182
+ title=f"IAM access key not rotated ({age_days} days old)",
183
+ description=f"Access key '{access_key_id}' for user '{user_name}' is {age_days} days old. "
184
+ f"Keys should be rotated every 90 days. This key has not been rotated in "
185
+ f"{age_days // 30} months. Old credentials increase the risk of unauthorized access. "
186
+ "ISO 27001 A.8.5 requires regular rotation of access credentials.",
187
+ remediation=(
188
+ f"Rotate access key for user '{user_name}':\n\n"
189
+ "1. Create a new access key:\n"
190
+ f" aws iam create-access-key --user-name {user_name}\n\n"
191
+ "2. Update all applications/services using the old key\n"
192
+ " with the new access key ID and secret access key\n\n"
193
+ "3. Test that applications work with the new key\n\n"
194
+ "4. Deactivate the old key (don't delete yet):\n"
195
+ f" aws iam update-access-key --user-name {user_name} \\\n"
196
+ f" --access-key-id {access_key_id} --status Inactive\n\n"
197
+ "5. Monitor for a few days to ensure no errors\n\n"
198
+ "6. Delete the old key:\n"
199
+ f" aws iam delete-access-key --user-name {user_name} \\\n"
200
+ f" --access-key-id {access_key_id}\n\n"
201
+ "Or use AWS Console:\n"
202
+ "1. Go to IAM → Users\n"
203
+ f"2. Select user '{user_name}'\n"
204
+ "3. Go to 'Security credentials' tab\n"
205
+ "4. Click 'Create access key'\n"
206
+ "5. Update applications with new key\n"
207
+ f"6. Deactivate and delete old key '{access_key_id}'\n\n"
208
+ "Best practice: Rotate keys every 90 days."
209
+ ),
210
+ evidence=evidence
211
+ )
212
+ result.add_finding(finding)
213
+
214
+ self.logger.warning(
215
+ "access_key_rotation_overdue",
216
+ access_key_id=access_key_id,
217
+ user=user_name,
218
+ age_days=age_days,
219
+ severity=severity.value
220
+ )
221
+
222
+ except ClientError as e:
223
+ error_code = e.response.get("Error", {}).get("Code")
224
+ self.logger.warning(
225
+ "user_access_keys_list_error",
226
+ user=user_name,
227
+ error_code=error_code
228
+ )
229
+ continue
230
+
231
+ if total_keys == 0:
232
+ self.logger.info("no_active_access_keys_found")
233
+ result.metadata["message"] = "No active IAM access keys found in account"
234
+ return result
235
+
236
+ # Calculate compliance score
237
+ result.score = (compliant_keys / total_keys) * 100
238
+
239
+ # Determine pass/fail
240
+ result.passed = compliant_keys == total_keys
241
+ result.status = TestStatus.PASSED if result.passed else TestStatus.FAILED
242
+
243
+ # Add metadata
244
+ result.metadata = {
245
+ "total_users": len(users),
246
+ "total_active_keys": total_keys,
247
+ "compliant_keys": compliant_keys,
248
+ "non_compliant_keys": total_keys - compliant_keys,
249
+ "compliance_percentage": result.score,
250
+ }
251
+
252
+ self.logger.info(
253
+ "access_key_rotation_test_completed",
254
+ total_keys=total_keys,
255
+ compliant=compliant_keys,
256
+ score=result.score,
257
+ passed=result.passed
258
+ )
259
+
260
+ except ClientError as e:
261
+ error_code = e.response.get("Error", {}).get("Code")
262
+ self.logger.error("access_key_rotation_test_error", error_code=error_code, error=str(e))
263
+ result.status = TestStatus.ERROR
264
+ result.passed = False
265
+ result.score = 0.0
266
+ result.error_message = f"AWS API Error: {error_code} - {str(e)}"
267
+
268
+ except Exception as e:
269
+ self.logger.error("access_key_rotation_test_error", error=str(e))
270
+ result.status = TestStatus.ERROR
271
+ result.passed = False
272
+ result.score = 0.0
273
+ result.error_message = str(e)
274
+
275
+ return result
276
+
277
+
278
+ # ============================================================================
279
+ # CONVENIENCE FUNCTION
280
+ # ============================================================================
281
+
282
+
283
+ def run_access_key_rotation_test(connector: AWSConnector) -> TestResult:
284
+ """Run IAM access key rotation compliance test.
285
+
286
+ Convenience function for running the test.
287
+
288
+ Args:
289
+ connector: AWS connector
290
+
291
+ Returns:
292
+ TestResult
293
+
294
+ Example:
295
+ >>> from complio.connectors.aws.client import AWSConnector
296
+ >>> connector = AWSConnector("production", "us-east-1")
297
+ >>> connector.connect()
298
+ >>> result = run_access_key_rotation_test(connector)
299
+ >>> print(f"Score: {result.score}%")
300
+ """
301
+ test = AccessKeyRotationTest(connector)
302
+ return test.execute()
@@ -0,0 +1,327 @@
1
+ """
2
+ MFA enforcement compliance test.
3
+
4
+ Checks that all IAM users have MFA (Multi-Factor Authentication) enabled.
5
+
6
+ ISO 27001 Control: A.8.5 - Access control
7
+ Requirement: Multi-factor authentication for user access
8
+
9
+ Example:
10
+ >>> from complio.connectors.aws.client import AWSConnector
11
+ >>> from complio.tests_library.identity.mfa_enforcement import MFAEnforcementTest
12
+ >>>
13
+ >>> connector = AWSConnector("production", "us-east-1")
14
+ >>> connector.connect()
15
+ >>>
16
+ >>> test = MFAEnforcementTest(connector)
17
+ >>> result = test.run()
18
+ >>> print(f"Passed: {result.passed}, Score: {result.score}")
19
+ """
20
+
21
+ import csv
22
+ import io
23
+ import time
24
+ from typing import Any, Dict
25
+
26
+ from botocore.exceptions import ClientError
27
+
28
+ from complio.connectors.aws.client import AWSConnector
29
+ from complio.tests_library.base import (
30
+ ComplianceTest,
31
+ Severity,
32
+ TestResult,
33
+ TestStatus,
34
+ )
35
+
36
+
37
+ class MFAEnforcementTest(ComplianceTest):
38
+ """Test for MFA enforcement compliance.
39
+
40
+ Verifies that all IAM users have MFA enabled by parsing the
41
+ IAM credential report.
42
+
43
+ Compliance Requirements:
44
+ - All IAM users must have MFA enabled
45
+ - Users without MFA are vulnerable to credential compromise
46
+ - Password-based access requires MFA protection
47
+
48
+ Scoring:
49
+ - 100% if all users have MFA enabled
50
+ - Proportional score based on MFA-enabled/total ratio
51
+ - 0% if no users have MFA enabled
52
+
53
+ Example:
54
+ >>> test = MFAEnforcementTest(connector)
55
+ >>> result = test.execute()
56
+ >>> for finding in result.findings:
57
+ ... print(f"{finding.resource_id}: {finding.title}")
58
+ """
59
+
60
+ def __init__(self, connector: AWSConnector) -> None:
61
+ """Initialize MFA enforcement test.
62
+
63
+ Args:
64
+ connector: AWS connector instance
65
+ """
66
+ super().__init__(
67
+ test_id="mfa_enforcement",
68
+ test_name="IAM MFA Enforcement Check",
69
+ description="Verify all IAM users have MFA (Multi-Factor Authentication) enabled",
70
+ control_id="A.8.5",
71
+ connector=connector,
72
+ scope="global",
73
+ )
74
+
75
+ def execute(self) -> TestResult:
76
+ """Execute MFA enforcement compliance test.
77
+
78
+ Returns:
79
+ TestResult with findings for users without MFA
80
+
81
+ Example:
82
+ >>> test = MFAEnforcementTest(connector)
83
+ >>> result = test.execute()
84
+ >>> print(result.score)
85
+ 92.5
86
+ """
87
+ result = TestResult(
88
+ test_id=self.test_id,
89
+ test_name=self.test_name,
90
+ status=TestStatus.PASSED,
91
+ passed=True,
92
+ score=100.0,
93
+ )
94
+
95
+ try:
96
+ # Get IAM client
97
+ iam_client = self.connector.get_client("iam")
98
+
99
+ # Generate credential report (may need to wait)
100
+ self.logger.info("generating_credential_report")
101
+ report_ready = self._generate_credential_report(iam_client)
102
+
103
+ if not report_ready:
104
+ self.logger.error("credential_report_generation_failed")
105
+ result.status = TestStatus.ERROR
106
+ result.passed = False
107
+ result.score = 0.0
108
+ result.error_message = "Failed to generate IAM credential report after retries"
109
+ return result
110
+
111
+ # Get credential report
112
+ self.logger.info("retrieving_credential_report")
113
+ report_response = iam_client.get_credential_report()
114
+ report_content = report_response["Content"]
115
+
116
+ # Parse CSV report
117
+ report_csv = report_content.decode("utf-8")
118
+ csv_reader = csv.DictReader(io.StringIO(report_csv))
119
+
120
+ users_with_mfa = 0
121
+ total_users = 0
122
+ root_user_checked = False
123
+
124
+ for row in csv_reader:
125
+ user_name = row.get("user", "")
126
+
127
+ # Skip root account (checked in separate test)
128
+ if user_name == "<root_account>":
129
+ root_user_checked = True
130
+ continue
131
+
132
+ # Check if user has console access (password enabled)
133
+ password_enabled = row.get("password_enabled", "false") == "true"
134
+
135
+ # If no console password, MFA is not required
136
+ if not password_enabled:
137
+ self.logger.debug("user_no_console_access", user=user_name)
138
+ continue
139
+
140
+ total_users += 1
141
+ result.resources_scanned += 1
142
+
143
+ # Check MFA status
144
+ mfa_active = row.get("mfa_active", "false") == "true"
145
+
146
+ # Get user creation date and last login
147
+ user_creation_time = row.get("user_creation_time", "")
148
+ password_last_used = row.get("password_last_used", "no_information")
149
+
150
+ # Create evidence
151
+ evidence = self.create_evidence(
152
+ resource_id=user_name,
153
+ resource_type="iam_user",
154
+ data={
155
+ "user_name": user_name,
156
+ "mfa_active": mfa_active,
157
+ "password_enabled": password_enabled,
158
+ "user_creation_time": user_creation_time,
159
+ "password_last_used": password_last_used,
160
+ "access_key_1_active": row.get("access_key_1_active", "false") == "true",
161
+ "access_key_2_active": row.get("access_key_2_active", "false") == "true",
162
+ }
163
+ )
164
+ result.add_evidence(evidence)
165
+
166
+ if mfa_active:
167
+ users_with_mfa += 1
168
+ self.logger.debug(
169
+ "user_has_mfa",
170
+ user=user_name
171
+ )
172
+ else:
173
+ # Create finding for user without MFA
174
+ finding = self.create_finding(
175
+ resource_id=user_name,
176
+ resource_type="iam_user",
177
+ severity=Severity.HIGH,
178
+ title="IAM user does not have MFA enabled",
179
+ description=f"IAM user '{user_name}' has console access (password enabled) but does not have "
180
+ "Multi-Factor Authentication (MFA) enabled. This leaves the account vulnerable to "
181
+ "credential compromise through phishing, password leaks, or brute force attacks. "
182
+ f"User was created on {user_creation_time} and last used password on {password_last_used}. "
183
+ "ISO 27001 A.8.5 requires multi-factor authentication for privileged access.",
184
+ remediation=(
185
+ f"Enable MFA for IAM user '{user_name}':\n\n"
186
+ "User must enable MFA themselves:\n"
187
+ "1. Sign in to AWS Console as the user\n"
188
+ "2. Go to IAM → Users → Your username → Security credentials\n"
189
+ "3. Under 'Multi-factor authentication (MFA)', click 'Assign MFA device'\n"
190
+ "4. Choose virtual MFA device (Google Authenticator, Authy, etc.)\n"
191
+ "5. Scan QR code with authenticator app\n"
192
+ "6. Enter two consecutive MFA codes to verify\n\n"
193
+ "Administrator can enforce MFA:\n"
194
+ "1. Create IAM policy requiring MFA for API calls\n"
195
+ "2. Attach policy to users or groups\n"
196
+ "3. Set up console login with MFA requirement\n\n"
197
+ f"Or use AWS CLI to check MFA devices:\n"
198
+ f"aws iam list-mfa-devices --user-name {user_name}\n\n"
199
+ "Best practice: Enforce MFA for all users with console access."
200
+ ),
201
+ evidence=evidence
202
+ )
203
+ result.add_finding(finding)
204
+
205
+ self.logger.warning(
206
+ "user_without_mfa",
207
+ user=user_name,
208
+ password_last_used=password_last_used
209
+ )
210
+
211
+ if total_users == 0:
212
+ self.logger.info("no_users_with_console_access")
213
+ result.metadata["message"] = "No IAM users with console access found"
214
+ return result
215
+
216
+ # Calculate compliance score
217
+ result.score = (users_with_mfa / total_users) * 100
218
+
219
+ # Determine pass/fail
220
+ result.passed = users_with_mfa == total_users
221
+ result.status = TestStatus.PASSED if result.passed else TestStatus.FAILED
222
+
223
+ # Add metadata
224
+ result.metadata = {
225
+ "total_users_with_console_access": total_users,
226
+ "users_with_mfa": users_with_mfa,
227
+ "users_without_mfa": total_users - users_with_mfa,
228
+ "compliance_percentage": result.score,
229
+ "root_user_checked": root_user_checked,
230
+ }
231
+
232
+ self.logger.info(
233
+ "mfa_enforcement_test_completed",
234
+ total_users=total_users,
235
+ with_mfa=users_with_mfa,
236
+ score=result.score,
237
+ passed=result.passed
238
+ )
239
+
240
+ except ClientError as e:
241
+ error_code = e.response.get("Error", {}).get("Code")
242
+ self.logger.error("mfa_enforcement_test_error", error_code=error_code, error=str(e))
243
+ result.status = TestStatus.ERROR
244
+ result.passed = False
245
+ result.score = 0.0
246
+ result.error_message = f"AWS API Error: {error_code} - {str(e)}"
247
+
248
+ except Exception as e:
249
+ self.logger.error("mfa_enforcement_test_error", error=str(e))
250
+ result.status = TestStatus.ERROR
251
+ result.passed = False
252
+ result.score = 0.0
253
+ result.error_message = str(e)
254
+
255
+ return result
256
+
257
+ def _generate_credential_report(self, iam_client: Any, max_retries: int = 3, retry_delay: int = 5) -> bool:
258
+ """Generate IAM credential report with retry logic.
259
+
260
+ Args:
261
+ iam_client: Boto3 IAM client
262
+ max_retries: Maximum number of retries
263
+ retry_delay: Delay between retries in seconds
264
+
265
+ Returns:
266
+ True if report is ready, False otherwise
267
+ """
268
+ for attempt in range(max_retries):
269
+ try:
270
+ response = iam_client.generate_credential_report()
271
+ state = response.get("State")
272
+
273
+ if state == "COMPLETE":
274
+ self.logger.info("credential_report_ready")
275
+ return True
276
+ elif state == "INPROGRESS":
277
+ self.logger.info(
278
+ "credential_report_generating",
279
+ attempt=attempt + 1,
280
+ max_retries=max_retries
281
+ )
282
+ time.sleep(retry_delay)
283
+ else:
284
+ self.logger.warning("credential_report_unexpected_state", state=state)
285
+ time.sleep(retry_delay)
286
+
287
+ except ClientError as e:
288
+ error_code = e.response.get("Error", {}).get("Code")
289
+ if error_code == "LimitExceededException":
290
+ self.logger.warning("credential_report_rate_limited", attempt=attempt + 1)
291
+ time.sleep(retry_delay)
292
+ else:
293
+ raise
294
+
295
+ # Final check
296
+ try:
297
+ response = iam_client.generate_credential_report()
298
+ return response.get("State") == "COMPLETE"
299
+ except Exception:
300
+ return False
301
+
302
+
303
+ # ============================================================================
304
+ # CONVENIENCE FUNCTION
305
+ # ============================================================================
306
+
307
+
308
+ def run_mfa_enforcement_test(connector: AWSConnector) -> TestResult:
309
+ """Run MFA enforcement compliance test.
310
+
311
+ Convenience function for running the test.
312
+
313
+ Args:
314
+ connector: AWS connector
315
+
316
+ Returns:
317
+ TestResult
318
+
319
+ Example:
320
+ >>> from complio.connectors.aws.client import AWSConnector
321
+ >>> connector = AWSConnector("production", "us-east-1")
322
+ >>> connector.connect()
323
+ >>> result = run_mfa_enforcement_test(connector)
324
+ >>> print(f"Score: {result.score}%")
325
+ """
326
+ test = MFAEnforcementTest(connector)
327
+ return test.execute()