regscale-cli 6.27.3.0__py3-none-any.whl → 6.28.0.0__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.

Potentially problematic release.


This version of regscale-cli might be problematic. Click here for more details.

Files changed (112) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/utils/app_utils.py +11 -2
  3. regscale/dev/cli.py +26 -0
  4. regscale/dev/version.py +72 -0
  5. regscale/integrations/commercial/__init__.py +15 -1
  6. regscale/integrations/commercial/amazon/amazon/__init__.py +0 -0
  7. regscale/integrations/commercial/amazon/amazon/common.py +204 -0
  8. regscale/integrations/commercial/amazon/common.py +48 -58
  9. regscale/integrations/commercial/aws/audit_manager_compliance.py +2671 -0
  10. regscale/integrations/commercial/aws/cli.py +3093 -55
  11. regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
  12. regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
  13. regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
  14. regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
  15. regscale/integrations/commercial/aws/config_compliance.py +914 -0
  16. regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
  17. regscale/integrations/commercial/aws/evidence_generator.py +283 -0
  18. regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
  19. regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
  20. regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
  21. regscale/integrations/commercial/aws/iam_evidence.py +574 -0
  22. regscale/integrations/commercial/aws/inventory/__init__.py +223 -22
  23. regscale/integrations/commercial/aws/inventory/base.py +107 -5
  24. regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
  25. regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
  26. regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
  27. regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
  28. regscale/integrations/commercial/aws/inventory/resources/compute.py +66 -9
  29. regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
  30. regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
  31. regscale/integrations/commercial/aws/inventory/resources/database.py +106 -31
  32. regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
  33. regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
  34. regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
  35. regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
  36. regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
  37. regscale/integrations/commercial/aws/inventory/resources/networking.py +103 -67
  38. regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
  39. regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
  40. regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
  41. regscale/integrations/commercial/aws/inventory/resources/storage.py +53 -29
  42. regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
  43. regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
  44. regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
  45. regscale/integrations/commercial/aws/kms_evidence.py +879 -0
  46. regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
  47. regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
  48. regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
  49. regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
  50. regscale/integrations/commercial/aws/org_evidence.py +666 -0
  51. regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
  52. regscale/integrations/commercial/aws/s3_evidence.py +632 -0
  53. regscale/integrations/commercial/aws/scanner.py +851 -206
  54. regscale/integrations/commercial/aws/security_hub.py +319 -0
  55. regscale/integrations/commercial/aws/session_manager.py +282 -0
  56. regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
  57. regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
  58. regscale/integrations/compliance_integration.py +308 -38
  59. regscale/integrations/due_date_handler.py +3 -0
  60. regscale/integrations/scanner_integration.py +399 -84
  61. regscale/models/integration_models/cisa_kev_data.json +34 -4
  62. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  63. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +17 -9
  64. regscale/models/regscale_models/assessment.py +2 -1
  65. regscale/models/regscale_models/control_objective.py +74 -5
  66. regscale/models/regscale_models/file.py +2 -0
  67. regscale/models/regscale_models/issue.py +2 -5
  68. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/METADATA +1 -1
  69. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/RECORD +112 -33
  70. tests/regscale/integrations/commercial/aws/__init__.py +0 -0
  71. tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
  72. tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
  73. tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
  74. tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
  75. tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
  76. tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
  77. tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
  78. tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
  79. tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
  80. tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
  81. tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
  82. tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
  83. tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
  84. tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
  85. tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
  86. tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
  87. tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
  88. tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
  89. tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
  90. tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
  91. tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
  92. tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
  93. tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
  94. tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
  95. tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
  96. tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
  97. tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
  98. tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
  99. tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
  100. tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
  101. tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
  102. tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
  103. tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
  104. tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
  105. tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
  106. tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
  107. tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
  108. tests/regscale/integrations/commercial/test_aws.py +55 -56
  109. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/LICENSE +0 -0
  110. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/WHEEL +0 -0
  111. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/entry_points.txt +0 -0
  112. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,718 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Unit tests for AWS IAM Control Mappings."""
4
+
5
+ import pytest
6
+
7
+ from regscale.integrations.commercial.aws.iam_control_mappings import (
8
+ ACCESS_KEY_MAX_AGE_DAYS,
9
+ CREDENTIAL_UNUSED_DAYS,
10
+ IAM_CONTROL_MAPPINGS,
11
+ ISO_27001_MAPPINGS,
12
+ PASSWORD_MAX_AGE_DAYS,
13
+ STRONG_PASSWORD_POLICY,
14
+ IAMControlMapper,
15
+ )
16
+
17
+
18
+ class TestIAMControlMapper:
19
+ """Test IAMControlMapper class."""
20
+
21
+ def test_init_default_framework(self):
22
+ """Test initialization with default framework."""
23
+ mapper = IAMControlMapper()
24
+ assert mapper.framework == "NIST800-53R5"
25
+ assert mapper.mappings == IAM_CONTROL_MAPPINGS
26
+
27
+ def test_init_iso27001_framework(self):
28
+ """Test initialization with ISO27001 framework."""
29
+ mapper = IAMControlMapper(framework="ISO27001")
30
+ assert mapper.framework == "ISO27001"
31
+ assert mapper.mappings == ISO_27001_MAPPINGS
32
+
33
+ def test_init_nist_framework_explicit(self):
34
+ """Test initialization with explicit NIST framework."""
35
+ mapper = IAMControlMapper(framework="NIST800-53R5")
36
+ assert mapper.framework == "NIST800-53R5"
37
+ assert mapper.mappings == IAM_CONTROL_MAPPINGS
38
+
39
+ def test_get_mapped_controls_nist(self):
40
+ """Test getting list of mapped controls for NIST."""
41
+ mapper = IAMControlMapper()
42
+ controls = mapper.get_mapped_controls()
43
+ assert isinstance(controls, list)
44
+ assert len(controls) > 0
45
+ assert "AC-2" in controls
46
+ assert "AC-6" in controls
47
+ assert "IA-2" in controls
48
+ assert "IA-5" in controls
49
+ assert "AC-3" in controls
50
+
51
+ def test_get_mapped_controls_iso(self):
52
+ """Test getting list of mapped controls for ISO27001."""
53
+ mapper = IAMControlMapper(framework="ISO27001")
54
+ controls = mapper.get_mapped_controls()
55
+ assert isinstance(controls, list)
56
+ assert len(controls) > 0
57
+ assert "A.9.2.1" in controls
58
+ assert "A.9.2.2" in controls
59
+ assert "A.9.4.3" in controls
60
+
61
+ def test_get_control_description_valid(self):
62
+ """Test getting control description for valid control."""
63
+ mapper = IAMControlMapper()
64
+ description = mapper.get_control_description("AC-2")
65
+ assert description is not None
66
+ assert "Account Management" in description
67
+ assert "Manage system accounts" in description
68
+
69
+ def test_get_control_description_invalid(self):
70
+ """Test getting control description for invalid control."""
71
+ mapper = IAMControlMapper()
72
+ description = mapper.get_control_description("INVALID-CONTROL")
73
+ assert description is None
74
+
75
+ def test_get_check_details_valid(self):
76
+ """Test getting check details for valid control."""
77
+ mapper = IAMControlMapper()
78
+ checks = mapper.get_check_details("AC-2")
79
+ assert checks is not None
80
+ assert isinstance(checks, dict)
81
+ assert "user_mfa" in checks
82
+ assert "inactive_users" in checks
83
+ assert "root_account_usage" in checks
84
+ assert checks["user_mfa"]["weight"] == 100
85
+
86
+ def test_get_check_details_invalid(self):
87
+ """Test getting check details for invalid control."""
88
+ mapper = IAMControlMapper()
89
+ checks = mapper.get_check_details("INVALID-CONTROL")
90
+ assert checks is None
91
+
92
+
93
+ class TestAssessIAMCompliance:
94
+ """Test assess_iam_compliance method."""
95
+
96
+ def test_assess_compliance_all_pass(self):
97
+ """Test assessment when all controls pass."""
98
+ mapper = IAMControlMapper()
99
+ iam_data = {
100
+ "users": [{"UserName": "user1", "MfaEnabled": True, "AttachedPolicies": [], "InlinePolicies": []}],
101
+ "account_summary": {
102
+ "AccountMFAEnabled": True,
103
+ "AccountAccessKeysPresent": 0,
104
+ },
105
+ "password_policy": {
106
+ "MinimumPasswordLength": 14,
107
+ "RequireSymbols": True,
108
+ "RequireNumbers": True,
109
+ "RequireUppercaseCharacters": True,
110
+ "RequireLowercaseCharacters": True,
111
+ "MaxPasswordAge": 90,
112
+ "PasswordReusePrevention": 24,
113
+ },
114
+ "roles": [
115
+ {
116
+ "RoleName": "test-role",
117
+ "AssumeRolePolicyDocument": {"Statement": [{"Principal": {"Service": "ec2.amazonaws.com"}}]},
118
+ }
119
+ ],
120
+ }
121
+ results = mapper.assess_iam_compliance(iam_data)
122
+ assert results["AC-2"] == "PASS"
123
+ assert results["AC-6"] == "PASS"
124
+ assert results["IA-2"] == "PASS"
125
+ assert results["IA-5"] == "PASS"
126
+ assert results["AC-3"] == "PASS"
127
+
128
+ def test_assess_compliance_non_nist_framework(self):
129
+ """Test assessment with non-NIST framework returns empty results."""
130
+ mapper = IAMControlMapper(framework="ISO27001")
131
+ iam_data = {"users": []}
132
+ results = mapper.assess_iam_compliance(iam_data)
133
+ assert results == {}
134
+
135
+
136
+ class TestAssessAC2:
137
+ """Test _assess_ac2 (Account Management) compliance."""
138
+
139
+ def test_ac2_pass_all_requirements_met(self):
140
+ """Test AC-2 passes when all requirements met."""
141
+ mapper = IAMControlMapper()
142
+ iam_data = {
143
+ "users": [{"UserName": "user1", "MfaEnabled": True}],
144
+ "account_summary": {
145
+ "AccountMFAEnabled": True,
146
+ "AccountAccessKeysPresent": 0,
147
+ },
148
+ }
149
+ result = mapper._assess_ac2(iam_data)
150
+ assert result == "PASS"
151
+
152
+ def test_ac2_fail_user_without_mfa(self):
153
+ """Test AC-2 fails when user doesn't have MFA."""
154
+ mapper = IAMControlMapper()
155
+ iam_data = {
156
+ "users": [{"UserName": "user1", "MfaEnabled": False}],
157
+ "account_summary": {
158
+ "AccountMFAEnabled": True,
159
+ "AccountAccessKeysPresent": 0,
160
+ },
161
+ }
162
+ result = mapper._assess_ac2(iam_data)
163
+ assert result == "FAIL"
164
+
165
+ def test_ac2_fail_root_without_mfa(self):
166
+ """Test AC-2 fails when root account doesn't have MFA."""
167
+ mapper = IAMControlMapper()
168
+ iam_data = {
169
+ "users": [{"UserName": "user1", "MfaEnabled": True}],
170
+ "account_summary": {
171
+ "AccountMFAEnabled": False,
172
+ "AccountAccessKeysPresent": 0,
173
+ },
174
+ }
175
+ result = mapper._assess_ac2(iam_data)
176
+ assert result == "FAIL"
177
+
178
+ def test_ac2_fail_root_has_access_keys(self):
179
+ """Test AC-2 fails when root account has access keys."""
180
+ mapper = IAMControlMapper()
181
+ iam_data = {
182
+ "users": [{"UserName": "user1", "MfaEnabled": True}],
183
+ "account_summary": {
184
+ "AccountMFAEnabled": True,
185
+ "AccountAccessKeysPresent": 2,
186
+ },
187
+ }
188
+ result = mapper._assess_ac2(iam_data)
189
+ assert result == "FAIL"
190
+
191
+ def test_ac2_pass_multiple_users_with_mfa(self):
192
+ """Test AC-2 passes with multiple users all having MFA."""
193
+ mapper = IAMControlMapper()
194
+ iam_data = {
195
+ "users": [
196
+ {"UserName": "user1", "MfaEnabled": True},
197
+ {"UserName": "user2", "MfaEnabled": True},
198
+ ],
199
+ "account_summary": {
200
+ "AccountMFAEnabled": True,
201
+ "AccountAccessKeysPresent": 0,
202
+ },
203
+ }
204
+ result = mapper._assess_ac2(iam_data)
205
+ assert result == "PASS"
206
+
207
+
208
+ class TestAssessAC6:
209
+ """Test _assess_ac6 (Least Privilege) compliance."""
210
+
211
+ def test_ac6_pass_no_admin_users(self):
212
+ """Test AC-6 passes when no users have admin access."""
213
+ mapper = IAMControlMapper()
214
+ iam_data = {
215
+ "users": [
216
+ {
217
+ "UserName": "user1",
218
+ "AttachedPolicies": [{"PolicyName": "ReadOnlyAccess"}],
219
+ "InlinePolicies": [],
220
+ }
221
+ ]
222
+ }
223
+ result = mapper._assess_ac6(iam_data)
224
+ assert result == "PASS"
225
+
226
+ def test_ac6_fail_user_with_administrator_access(self):
227
+ """Test AC-6 fails when user has AdministratorAccess policy."""
228
+ mapper = IAMControlMapper()
229
+ iam_data = {
230
+ "users": [
231
+ {
232
+ "UserName": "admin-user",
233
+ "AttachedPolicies": [{"PolicyName": "AdministratorAccess"}],
234
+ "InlinePolicies": [],
235
+ }
236
+ ]
237
+ }
238
+ result = mapper._assess_ac6(iam_data)
239
+ assert result == "FAIL"
240
+
241
+ def test_ac6_fail_inline_policy_full_admin(self):
242
+ """Test AC-6 fails when user has inline policy with full admin permissions."""
243
+ mapper = IAMControlMapper()
244
+ iam_data = {
245
+ "users": [
246
+ {
247
+ "UserName": "user1",
248
+ "AttachedPolicies": [],
249
+ "InlinePolicies": [
250
+ {
251
+ "PolicyName": "custom-admin",
252
+ "PolicyDocument": {
253
+ "Statement": [
254
+ {
255
+ "Effect": "Allow",
256
+ "Action": "*",
257
+ "Resource": "*",
258
+ }
259
+ ]
260
+ },
261
+ }
262
+ ],
263
+ }
264
+ ]
265
+ }
266
+ result = mapper._assess_ac6(iam_data)
267
+ assert result == "FAIL"
268
+
269
+ def test_ac6_pass_inline_policy_restricted(self):
270
+ """Test AC-6 passes with restricted inline policies."""
271
+ mapper = IAMControlMapper()
272
+ iam_data = {
273
+ "users": [
274
+ {
275
+ "UserName": "user1",
276
+ "AttachedPolicies": [],
277
+ "InlinePolicies": [
278
+ {
279
+ "PolicyName": "s3-read-only",
280
+ "PolicyDocument": {
281
+ "Statement": [
282
+ {
283
+ "Effect": "Allow",
284
+ "Action": "s3:GetObject",
285
+ "Resource": "arn:aws:s3:::bucket/*",
286
+ }
287
+ ]
288
+ },
289
+ }
290
+ ],
291
+ }
292
+ ]
293
+ }
294
+ result = mapper._assess_ac6(iam_data)
295
+ assert result == "PASS"
296
+
297
+
298
+ class TestAssessIA2:
299
+ """Test _assess_ia2 (Identification and Authentication) compliance."""
300
+
301
+ def test_ia2_pass_strong_policy_and_mfa(self):
302
+ """Test IA-2 passes with strong password policy and MFA."""
303
+ mapper = IAMControlMapper()
304
+ iam_data = {
305
+ "users": [{"UserName": "user1", "MfaEnabled": True}],
306
+ "password_policy": {
307
+ "MinimumPasswordLength": 14,
308
+ "RequireSymbols": True,
309
+ "RequireNumbers": True,
310
+ "RequireUppercaseCharacters": True,
311
+ "RequireLowercaseCharacters": True,
312
+ "MaxPasswordAge": 90,
313
+ },
314
+ }
315
+ result = mapper._assess_ia2(iam_data)
316
+ assert result == "PASS"
317
+
318
+ def test_ia2_fail_weak_password_policy(self):
319
+ """Test IA-2 fails with weak password policy."""
320
+ mapper = IAMControlMapper()
321
+ iam_data = {
322
+ "users": [{"UserName": "user1", "MfaEnabled": True}],
323
+ "password_policy": {
324
+ "MinimumPasswordLength": 8,
325
+ "RequireSymbols": False,
326
+ "RequireNumbers": False,
327
+ "RequireUppercaseCharacters": False,
328
+ "RequireLowercaseCharacters": False,
329
+ "MaxPasswordAge": 180,
330
+ },
331
+ }
332
+ result = mapper._assess_ia2(iam_data)
333
+ assert result == "FAIL"
334
+
335
+ def test_ia2_fail_no_mfa(self):
336
+ """Test IA-2 fails when users don't have MFA."""
337
+ mapper = IAMControlMapper()
338
+ iam_data = {
339
+ "users": [{"UserName": "user1", "MfaEnabled": False}],
340
+ "password_policy": {
341
+ "MinimumPasswordLength": 14,
342
+ "RequireSymbols": True,
343
+ "RequireNumbers": True,
344
+ "RequireUppercaseCharacters": True,
345
+ "RequireLowercaseCharacters": True,
346
+ "MaxPasswordAge": 90,
347
+ },
348
+ }
349
+ result = mapper._assess_ia2(iam_data)
350
+ assert result == "FAIL"
351
+
352
+
353
+ class TestAssessIA5:
354
+ """Test _assess_ia5 (Authenticator Management) compliance."""
355
+
356
+ def test_ia5_pass_all_keys_rotated(self):
357
+ """Test IA-5 passes when all access keys are rotated."""
358
+ mapper = IAMControlMapper()
359
+ iam_data = {
360
+ "users": [
361
+ {
362
+ "UserName": "user1",
363
+ "AccessKeys": [{"AccessKeyId": "key1", "AgeDays": 30}],
364
+ "PasswordLastUsed": {"DaysSinceUsed": 10},
365
+ }
366
+ ]
367
+ }
368
+ result = mapper._assess_ia5(iam_data)
369
+ assert result == "PASS"
370
+
371
+ def test_ia5_fail_old_access_key(self):
372
+ """Test IA-5 fails when access key is too old."""
373
+ mapper = IAMControlMapper()
374
+ iam_data = {
375
+ "users": [
376
+ {
377
+ "UserName": "user1",
378
+ "AccessKeys": [{"AccessKeyId": "key1", "AgeDays": 120}],
379
+ "PasswordLastUsed": {"DaysSinceUsed": 10},
380
+ }
381
+ ]
382
+ }
383
+ result = mapper._assess_ia5(iam_data)
384
+ assert result == "FAIL"
385
+
386
+ def test_ia5_fail_unused_credentials(self):
387
+ """Test IA-5 fails when credentials are unused for too long."""
388
+ mapper = IAMControlMapper()
389
+ iam_data = {
390
+ "users": [
391
+ {
392
+ "UserName": "user1",
393
+ "AccessKeys": [],
394
+ "PasswordLastUsed": {"DaysSinceUsed": 120},
395
+ }
396
+ ]
397
+ }
398
+ result = mapper._assess_ia5(iam_data)
399
+ assert result == "FAIL"
400
+
401
+ def test_ia5_pass_no_access_keys(self):
402
+ """Test IA-5 passes when user has no access keys."""
403
+ mapper = IAMControlMapper()
404
+ iam_data = {
405
+ "users": [
406
+ {
407
+ "UserName": "user1",
408
+ "AccessKeys": [],
409
+ "PasswordLastUsed": {"DaysSinceUsed": 10},
410
+ }
411
+ ]
412
+ }
413
+ result = mapper._assess_ia5(iam_data)
414
+ assert result == "PASS"
415
+
416
+ def test_ia5_pass_no_password_last_used(self):
417
+ """Test IA-5 passes when PasswordLastUsed is None."""
418
+ mapper = IAMControlMapper()
419
+ iam_data = {
420
+ "users": [
421
+ {
422
+ "UserName": "user1",
423
+ "AccessKeys": [{"AccessKeyId": "key1", "AgeDays": 30}],
424
+ "PasswordLastUsed": None,
425
+ }
426
+ ]
427
+ }
428
+ result = mapper._assess_ia5(iam_data)
429
+ assert result == "PASS"
430
+
431
+
432
+ class TestAssessAC3:
433
+ """Test _assess_ac3 (Access Enforcement) compliance."""
434
+
435
+ def test_ac3_pass_restrictive_trust_policies(self):
436
+ """Test AC-3 passes when roles have restrictive trust policies."""
437
+ mapper = IAMControlMapper()
438
+ iam_data = {
439
+ "roles": [
440
+ {
441
+ "RoleName": "test-role",
442
+ "AssumeRolePolicyDocument": {
443
+ "Statement": [
444
+ {
445
+ "Principal": {"Service": "ec2.amazonaws.com"},
446
+ }
447
+ ]
448
+ },
449
+ }
450
+ ]
451
+ }
452
+ result = mapper._assess_ac3(iam_data)
453
+ assert result == "PASS"
454
+
455
+ def test_ac3_fail_wildcard_principal(self):
456
+ """Test AC-3 fails when role has wildcard principal."""
457
+ mapper = IAMControlMapper()
458
+ iam_data = {
459
+ "roles": [
460
+ {
461
+ "RoleName": "public-role",
462
+ "AssumeRolePolicyDocument": {
463
+ "Statement": [
464
+ {
465
+ "Principal": "*",
466
+ }
467
+ ]
468
+ },
469
+ }
470
+ ]
471
+ }
472
+ result = mapper._assess_ac3(iam_data)
473
+ assert result == "FAIL"
474
+
475
+ def test_ac3_fail_wildcard_aws_principal(self):
476
+ """Test AC-3 fails when role has wildcard AWS principal."""
477
+ mapper = IAMControlMapper()
478
+ iam_data = {
479
+ "roles": [
480
+ {
481
+ "RoleName": "overly-permissive-role",
482
+ "AssumeRolePolicyDocument": {
483
+ "Statement": [
484
+ {
485
+ "Principal": {"AWS": "*"},
486
+ }
487
+ ]
488
+ },
489
+ }
490
+ ]
491
+ }
492
+ result = mapper._assess_ac3(iam_data)
493
+ assert result == "FAIL"
494
+
495
+ def test_ac3_pass_empty_roles_list(self):
496
+ """Test AC-3 passes when no roles exist."""
497
+ mapper = IAMControlMapper()
498
+ iam_data = {"roles": []}
499
+ result = mapper._assess_ac3(iam_data)
500
+ assert result == "PASS"
501
+
502
+
503
+ class TestHelperMethods:
504
+ """Test helper methods."""
505
+
506
+ def test_has_full_admin_permissions_true(self):
507
+ """Test detection of full admin permissions."""
508
+ mapper = IAMControlMapper()
509
+ policy_doc = {
510
+ "Statement": [
511
+ {
512
+ "Effect": "Allow",
513
+ "Action": "*",
514
+ "Resource": "*",
515
+ }
516
+ ]
517
+ }
518
+ assert mapper._has_full_admin_permissions(policy_doc) is True
519
+
520
+ def test_has_full_admin_permissions_star_star_true(self):
521
+ """Test detection of *:* admin permissions."""
522
+ mapper = IAMControlMapper()
523
+ policy_doc = {
524
+ "Statement": [
525
+ {
526
+ "Effect": "Allow",
527
+ "Action": "*:*",
528
+ "Resource": "*",
529
+ }
530
+ ]
531
+ }
532
+ assert mapper._has_full_admin_permissions(policy_doc) is True
533
+
534
+ def test_has_full_admin_permissions_false_limited_action(self):
535
+ """Test no false positive for limited action."""
536
+ mapper = IAMControlMapper()
537
+ policy_doc = {
538
+ "Statement": [
539
+ {
540
+ "Effect": "Allow",
541
+ "Action": "s3:GetObject",
542
+ "Resource": "*",
543
+ }
544
+ ]
545
+ }
546
+ assert mapper._has_full_admin_permissions(policy_doc) is False
547
+
548
+ def test_has_full_admin_permissions_false_limited_resource(self):
549
+ """Test no false positive for limited resource."""
550
+ mapper = IAMControlMapper()
551
+ policy_doc = {
552
+ "Statement": [
553
+ {
554
+ "Effect": "Allow",
555
+ "Action": ["s3:*"],
556
+ "Resource": "arn:aws:s3:::bucket/*",
557
+ }
558
+ ]
559
+ }
560
+ assert mapper._has_full_admin_permissions(policy_doc) is False
561
+
562
+ def test_has_permissive_trust_policy_wildcard(self):
563
+ """Test detection of wildcard trust policy."""
564
+ mapper = IAMControlMapper()
565
+ trust_policy = {"Statement": [{"Principal": "*"}]}
566
+ assert mapper._has_permissive_trust_policy(trust_policy) is True
567
+
568
+ def test_has_permissive_trust_policy_aws_wildcard(self):
569
+ """Test detection of AWS wildcard trust policy."""
570
+ mapper = IAMControlMapper()
571
+ trust_policy = {"Statement": [{"Principal": {"AWS": "*"}}]}
572
+ assert mapper._has_permissive_trust_policy(trust_policy) is True
573
+
574
+ def test_has_permissive_trust_policy_false(self):
575
+ """Test no false positive for restricted trust policy."""
576
+ mapper = IAMControlMapper()
577
+ trust_policy = {"Statement": [{"Principal": {"Service": "ec2.amazonaws.com"}}]}
578
+ assert mapper._has_permissive_trust_policy(trust_policy) is False
579
+
580
+ def test_is_strong_password_policy_true(self):
581
+ """Test strong password policy detection."""
582
+ mapper = IAMControlMapper()
583
+ policy = {
584
+ "MinimumPasswordLength": 14,
585
+ "RequireSymbols": True,
586
+ "RequireNumbers": True,
587
+ "RequireUppercaseCharacters": True,
588
+ "RequireLowercaseCharacters": True,
589
+ "MaxPasswordAge": 90,
590
+ }
591
+ assert mapper._is_strong_password_policy(policy) is True
592
+
593
+ def test_is_strong_password_policy_false_short_length(self):
594
+ """Test weak password policy with short length."""
595
+ mapper = IAMControlMapper()
596
+ policy = {
597
+ "MinimumPasswordLength": 8,
598
+ "RequireSymbols": True,
599
+ "RequireNumbers": True,
600
+ "RequireUppercaseCharacters": True,
601
+ "RequireLowercaseCharacters": True,
602
+ "MaxPasswordAge": 90,
603
+ }
604
+ assert mapper._is_strong_password_policy(policy) is False
605
+
606
+ def test_is_strong_password_policy_false_no_complexity(self):
607
+ """Test weak password policy without complexity requirements."""
608
+ mapper = IAMControlMapper()
609
+ policy = {
610
+ "MinimumPasswordLength": 14,
611
+ "RequireSymbols": False,
612
+ "RequireNumbers": False,
613
+ "RequireUppercaseCharacters": False,
614
+ "RequireLowercaseCharacters": False,
615
+ "MaxPasswordAge": 90,
616
+ }
617
+ assert mapper._is_strong_password_policy(policy) is False
618
+
619
+ def test_is_strong_password_policy_false_long_max_age(self):
620
+ """Test weak password policy with long max age."""
621
+ mapper = IAMControlMapper()
622
+ policy = {
623
+ "MinimumPasswordLength": 14,
624
+ "RequireSymbols": True,
625
+ "RequireNumbers": True,
626
+ "RequireUppercaseCharacters": True,
627
+ "RequireLowercaseCharacters": True,
628
+ "MaxPasswordAge": 180,
629
+ }
630
+ assert mapper._is_strong_password_policy(policy) is False
631
+
632
+ def test_is_strong_password_policy_false_empty(self):
633
+ """Test empty password policy is not strong."""
634
+ mapper = IAMControlMapper()
635
+ assert mapper._is_strong_password_policy({}) is False
636
+
637
+ def test_is_strong_password_policy_false_none(self):
638
+ """Test None password policy is not strong."""
639
+ mapper = IAMControlMapper()
640
+ assert mapper._is_strong_password_policy(None) is False
641
+
642
+
643
+ class TestIAMControlMappings:
644
+ """Test IAM_CONTROL_MAPPINGS structure."""
645
+
646
+ def test_mappings_exist(self):
647
+ """Test that mappings dictionary exists and has content."""
648
+ assert len(IAM_CONTROL_MAPPINGS) > 0
649
+
650
+ def test_ac2_mapping_structure(self):
651
+ """Test AC-2 mapping structure."""
652
+ assert "AC-2" in IAM_CONTROL_MAPPINGS
653
+ ac2 = IAM_CONTROL_MAPPINGS["AC-2"]
654
+ assert "name" in ac2
655
+ assert "description" in ac2
656
+ assert "checks" in ac2
657
+ assert "user_mfa" in ac2["checks"]
658
+ assert "inactive_users" in ac2["checks"]
659
+ assert "root_account_usage" in ac2["checks"]
660
+
661
+ def test_ac6_mapping_structure(self):
662
+ """Test AC-6 mapping structure."""
663
+ assert "AC-6" in IAM_CONTROL_MAPPINGS
664
+ ac6 = IAM_CONTROL_MAPPINGS["AC-6"]
665
+ assert len(ac6["checks"]) >= 2
666
+ assert "admin_policies" in ac6["checks"]
667
+ assert "inline_policies" in ac6["checks"]
668
+
669
+ def test_all_controls_have_required_fields(self):
670
+ """Test all control mappings have required fields."""
671
+ for control_id, control_data in IAM_CONTROL_MAPPINGS.items():
672
+ assert "name" in control_data
673
+ assert "description" in control_data
674
+ assert "checks" in control_data
675
+ assert isinstance(control_data["checks"], dict)
676
+ for check_name, check_data in control_data["checks"].items():
677
+ assert "weight" in check_data
678
+ assert "pass_criteria" in check_data
679
+ assert "fail_criteria" in check_data
680
+
681
+
682
+ class TestISO27001Mappings:
683
+ """Test ISO_27001_MAPPINGS structure."""
684
+
685
+ def test_iso_mappings_exist(self):
686
+ """Test that ISO mappings exist."""
687
+ assert len(ISO_27001_MAPPINGS) > 0
688
+
689
+ def test_iso_mapping_structure(self):
690
+ """Test ISO mapping structure."""
691
+ for control_id, control_data in ISO_27001_MAPPINGS.items():
692
+ assert "name" in control_data
693
+ assert "iam_attributes" in control_data
694
+ assert isinstance(control_data["iam_attributes"], list)
695
+
696
+
697
+ class TestConstants:
698
+ """Test constant values."""
699
+
700
+ def test_strong_password_policy_constant(self):
701
+ """Test STRONG_PASSWORD_POLICY constant."""
702
+ assert STRONG_PASSWORD_POLICY["MinimumPasswordLength"] == 14
703
+ assert STRONG_PASSWORD_POLICY["RequireSymbols"] is True
704
+ assert STRONG_PASSWORD_POLICY["RequireNumbers"] is True
705
+ assert STRONG_PASSWORD_POLICY["RequireUppercaseCharacters"] is True
706
+ assert STRONG_PASSWORD_POLICY["RequireLowercaseCharacters"] is True
707
+ assert STRONG_PASSWORD_POLICY["MaxPasswordAge"] == 90
708
+ assert STRONG_PASSWORD_POLICY["PasswordReusePrevention"] == 24
709
+
710
+ def test_age_threshold_constants(self):
711
+ """Test age threshold constants."""
712
+ assert ACCESS_KEY_MAX_AGE_DAYS == 90
713
+ assert PASSWORD_MAX_AGE_DAYS == 90
714
+ assert CREDENTIAL_UNUSED_DAYS == 90
715
+
716
+
717
+ if __name__ == "__main__":
718
+ pytest.main([__file__, "-v"])