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.
- CHANGELOG.md +208 -0
- README.md +343 -0
- complio/__init__.py +48 -0
- complio/cli/__init__.py +0 -0
- complio/cli/banner.py +87 -0
- complio/cli/commands/__init__.py +0 -0
- complio/cli/commands/history.py +439 -0
- complio/cli/commands/scan.py +700 -0
- complio/cli/main.py +115 -0
- complio/cli/output.py +338 -0
- complio/config/__init__.py +17 -0
- complio/config/settings.py +333 -0
- complio/connectors/__init__.py +9 -0
- complio/connectors/aws/__init__.py +0 -0
- complio/connectors/aws/client.py +342 -0
- complio/connectors/base.py +135 -0
- complio/core/__init__.py +10 -0
- complio/core/registry.py +228 -0
- complio/core/runner.py +351 -0
- complio/py.typed +0 -0
- complio/reporters/__init__.py +7 -0
- complio/reporters/generator.py +417 -0
- complio/tests_library/__init__.py +0 -0
- complio/tests_library/base.py +492 -0
- complio/tests_library/identity/__init__.py +0 -0
- complio/tests_library/identity/access_key_rotation.py +302 -0
- complio/tests_library/identity/mfa_enforcement.py +327 -0
- complio/tests_library/identity/root_account_protection.py +470 -0
- complio/tests_library/infrastructure/__init__.py +0 -0
- complio/tests_library/infrastructure/cloudtrail_encryption.py +286 -0
- complio/tests_library/infrastructure/cloudtrail_log_validation.py +274 -0
- complio/tests_library/infrastructure/cloudtrail_logging.py +400 -0
- complio/tests_library/infrastructure/ebs_encryption.py +244 -0
- complio/tests_library/infrastructure/ec2_security_groups.py +321 -0
- complio/tests_library/infrastructure/iam_password_policy.py +460 -0
- complio/tests_library/infrastructure/nacl_security.py +356 -0
- complio/tests_library/infrastructure/rds_encryption.py +252 -0
- complio/tests_library/infrastructure/s3_encryption.py +301 -0
- complio/tests_library/infrastructure/s3_public_access.py +369 -0
- complio/tests_library/infrastructure/secrets_manager_encryption.py +248 -0
- complio/tests_library/infrastructure/vpc_flow_logs.py +287 -0
- complio/tests_library/logging/__init__.py +0 -0
- complio/tests_library/logging/cloudwatch_alarms.py +354 -0
- complio/tests_library/logging/cloudwatch_logs_encryption.py +281 -0
- complio/tests_library/logging/cloudwatch_retention.py +252 -0
- complio/tests_library/logging/config_enabled.py +393 -0
- complio/tests_library/logging/eventbridge_rules.py +460 -0
- complio/tests_library/logging/guardduty_enabled.py +436 -0
- complio/tests_library/logging/security_hub_enabled.py +416 -0
- complio/tests_library/logging/sns_encryption.py +273 -0
- complio/tests_library/network/__init__.py +0 -0
- complio/tests_library/network/alb_nlb_security.py +421 -0
- complio/tests_library/network/api_gateway_security.py +452 -0
- complio/tests_library/network/cloudfront_https.py +332 -0
- complio/tests_library/network/direct_connect_security.py +343 -0
- complio/tests_library/network/nacl_configuration.py +367 -0
- complio/tests_library/network/network_firewall.py +355 -0
- complio/tests_library/network/transit_gateway_security.py +318 -0
- complio/tests_library/network/vpc_endpoints_security.py +339 -0
- complio/tests_library/network/vpn_security.py +333 -0
- complio/tests_library/network/waf_configuration.py +428 -0
- complio/tests_library/security/__init__.py +0 -0
- complio/tests_library/security/kms_key_rotation.py +314 -0
- complio/tests_library/storage/__init__.py +0 -0
- complio/tests_library/storage/backup_encryption.py +288 -0
- complio/tests_library/storage/dynamodb_encryption.py +280 -0
- complio/tests_library/storage/efs_encryption.py +257 -0
- complio/tests_library/storage/elasticache_encryption.py +370 -0
- complio/tests_library/storage/redshift_encryption.py +252 -0
- complio/tests_library/storage/s3_versioning.py +264 -0
- complio/utils/__init__.py +26 -0
- complio/utils/errors.py +179 -0
- complio/utils/exceptions.py +151 -0
- complio/utils/history.py +243 -0
- complio/utils/logger.py +391 -0
- complio-0.1.1.dist-info/METADATA +385 -0
- complio-0.1.1.dist-info/RECORD +79 -0
- complio-0.1.1.dist-info/WHEEL +4 -0
- 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()
|