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,470 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Root account protection compliance test.
|
|
3
|
+
|
|
4
|
+
Checks multiple security controls for the AWS root account.
|
|
5
|
+
|
|
6
|
+
ISO 27001 Control: A.8.2 - Privileged access rights
|
|
7
|
+
Requirement: Root account must be properly secured
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
11
|
+
>>> from complio.tests_library.identity.root_account_protection import RootAccountProtectionTest
|
|
12
|
+
>>>
|
|
13
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
14
|
+
>>> connector.connect()
|
|
15
|
+
>>>
|
|
16
|
+
>>> test = RootAccountProtectionTest(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 datetime import datetime, timezone
|
|
25
|
+
from typing import Any, Dict
|
|
26
|
+
|
|
27
|
+
from botocore.exceptions import ClientError
|
|
28
|
+
|
|
29
|
+
from complio.connectors.aws.client import AWSConnector
|
|
30
|
+
from complio.tests_library.base import (
|
|
31
|
+
ComplianceTest,
|
|
32
|
+
Severity,
|
|
33
|
+
TestResult,
|
|
34
|
+
TestStatus,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RootAccountProtectionTest(ComplianceTest):
|
|
39
|
+
"""Test for root account protection compliance.
|
|
40
|
+
|
|
41
|
+
Performs multiple checks on the AWS root account:
|
|
42
|
+
1. Root account has MFA enabled (CRITICAL)
|
|
43
|
+
2. Root account has no access keys (HIGH)
|
|
44
|
+
3. Root account not used recently (MEDIUM if used in last 90 days)
|
|
45
|
+
|
|
46
|
+
Compliance Requirements:
|
|
47
|
+
- Root MFA must be enabled
|
|
48
|
+
- Root access keys must not exist
|
|
49
|
+
- Root account should not be used for day-to-day operations
|
|
50
|
+
|
|
51
|
+
Scoring:
|
|
52
|
+
- MFA enabled: 50 points
|
|
53
|
+
- No access keys: 30 points
|
|
54
|
+
- Not used recently: 20 points
|
|
55
|
+
- Total: 100 points
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
>>> test = RootAccountProtectionTest(connector)
|
|
59
|
+
>>> result = test.execute()
|
|
60
|
+
>>> for finding in result.findings:
|
|
61
|
+
... print(f"{finding.resource_id}: {finding.title}")
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, connector: AWSConnector) -> None:
|
|
65
|
+
"""Initialize root account protection test.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
connector: AWS connector instance
|
|
69
|
+
"""
|
|
70
|
+
super().__init__(
|
|
71
|
+
test_id="root_account_protection",
|
|
72
|
+
test_name="Root Account Protection Check",
|
|
73
|
+
description="Verify root account has MFA, no access keys, and is not used regularly",
|
|
74
|
+
control_id="A.8.2",
|
|
75
|
+
connector=connector,
|
|
76
|
+
scope="global",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def execute(self) -> TestResult:
|
|
80
|
+
"""Execute root account protection compliance test.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
TestResult with findings for root account security issues
|
|
84
|
+
|
|
85
|
+
Example:
|
|
86
|
+
>>> test = RootAccountProtectionTest(connector)
|
|
87
|
+
>>> result = test.execute()
|
|
88
|
+
>>> print(result.score)
|
|
89
|
+
70.0
|
|
90
|
+
"""
|
|
91
|
+
result = TestResult(
|
|
92
|
+
test_id=self.test_id,
|
|
93
|
+
test_name=self.test_name,
|
|
94
|
+
status=TestStatus.PASSED,
|
|
95
|
+
passed=True,
|
|
96
|
+
score=100.0,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
# Get IAM client
|
|
101
|
+
iam_client = self.connector.get_client("iam")
|
|
102
|
+
|
|
103
|
+
# Initialize score components
|
|
104
|
+
mfa_score = 0 # 50 points
|
|
105
|
+
access_keys_score = 0 # 30 points
|
|
106
|
+
usage_score = 0 # 20 points
|
|
107
|
+
|
|
108
|
+
# Check 1: Root account MFA status
|
|
109
|
+
self.logger.info("checking_root_mfa_status")
|
|
110
|
+
root_mfa_enabled = self._check_root_mfa(iam_client, result)
|
|
111
|
+
|
|
112
|
+
if root_mfa_enabled:
|
|
113
|
+
mfa_score = 50
|
|
114
|
+
self.logger.info("root_mfa_enabled")
|
|
115
|
+
else:
|
|
116
|
+
self.logger.error("root_mfa_disabled")
|
|
117
|
+
# Finding already created in _check_root_mfa
|
|
118
|
+
|
|
119
|
+
# Check 2: Root access keys
|
|
120
|
+
self.logger.info("checking_root_access_keys")
|
|
121
|
+
has_access_keys, access_key_details = self._check_root_access_keys(iam_client, result)
|
|
122
|
+
|
|
123
|
+
if not has_access_keys:
|
|
124
|
+
access_keys_score = 30
|
|
125
|
+
self.logger.info("root_no_access_keys")
|
|
126
|
+
else:
|
|
127
|
+
self.logger.warning("root_has_access_keys", details=access_key_details)
|
|
128
|
+
# Finding already created in _check_root_access_keys
|
|
129
|
+
|
|
130
|
+
# Check 3: Root account usage
|
|
131
|
+
self.logger.info("checking_root_usage")
|
|
132
|
+
recently_used, last_used = self._check_root_usage(iam_client, result)
|
|
133
|
+
|
|
134
|
+
if not recently_used:
|
|
135
|
+
usage_score = 20
|
|
136
|
+
self.logger.info("root_not_recently_used", last_used=last_used)
|
|
137
|
+
else:
|
|
138
|
+
self.logger.warning("root_recently_used", last_used=last_used)
|
|
139
|
+
# Finding already created in _check_root_usage
|
|
140
|
+
|
|
141
|
+
# Calculate total score
|
|
142
|
+
result.score = mfa_score + access_keys_score + usage_score
|
|
143
|
+
result.resources_scanned = 1 # Root account
|
|
144
|
+
|
|
145
|
+
# Determine pass/fail (require all checks to pass)
|
|
146
|
+
result.passed = result.score == 100
|
|
147
|
+
result.status = TestStatus.PASSED if result.passed else TestStatus.FAILED
|
|
148
|
+
|
|
149
|
+
# Add metadata
|
|
150
|
+
result.metadata = {
|
|
151
|
+
"root_mfa_enabled": root_mfa_enabled,
|
|
152
|
+
"root_has_access_keys": has_access_keys,
|
|
153
|
+
"root_recently_used": recently_used,
|
|
154
|
+
"root_last_used": last_used,
|
|
155
|
+
"mfa_score": mfa_score,
|
|
156
|
+
"access_keys_score": access_keys_score,
|
|
157
|
+
"usage_score": usage_score,
|
|
158
|
+
"total_score": result.score,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
self.logger.info(
|
|
162
|
+
"root_account_protection_test_completed",
|
|
163
|
+
score=result.score,
|
|
164
|
+
passed=result.passed,
|
|
165
|
+
mfa=root_mfa_enabled,
|
|
166
|
+
access_keys=has_access_keys,
|
|
167
|
+
recently_used=recently_used
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
except ClientError as e:
|
|
171
|
+
error_code = e.response.get("Error", {}).get("Code")
|
|
172
|
+
self.logger.error("root_account_protection_test_error", error_code=error_code, error=str(e))
|
|
173
|
+
result.status = TestStatus.ERROR
|
|
174
|
+
result.passed = False
|
|
175
|
+
result.score = 0.0
|
|
176
|
+
result.error_message = f"AWS API Error: {error_code} - {str(e)}"
|
|
177
|
+
|
|
178
|
+
except Exception as e:
|
|
179
|
+
self.logger.error("root_account_protection_test_error", error=str(e))
|
|
180
|
+
result.status = TestStatus.ERROR
|
|
181
|
+
result.passed = False
|
|
182
|
+
result.score = 0.0
|
|
183
|
+
result.error_message = str(e)
|
|
184
|
+
|
|
185
|
+
return result
|
|
186
|
+
|
|
187
|
+
def _check_root_mfa(self, iam_client: Any, result: TestResult) -> bool:
|
|
188
|
+
"""Check if root account has MFA enabled.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
iam_client: Boto3 IAM client
|
|
192
|
+
result: TestResult to add findings to
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
True if MFA is enabled, False otherwise
|
|
196
|
+
"""
|
|
197
|
+
try:
|
|
198
|
+
summary = iam_client.get_account_summary()
|
|
199
|
+
account_mfa_enabled = summary.get("SummaryMap", {}).get("AccountMFAEnabled", 0)
|
|
200
|
+
|
|
201
|
+
mfa_enabled = account_mfa_enabled == 1
|
|
202
|
+
|
|
203
|
+
if not mfa_enabled:
|
|
204
|
+
finding = self.create_finding(
|
|
205
|
+
resource_id="root_account",
|
|
206
|
+
resource_type="aws_account",
|
|
207
|
+
severity=Severity.CRITICAL,
|
|
208
|
+
title="Root account MFA not enabled",
|
|
209
|
+
description="The AWS root account does not have Multi-Factor Authentication (MFA) enabled. "
|
|
210
|
+
"The root account has unrestricted access to all resources and services. "
|
|
211
|
+
"Without MFA, the account is vulnerable to credential compromise. "
|
|
212
|
+
"If root credentials are stolen, attackers gain complete control of the AWS account. "
|
|
213
|
+
"ISO 27001 A.8.2 requires strong protection for privileged accounts.",
|
|
214
|
+
remediation=(
|
|
215
|
+
"Enable MFA for the root account immediately:\n\n"
|
|
216
|
+
"1. Sign in to AWS Console as root user\n"
|
|
217
|
+
"2. Click on account name → Security credentials\n"
|
|
218
|
+
"3. Under 'Multi-factor authentication (MFA)', click 'Assign MFA device'\n"
|
|
219
|
+
"4. Choose virtual MFA device (recommended) or hardware MFA device\n"
|
|
220
|
+
"5. For virtual MFA:\n"
|
|
221
|
+
" - Install an authenticator app (Google Authenticator, Authy, etc.)\n"
|
|
222
|
+
" - Scan the QR code\n"
|
|
223
|
+
" - Enter two consecutive MFA codes\n"
|
|
224
|
+
"6. Securely store MFA recovery codes\n\n"
|
|
225
|
+
"Best practices:\n"
|
|
226
|
+
"- Use hardware MFA device for maximum security\n"
|
|
227
|
+
"- Store recovery codes in secure offline location\n"
|
|
228
|
+
"- Never share MFA device or recovery codes\n"
|
|
229
|
+
"- Avoid using root account for day-to-day operations"
|
|
230
|
+
),
|
|
231
|
+
evidence=self.create_evidence(
|
|
232
|
+
resource_id="root_account",
|
|
233
|
+
resource_type="aws_account",
|
|
234
|
+
data={"mfa_enabled": mfa_enabled, "check": "root_mfa"}
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
result.add_finding(finding)
|
|
238
|
+
|
|
239
|
+
return mfa_enabled
|
|
240
|
+
|
|
241
|
+
except Exception as e:
|
|
242
|
+
self.logger.error("root_mfa_check_error", error=str(e))
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
def _check_root_access_keys(self, iam_client: Any, result: TestResult) -> tuple:
|
|
246
|
+
"""Check if root account has access keys.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
iam_client: Boto3 IAM client
|
|
250
|
+
result: TestResult to add findings to
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Tuple of (has_keys: bool, key_details: dict)
|
|
254
|
+
"""
|
|
255
|
+
try:
|
|
256
|
+
# Generate credential report to check root access keys
|
|
257
|
+
self._generate_credential_report(iam_client)
|
|
258
|
+
|
|
259
|
+
report_response = iam_client.get_credential_report()
|
|
260
|
+
report_content = report_response["Content"]
|
|
261
|
+
report_csv = report_content.decode("utf-8")
|
|
262
|
+
csv_reader = csv.DictReader(io.StringIO(report_csv))
|
|
263
|
+
|
|
264
|
+
for row in csv_reader:
|
|
265
|
+
if row.get("user") == "<root_account>":
|
|
266
|
+
key1_active = row.get("access_key_1_active", "false") == "true"
|
|
267
|
+
key2_active = row.get("access_key_2_active", "false") == "true"
|
|
268
|
+
|
|
269
|
+
has_keys = key1_active or key2_active
|
|
270
|
+
|
|
271
|
+
if has_keys:
|
|
272
|
+
active_keys = []
|
|
273
|
+
if key1_active:
|
|
274
|
+
active_keys.append("access_key_1")
|
|
275
|
+
if key2_active:
|
|
276
|
+
active_keys.append("access_key_2")
|
|
277
|
+
|
|
278
|
+
finding = self.create_finding(
|
|
279
|
+
resource_id="root_account",
|
|
280
|
+
resource_type="aws_account",
|
|
281
|
+
severity=Severity.HIGH,
|
|
282
|
+
title="Root account has active access keys",
|
|
283
|
+
description=f"The AWS root account has {len(active_keys)} active access key(s): "
|
|
284
|
+
f"{', '.join(active_keys)}. Root account access keys provide "
|
|
285
|
+
"unrestricted access to all AWS resources and should never exist. "
|
|
286
|
+
"If these keys are leaked or compromised, attackers gain full control. "
|
|
287
|
+
"ISO 27001 A.8.2 requires minimizing use of highly privileged credentials.",
|
|
288
|
+
remediation=(
|
|
289
|
+
"Delete root account access keys immediately:\n\n"
|
|
290
|
+
"1. Sign in to AWS Console as root user\n"
|
|
291
|
+
"2. Click on account name → Security credentials\n"
|
|
292
|
+
"3. Under 'Access keys', locate the active keys\n"
|
|
293
|
+
"4. Click 'Delete' for each access key\n"
|
|
294
|
+
"5. Confirm deletion\n\n"
|
|
295
|
+
"If you need programmatic access:\n"
|
|
296
|
+
"1. Create IAM users with specific permissions\n"
|
|
297
|
+
"2. Use IAM roles for EC2 instances and Lambda functions\n"
|
|
298
|
+
"3. Enable AWS Organizations for multi-account management\n"
|
|
299
|
+
"4. Never use root access keys for any purpose\n\n"
|
|
300
|
+
"CRITICAL: If root keys were exposed:\n"
|
|
301
|
+
"1. Delete keys immediately\n"
|
|
302
|
+
"2. Check CloudTrail for unauthorized activity\n"
|
|
303
|
+
"3. Review all resources for changes\n"
|
|
304
|
+
"4. Contact AWS Support if compromise is suspected"
|
|
305
|
+
),
|
|
306
|
+
evidence=self.create_evidence(
|
|
307
|
+
resource_id="root_account",
|
|
308
|
+
resource_type="aws_account",
|
|
309
|
+
data={
|
|
310
|
+
"access_key_1_active": key1_active,
|
|
311
|
+
"access_key_2_active": key2_active,
|
|
312
|
+
"check": "root_access_keys"
|
|
313
|
+
}
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
result.add_finding(finding)
|
|
317
|
+
|
|
318
|
+
return has_keys, {"key1": key1_active, "key2": key2_active}
|
|
319
|
+
|
|
320
|
+
return False, {}
|
|
321
|
+
|
|
322
|
+
except Exception as e:
|
|
323
|
+
self.logger.error("root_access_keys_check_error", error=str(e))
|
|
324
|
+
return False, {}
|
|
325
|
+
|
|
326
|
+
def _check_root_usage(self, iam_client: Any, result: TestResult) -> tuple:
|
|
327
|
+
"""Check when root account was last used.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
iam_client: Boto3 IAM client
|
|
331
|
+
result: TestResult to add findings to
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
Tuple of (recently_used: bool, last_used: str)
|
|
335
|
+
"""
|
|
336
|
+
try:
|
|
337
|
+
# Get credential report for root usage info
|
|
338
|
+
self._generate_credential_report(iam_client)
|
|
339
|
+
|
|
340
|
+
report_response = iam_client.get_credential_report()
|
|
341
|
+
report_content = report_response["Content"]
|
|
342
|
+
report_csv = report_content.decode("utf-8")
|
|
343
|
+
csv_reader = csv.DictReader(io.StringIO(report_csv))
|
|
344
|
+
|
|
345
|
+
for row in csv_reader:
|
|
346
|
+
if row.get("user") == "<root_account>":
|
|
347
|
+
password_last_used = row.get("password_last_used", "no_information")
|
|
348
|
+
|
|
349
|
+
if password_last_used == "no_information" or password_last_used == "N/A":
|
|
350
|
+
# Root has never been used (good)
|
|
351
|
+
return False, "never"
|
|
352
|
+
|
|
353
|
+
# Parse last used date
|
|
354
|
+
try:
|
|
355
|
+
last_used_date = datetime.fromisoformat(password_last_used.replace("Z", "+00:00"))
|
|
356
|
+
now = datetime.now(timezone.utc)
|
|
357
|
+
days_since_use = (now - last_used_date).days
|
|
358
|
+
|
|
359
|
+
recently_used = days_since_use < 90
|
|
360
|
+
|
|
361
|
+
if recently_used:
|
|
362
|
+
finding = self.create_finding(
|
|
363
|
+
resource_id="root_account",
|
|
364
|
+
resource_type="aws_account",
|
|
365
|
+
severity=Severity.MEDIUM,
|
|
366
|
+
title=f"Root account used recently ({days_since_use} days ago)",
|
|
367
|
+
description=f"The AWS root account was last used {days_since_use} days ago "
|
|
368
|
+
f"(on {password_last_used}). Root account should only be used for "
|
|
369
|
+
"account and service management tasks that cannot be performed by IAM users. "
|
|
370
|
+
"Regular use of root account increases risk of credential compromise. "
|
|
371
|
+
"ISO 27001 A.8.2 requires limiting use of privileged accounts.",
|
|
372
|
+
remediation=(
|
|
373
|
+
"Minimize root account usage:\n\n"
|
|
374
|
+
"1. Create IAM users with appropriate permissions for day-to-day tasks\n"
|
|
375
|
+
"2. Use IAM roles for service-to-service access\n"
|
|
376
|
+
"3. Enable AWS Organizations for centralized management\n"
|
|
377
|
+
"4. Use IAM users for administrative tasks\n\n"
|
|
378
|
+
"Root account should only be used for:\n"
|
|
379
|
+
"- Changing account settings\n"
|
|
380
|
+
"- Closing the account\n"
|
|
381
|
+
"- Restoring IAM user permissions\n"
|
|
382
|
+
"- Managing AWS support plan\n"
|
|
383
|
+
"- Tasks specifically requiring root access\n\n"
|
|
384
|
+
"Best practices:\n"
|
|
385
|
+
"- Lock away root credentials securely\n"
|
|
386
|
+
"- Enable MFA on root account\n"
|
|
387
|
+
"- Monitor root account usage via CloudTrail\n"
|
|
388
|
+
"- Set up SNS alerts for root account activity"
|
|
389
|
+
),
|
|
390
|
+
evidence=self.create_evidence(
|
|
391
|
+
resource_id="root_account",
|
|
392
|
+
resource_type="aws_account",
|
|
393
|
+
data={
|
|
394
|
+
"password_last_used": password_last_used,
|
|
395
|
+
"days_since_use": days_since_use,
|
|
396
|
+
"check": "root_usage"
|
|
397
|
+
}
|
|
398
|
+
)
|
|
399
|
+
)
|
|
400
|
+
result.add_finding(finding)
|
|
401
|
+
|
|
402
|
+
return recently_used, password_last_used
|
|
403
|
+
|
|
404
|
+
except Exception as e:
|
|
405
|
+
self.logger.error("parse_last_used_date_error", error=str(e))
|
|
406
|
+
return False, password_last_used
|
|
407
|
+
|
|
408
|
+
return False, "unknown"
|
|
409
|
+
|
|
410
|
+
except Exception as e:
|
|
411
|
+
self.logger.error("root_usage_check_error", error=str(e))
|
|
412
|
+
return False, "error"
|
|
413
|
+
|
|
414
|
+
def _generate_credential_report(self, iam_client: Any, max_retries: int = 3) -> bool:
|
|
415
|
+
"""Generate IAM credential report with retry logic.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
iam_client: Boto3 IAM client
|
|
419
|
+
max_retries: Maximum number of retries
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
True if successful, False otherwise
|
|
423
|
+
"""
|
|
424
|
+
for attempt in range(max_retries):
|
|
425
|
+
try:
|
|
426
|
+
response = iam_client.generate_credential_report()
|
|
427
|
+
state = response.get("State")
|
|
428
|
+
|
|
429
|
+
if state == "COMPLETE":
|
|
430
|
+
return True
|
|
431
|
+
elif state == "INPROGRESS":
|
|
432
|
+
time.sleep(5)
|
|
433
|
+
else:
|
|
434
|
+
time.sleep(5)
|
|
435
|
+
|
|
436
|
+
except ClientError as e:
|
|
437
|
+
error_code = e.response.get("Error", {}).get("Code")
|
|
438
|
+
if error_code == "LimitExceededException":
|
|
439
|
+
time.sleep(5)
|
|
440
|
+
else:
|
|
441
|
+
raise
|
|
442
|
+
|
|
443
|
+
return False
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
# ============================================================================
|
|
447
|
+
# CONVENIENCE FUNCTION
|
|
448
|
+
# ============================================================================
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def run_root_account_protection_test(connector: AWSConnector) -> TestResult:
|
|
452
|
+
"""Run root account protection compliance test.
|
|
453
|
+
|
|
454
|
+
Convenience function for running the test.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
connector: AWS connector
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
TestResult
|
|
461
|
+
|
|
462
|
+
Example:
|
|
463
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
464
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
465
|
+
>>> connector.connect()
|
|
466
|
+
>>> result = run_root_account_protection_test(connector)
|
|
467
|
+
>>> print(f"Score: {result.score}%")
|
|
468
|
+
"""
|
|
469
|
+
test = RootAccountProtectionTest(connector)
|
|
470
|
+
return test.execute()
|
|
File without changes
|