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,460 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IAM password policy compliance test.
|
|
3
|
+
|
|
4
|
+
Checks that AWS account has a strong IAM password policy configured.
|
|
5
|
+
|
|
6
|
+
ISO 27001 Control: A.9.4.3 - Password Management System
|
|
7
|
+
Requirement: Password management systems must be interactive and ensure quality passwords
|
|
8
|
+
|
|
9
|
+
Password policy requirements:
|
|
10
|
+
- Minimum password length >= 14 characters
|
|
11
|
+
- Require uppercase letters
|
|
12
|
+
- Require lowercase letters
|
|
13
|
+
- Require numbers
|
|
14
|
+
- Require symbols
|
|
15
|
+
- Password expiration <= 90 days
|
|
16
|
+
- Password reuse prevention (>= 5 passwords)
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
20
|
+
>>> from complio.tests_library.infrastructure.iam_password_policy import IAMPasswordPolicyTest
|
|
21
|
+
>>>
|
|
22
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
23
|
+
>>> connector.connect()
|
|
24
|
+
>>>
|
|
25
|
+
>>> test = IAMPasswordPolicyTest(connector)
|
|
26
|
+
>>> result = test.run()
|
|
27
|
+
>>> print(f"Passed: {result.passed}, Score: {result.score}")
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from typing import Any, Dict, List
|
|
31
|
+
|
|
32
|
+
from botocore.exceptions import ClientError
|
|
33
|
+
|
|
34
|
+
from complio.connectors.aws.client import AWSConnector
|
|
35
|
+
from complio.tests_library.base import (
|
|
36
|
+
ComplianceTest,
|
|
37
|
+
Evidence,
|
|
38
|
+
Finding,
|
|
39
|
+
Severity,
|
|
40
|
+
TestResult,
|
|
41
|
+
TestStatus,
|
|
42
|
+
)
|
|
43
|
+
from complio.utils.logger import get_logger
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class IAMPasswordPolicyTest(ComplianceTest):
|
|
47
|
+
"""Test for IAM password policy compliance.
|
|
48
|
+
|
|
49
|
+
Verifies that AWS account has a strong password policy configured.
|
|
50
|
+
|
|
51
|
+
Compliance Requirements (ISO 27001 A.9.4.3):
|
|
52
|
+
- Minimum password length >= 14 characters
|
|
53
|
+
- Require at least one uppercase letter
|
|
54
|
+
- Require at least one lowercase letter
|
|
55
|
+
- Require at least one number
|
|
56
|
+
- Require at least one symbol
|
|
57
|
+
- Password expiration <= 90 days
|
|
58
|
+
- Prevent password reuse (>= 5 previous passwords)
|
|
59
|
+
- Require password change on first login
|
|
60
|
+
|
|
61
|
+
Scoring:
|
|
62
|
+
- 100% if all requirements are met
|
|
63
|
+
- Deductions for each missing requirement
|
|
64
|
+
- Critical failure if minimum length < 8
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
>>> test = IAMPasswordPolicyTest(connector)
|
|
68
|
+
>>> result = test.run()
|
|
69
|
+
>>> if not result.passed:
|
|
70
|
+
... for finding in result.findings:
|
|
71
|
+
... print(f"{finding.title}: {finding.description}")
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, connector: AWSConnector) -> None:
|
|
75
|
+
"""Initialize IAM password policy test.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
connector: AWS connector instance
|
|
79
|
+
"""
|
|
80
|
+
super().__init__(
|
|
81
|
+
test_id="iam_password_policy",
|
|
82
|
+
test_name="IAM Password Policy Compliance",
|
|
83
|
+
description="Verifies that a strong password policy is configured requiring minimum length, complexity, expiration, and reuse prevention (account-wide)",
|
|
84
|
+
control_id="A.9.4.3",
|
|
85
|
+
connector=connector,
|
|
86
|
+
scope="global",
|
|
87
|
+
)
|
|
88
|
+
self.logger = get_logger(__name__)
|
|
89
|
+
|
|
90
|
+
def execute(self) -> TestResult:
|
|
91
|
+
"""Execute the IAM password policy compliance test.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
TestResult with findings and evidence
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
AWSConnectionError: If unable to connect to AWS
|
|
98
|
+
AWSCredentialsError: If credentials are invalid
|
|
99
|
+
"""
|
|
100
|
+
self.logger.info(
|
|
101
|
+
"starting_iam_password_policy_test",
|
|
102
|
+
region=self.connector.region,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
findings: List[Finding] = []
|
|
106
|
+
evidence_list: List[Evidence] = []
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
# Get IAM client (IAM is global, but we use connector's region)
|
|
110
|
+
iam_client = self.connector.get_client("iam")
|
|
111
|
+
|
|
112
|
+
# Get account password policy
|
|
113
|
+
try:
|
|
114
|
+
response = iam_client.get_account_password_policy()
|
|
115
|
+
policy = response.get("PasswordPolicy", {})
|
|
116
|
+
policy_exists = True
|
|
117
|
+
except ClientError as e:
|
|
118
|
+
if e.response.get("Error", {}).get("Code") == "NoSuchEntity":
|
|
119
|
+
# No password policy configured
|
|
120
|
+
policy = {}
|
|
121
|
+
policy_exists = False
|
|
122
|
+
else:
|
|
123
|
+
raise
|
|
124
|
+
|
|
125
|
+
# Create evidence
|
|
126
|
+
evidence = Evidence(
|
|
127
|
+
resource_id="aws-account",
|
|
128
|
+
resource_type="iam_password_policy",
|
|
129
|
+
region=self.connector.region,
|
|
130
|
+
data={
|
|
131
|
+
"policy_exists": policy_exists,
|
|
132
|
+
"minimum_password_length": policy.get("MinimumPasswordLength", 0),
|
|
133
|
+
"require_uppercase": policy.get("RequireUppercaseCharacters", False),
|
|
134
|
+
"require_lowercase": policy.get("RequireLowercaseCharacters", False),
|
|
135
|
+
"require_numbers": policy.get("RequireNumbers", False),
|
|
136
|
+
"require_symbols": policy.get("RequireSymbols", False),
|
|
137
|
+
"max_password_age": policy.get("MaxPasswordAge", None),
|
|
138
|
+
"password_reuse_prevention": policy.get("PasswordReusePrevention", 0),
|
|
139
|
+
"hard_expiry": policy.get("HardExpiry", False),
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
evidence_list.append(evidence)
|
|
143
|
+
|
|
144
|
+
# If no policy exists, this is a critical failure
|
|
145
|
+
if not policy_exists:
|
|
146
|
+
findings.append(
|
|
147
|
+
Finding(
|
|
148
|
+
resource_id="aws-account",
|
|
149
|
+
resource_type="iam_password_policy",
|
|
150
|
+
severity=Severity.CRITICAL,
|
|
151
|
+
title="No IAM password policy configured",
|
|
152
|
+
description=(
|
|
153
|
+
"The AWS account does not have an IAM password policy configured. "
|
|
154
|
+
"This allows users to set weak passwords, violating ISO 27001 A.9.4.3. "
|
|
155
|
+
"Users could set passwords like '12345' or 'password', creating "
|
|
156
|
+
"a severe security risk."
|
|
157
|
+
),
|
|
158
|
+
remediation=(
|
|
159
|
+
"Configure an IAM password policy:\n"
|
|
160
|
+
"1. Go to IAM Console → Account Settings\n"
|
|
161
|
+
"2. Click 'Set password policy'\n"
|
|
162
|
+
"3. Enable all recommended settings:\n"
|
|
163
|
+
" - Minimum length: 14 characters\n"
|
|
164
|
+
" - Require uppercase, lowercase, numbers, symbols\n"
|
|
165
|
+
" - Password expiration: 90 days\n"
|
|
166
|
+
" - Prevent reuse of last 5 passwords\n"
|
|
167
|
+
" - Require administrator reset on first login"
|
|
168
|
+
),
|
|
169
|
+
iso27001_control="A.9.4.3",
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return TestResult(
|
|
174
|
+
test_id=self.test_id,
|
|
175
|
+
test_name=self.test_name,
|
|
176
|
+
status=TestStatus.FAILED,
|
|
177
|
+
passed=False,
|
|
178
|
+
score=0.0,
|
|
179
|
+
findings=findings,
|
|
180
|
+
evidence=evidence_list,
|
|
181
|
+
metadata={
|
|
182
|
+
"region": self.connector.region,
|
|
183
|
+
"policy_exists": False,
|
|
184
|
+
"iso27001_control": "A.9.4.3",
|
|
185
|
+
},
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Check each requirement
|
|
189
|
+
score_deductions = 0
|
|
190
|
+
requirements_met = 0
|
|
191
|
+
total_requirements = 8
|
|
192
|
+
|
|
193
|
+
# 1. Minimum password length (>= 14)
|
|
194
|
+
min_length = policy.get("MinimumPasswordLength", 0)
|
|
195
|
+
if min_length < 8:
|
|
196
|
+
score_deductions += 50 # Critical - very weak passwords
|
|
197
|
+
findings.append(
|
|
198
|
+
Finding(
|
|
199
|
+
resource_id="aws-account",
|
|
200
|
+
resource_type="iam_password_policy",
|
|
201
|
+
severity=Severity.CRITICAL,
|
|
202
|
+
title="Password minimum length too short",
|
|
203
|
+
description=(
|
|
204
|
+
f"Minimum password length is {min_length} characters, which is "
|
|
205
|
+
f"critically insufficient. NIST recommends minimum 8 characters, "
|
|
206
|
+
f"ISO 27001 best practice recommends 14+ characters."
|
|
207
|
+
),
|
|
208
|
+
remediation=f"Increase MinimumPasswordLength to at least 14 (current: {min_length})",
|
|
209
|
+
iso27001_control="A.9.4.3",
|
|
210
|
+
metadata={"current_value": min_length, "required_value": 14},
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
elif min_length < 14:
|
|
214
|
+
score_deductions += 15
|
|
215
|
+
findings.append(
|
|
216
|
+
Finding(
|
|
217
|
+
resource_id="aws-account",
|
|
218
|
+
resource_type="iam_password_policy",
|
|
219
|
+
severity=Severity.MEDIUM,
|
|
220
|
+
title="Password minimum length below best practice",
|
|
221
|
+
description=(
|
|
222
|
+
f"Minimum password length is {min_length} characters. "
|
|
223
|
+
f"While above critical threshold, ISO 27001 best practice recommends 14+ characters."
|
|
224
|
+
),
|
|
225
|
+
remediation=f"Increase MinimumPasswordLength to 14 (current: {min_length})",
|
|
226
|
+
iso27001_control="A.9.4.3",
|
|
227
|
+
metadata={"current_value": min_length, "required_value": 14},
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
requirements_met += 1
|
|
232
|
+
|
|
233
|
+
# 2. Require uppercase
|
|
234
|
+
if not policy.get("RequireUppercaseCharacters", False):
|
|
235
|
+
score_deductions += 10
|
|
236
|
+
findings.append(
|
|
237
|
+
Finding(
|
|
238
|
+
resource_id="aws-account",
|
|
239
|
+
resource_type="iam_password_policy",
|
|
240
|
+
severity=Severity.MEDIUM,
|
|
241
|
+
title="Password policy doesn't require uppercase letters",
|
|
242
|
+
description="Passwords can be created without uppercase letters, reducing password complexity.",
|
|
243
|
+
remediation="Enable RequireUppercaseCharacters in password policy",
|
|
244
|
+
iso27001_control="A.9.4.3",
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
else:
|
|
248
|
+
requirements_met += 1
|
|
249
|
+
|
|
250
|
+
# 3. Require lowercase
|
|
251
|
+
if not policy.get("RequireLowercaseCharacters", False):
|
|
252
|
+
score_deductions += 10
|
|
253
|
+
findings.append(
|
|
254
|
+
Finding(
|
|
255
|
+
resource_id="aws-account",
|
|
256
|
+
resource_type="iam_password_policy",
|
|
257
|
+
severity=Severity.MEDIUM,
|
|
258
|
+
title="Password policy doesn't require lowercase letters",
|
|
259
|
+
description="Passwords can be created without lowercase letters, reducing password complexity.",
|
|
260
|
+
remediation="Enable RequireLowercaseCharacters in password policy",
|
|
261
|
+
iso27001_control="A.9.4.3",
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
else:
|
|
265
|
+
requirements_met += 1
|
|
266
|
+
|
|
267
|
+
# 4. Require numbers
|
|
268
|
+
if not policy.get("RequireNumbers", False):
|
|
269
|
+
score_deductions += 10
|
|
270
|
+
findings.append(
|
|
271
|
+
Finding(
|
|
272
|
+
resource_id="aws-account",
|
|
273
|
+
resource_type="iam_password_policy",
|
|
274
|
+
severity=Severity.MEDIUM,
|
|
275
|
+
title="Password policy doesn't require numbers",
|
|
276
|
+
description="Passwords can be created without numbers, reducing password complexity.",
|
|
277
|
+
remediation="Enable RequireNumbers in password policy",
|
|
278
|
+
iso27001_control="A.9.4.3",
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
else:
|
|
282
|
+
requirements_met += 1
|
|
283
|
+
|
|
284
|
+
# 5. Require symbols
|
|
285
|
+
if not policy.get("RequireSymbols", False):
|
|
286
|
+
score_deductions += 10
|
|
287
|
+
findings.append(
|
|
288
|
+
Finding(
|
|
289
|
+
resource_id="aws-account",
|
|
290
|
+
resource_type="iam_password_policy",
|
|
291
|
+
severity=Severity.MEDIUM,
|
|
292
|
+
title="Password policy doesn't require symbols",
|
|
293
|
+
description="Passwords can be created without special characters, reducing password complexity.",
|
|
294
|
+
remediation="Enable RequireSymbols in password policy",
|
|
295
|
+
iso27001_control="A.9.4.3",
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
else:
|
|
299
|
+
requirements_met += 1
|
|
300
|
+
|
|
301
|
+
# 6. Password expiration (<=90 days)
|
|
302
|
+
max_age = policy.get("MaxPasswordAge")
|
|
303
|
+
if max_age is None or max_age == 0:
|
|
304
|
+
score_deductions += 15
|
|
305
|
+
findings.append(
|
|
306
|
+
Finding(
|
|
307
|
+
resource_id="aws-account",
|
|
308
|
+
resource_type="iam_password_policy",
|
|
309
|
+
severity=Severity.HIGH,
|
|
310
|
+
title="Passwords never expire",
|
|
311
|
+
description=(
|
|
312
|
+
"Password policy does not enforce password expiration. "
|
|
313
|
+
"Passwords should be rotated regularly to limit exposure from compromised credentials."
|
|
314
|
+
),
|
|
315
|
+
remediation="Set MaxPasswordAge to 90 days or less",
|
|
316
|
+
iso27001_control="A.9.4.3",
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
elif max_age > 90:
|
|
320
|
+
score_deductions += 10
|
|
321
|
+
findings.append(
|
|
322
|
+
Finding(
|
|
323
|
+
resource_id="aws-account",
|
|
324
|
+
resource_type="iam_password_policy",
|
|
325
|
+
severity=Severity.MEDIUM,
|
|
326
|
+
title="Password expiration too long",
|
|
327
|
+
description=(
|
|
328
|
+
f"Passwords expire after {max_age} days. "
|
|
329
|
+
f"ISO 27001 best practice recommends password rotation every 90 days or less."
|
|
330
|
+
),
|
|
331
|
+
remediation=f"Reduce MaxPasswordAge to 90 or less (current: {max_age})",
|
|
332
|
+
iso27001_control="A.9.4.3",
|
|
333
|
+
metadata={"current_value": max_age, "required_value": 90},
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
else:
|
|
337
|
+
requirements_met += 1
|
|
338
|
+
|
|
339
|
+
# 7. Password reuse prevention (>=5)
|
|
340
|
+
reuse_prevention = policy.get("PasswordReusePrevention", 0)
|
|
341
|
+
if reuse_prevention == 0:
|
|
342
|
+
score_deductions += 15
|
|
343
|
+
findings.append(
|
|
344
|
+
Finding(
|
|
345
|
+
resource_id="aws-account",
|
|
346
|
+
resource_type="iam_password_policy",
|
|
347
|
+
severity=Severity.HIGH,
|
|
348
|
+
title="Password reuse not prevented",
|
|
349
|
+
description=(
|
|
350
|
+
"Users can reuse old passwords immediately. This allows users to "
|
|
351
|
+
"rotate back to previously compromised passwords."
|
|
352
|
+
),
|
|
353
|
+
remediation="Set PasswordReusePrevention to 5 or more",
|
|
354
|
+
iso27001_control="A.9.4.3",
|
|
355
|
+
)
|
|
356
|
+
)
|
|
357
|
+
elif reuse_prevention < 5:
|
|
358
|
+
score_deductions += 10
|
|
359
|
+
findings.append(
|
|
360
|
+
Finding(
|
|
361
|
+
resource_id="aws-account",
|
|
362
|
+
resource_type="iam_password_policy",
|
|
363
|
+
severity=Severity.MEDIUM,
|
|
364
|
+
title="Password reuse prevention too low",
|
|
365
|
+
description=(
|
|
366
|
+
f"Password policy prevents reuse of last {reuse_prevention} passwords. "
|
|
367
|
+
f"Best practice recommends preventing reuse of at least 5 previous passwords."
|
|
368
|
+
),
|
|
369
|
+
remediation=f"Increase PasswordReusePrevention to 5 or more (current: {reuse_prevention})",
|
|
370
|
+
iso27001_control="A.9.4.3",
|
|
371
|
+
metadata={"current_value": reuse_prevention, "required_value": 5},
|
|
372
|
+
)
|
|
373
|
+
)
|
|
374
|
+
else:
|
|
375
|
+
requirements_met += 1
|
|
376
|
+
|
|
377
|
+
# 8. Expire passwords (HardExpiry)
|
|
378
|
+
if not policy.get("HardExpiry", False):
|
|
379
|
+
score_deductions += 5
|
|
380
|
+
findings.append(
|
|
381
|
+
Finding(
|
|
382
|
+
resource_id="aws-account",
|
|
383
|
+
resource_type="iam_password_policy",
|
|
384
|
+
severity=Severity.LOW,
|
|
385
|
+
title="Hard expiry not enabled",
|
|
386
|
+
description=(
|
|
387
|
+
"Password hard expiry is not enabled. Users can continue using expired passwords "
|
|
388
|
+
"if they don't log into the console."
|
|
389
|
+
),
|
|
390
|
+
remediation="Enable HardExpiry in password policy",
|
|
391
|
+
iso27001_control="A.9.4.3",
|
|
392
|
+
)
|
|
393
|
+
)
|
|
394
|
+
else:
|
|
395
|
+
requirements_met += 1
|
|
396
|
+
|
|
397
|
+
# Calculate final score
|
|
398
|
+
score = max(0.0, 100.0 - score_deductions)
|
|
399
|
+
|
|
400
|
+
if score >= 90:
|
|
401
|
+
status = TestStatus.PASSED
|
|
402
|
+
passed = True
|
|
403
|
+
elif score >= 70:
|
|
404
|
+
status = TestStatus.WARNING
|
|
405
|
+
passed = False
|
|
406
|
+
else:
|
|
407
|
+
status = TestStatus.FAILED
|
|
408
|
+
passed = False
|
|
409
|
+
|
|
410
|
+
self.logger.info(
|
|
411
|
+
"iam_password_policy_test_complete",
|
|
412
|
+
requirements_met=requirements_met,
|
|
413
|
+
total_requirements=total_requirements,
|
|
414
|
+
score=score,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
return TestResult(
|
|
418
|
+
test_id=self.test_id,
|
|
419
|
+
test_name=self.test_name,
|
|
420
|
+
status=status,
|
|
421
|
+
passed=passed,
|
|
422
|
+
score=score,
|
|
423
|
+
findings=findings,
|
|
424
|
+
evidence=evidence_list,
|
|
425
|
+
metadata={
|
|
426
|
+
"region": self.connector.region,
|
|
427
|
+
"policy_exists": policy_exists,
|
|
428
|
+
"requirements_met": requirements_met,
|
|
429
|
+
"total_requirements": total_requirements,
|
|
430
|
+
"iso27001_control": "A.9.4.3",
|
|
431
|
+
},
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
except ClientError as e:
|
|
435
|
+
self.logger.error(
|
|
436
|
+
"iam_password_policy_test_failed",
|
|
437
|
+
error=str(e),
|
|
438
|
+
error_code=e.response.get("Error", {}).get("Code"),
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
return TestResult(
|
|
442
|
+
test_id=self.test_id,
|
|
443
|
+
test_name=self.test_name,
|
|
444
|
+
status=TestStatus.ERROR,
|
|
445
|
+
passed=False,
|
|
446
|
+
score=0.0,
|
|
447
|
+
findings=[
|
|
448
|
+
Finding(
|
|
449
|
+
resource_id="aws-account",
|
|
450
|
+
resource_type="iam_password_policy",
|
|
451
|
+
severity=Severity.HIGH,
|
|
452
|
+
title="Failed to check IAM password policy",
|
|
453
|
+
description=f"Error accessing IAM: {str(e)}",
|
|
454
|
+
remediation="Check AWS credentials and permissions. Ensure IAM policy allows iam:GetAccountPasswordPolicy",
|
|
455
|
+
iso27001_control="A.9.4.3",
|
|
456
|
+
)
|
|
457
|
+
],
|
|
458
|
+
evidence=[],
|
|
459
|
+
metadata={"error": str(e)},
|
|
460
|
+
)
|