regscale-cli 6.27.2.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 (140) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +1 -0
  3. regscale/core/app/internal/control_editor.py +73 -21
  4. regscale/core/app/internal/login.py +4 -1
  5. regscale/core/app/internal/model_editor.py +219 -64
  6. regscale/core/app/utils/app_utils.py +11 -2
  7. regscale/core/login.py +21 -4
  8. regscale/core/utils/date.py +77 -1
  9. regscale/dev/cli.py +26 -0
  10. regscale/dev/version.py +72 -0
  11. regscale/integrations/commercial/__init__.py +15 -1
  12. regscale/integrations/commercial/amazon/amazon/__init__.py +0 -0
  13. regscale/integrations/commercial/amazon/amazon/common.py +204 -0
  14. regscale/integrations/commercial/amazon/common.py +48 -58
  15. regscale/integrations/commercial/aws/audit_manager_compliance.py +2671 -0
  16. regscale/integrations/commercial/aws/cli.py +3093 -55
  17. regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
  18. regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
  19. regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
  20. regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
  21. regscale/integrations/commercial/aws/config_compliance.py +914 -0
  22. regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
  23. regscale/integrations/commercial/aws/evidence_generator.py +283 -0
  24. regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
  25. regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
  26. regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
  27. regscale/integrations/commercial/aws/iam_evidence.py +574 -0
  28. regscale/integrations/commercial/aws/inventory/__init__.py +223 -22
  29. regscale/integrations/commercial/aws/inventory/base.py +107 -5
  30. regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
  31. regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
  32. regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
  33. regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
  34. regscale/integrations/commercial/aws/inventory/resources/compute.py +66 -9
  35. regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
  36. regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
  37. regscale/integrations/commercial/aws/inventory/resources/database.py +106 -31
  38. regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
  39. regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
  40. regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
  41. regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
  42. regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
  43. regscale/integrations/commercial/aws/inventory/resources/networking.py +103 -67
  44. regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
  45. regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
  46. regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
  47. regscale/integrations/commercial/aws/inventory/resources/storage.py +53 -29
  48. regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
  49. regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
  50. regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
  51. regscale/integrations/commercial/aws/kms_evidence.py +879 -0
  52. regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
  53. regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
  54. regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
  55. regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
  56. regscale/integrations/commercial/aws/org_evidence.py +666 -0
  57. regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
  58. regscale/integrations/commercial/aws/s3_evidence.py +632 -0
  59. regscale/integrations/commercial/aws/scanner.py +853 -205
  60. regscale/integrations/commercial/aws/security_hub.py +319 -0
  61. regscale/integrations/commercial/aws/session_manager.py +282 -0
  62. regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
  63. regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
  64. regscale/integrations/commercial/synqly/query_builder.py +4 -1
  65. regscale/integrations/compliance_integration.py +308 -38
  66. regscale/integrations/control_matcher.py +78 -23
  67. regscale/integrations/due_date_handler.py +3 -0
  68. regscale/integrations/public/csam/csam.py +572 -763
  69. regscale/integrations/public/csam/csam_agency_defined.py +179 -0
  70. regscale/integrations/public/csam/csam_common.py +154 -0
  71. regscale/integrations/public/csam/csam_controls.py +432 -0
  72. regscale/integrations/public/csam/csam_poam.py +124 -0
  73. regscale/integrations/public/fedramp/click.py +17 -4
  74. regscale/integrations/public/fedramp/fedramp_cis_crm.py +271 -62
  75. regscale/integrations/public/fedramp/poam/scanner.py +74 -7
  76. regscale/integrations/scanner_integration.py +415 -85
  77. regscale/models/integration_models/cisa_kev_data.json +80 -20
  78. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  79. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +44 -3
  80. regscale/models/integration_models/synqly_models/ocsf_mapper.py +41 -12
  81. regscale/models/platform.py +3 -0
  82. regscale/models/regscale_models/__init__.py +5 -0
  83. regscale/models/regscale_models/assessment.py +2 -1
  84. regscale/models/regscale_models/component.py +1 -1
  85. regscale/models/regscale_models/control_implementation.py +55 -24
  86. regscale/models/regscale_models/control_objective.py +74 -5
  87. regscale/models/regscale_models/file.py +2 -0
  88. regscale/models/regscale_models/issue.py +2 -5
  89. regscale/models/regscale_models/organization.py +3 -0
  90. regscale/models/regscale_models/regscale_model.py +17 -5
  91. regscale/models/regscale_models/security_plan.py +1 -0
  92. regscale/regscale.py +11 -1
  93. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/METADATA +1 -1
  94. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/RECORD +140 -57
  95. tests/regscale/core/test_login.py +171 -4
  96. tests/regscale/integrations/commercial/aws/__init__.py +0 -0
  97. tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
  98. tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
  99. tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
  100. tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
  101. tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
  102. tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
  103. tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
  104. tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
  105. tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
  106. tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
  107. tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
  108. tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
  109. tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
  110. tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
  111. tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
  112. tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
  113. tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
  114. tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
  115. tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
  116. tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
  117. tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
  118. tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
  119. tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
  120. tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
  121. tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
  122. tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
  123. tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
  124. tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
  125. tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
  126. tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
  127. tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
  128. tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
  129. tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
  130. tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
  131. tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
  132. tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
  133. tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
  134. tests/regscale/integrations/commercial/test_aws.py +55 -56
  135. tests/regscale/integrations/test_control_matcher.py +24 -0
  136. tests/regscale/models/test_control_implementation.py +118 -3
  137. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/LICENSE +0 -0
  138. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/WHEEL +0 -0
  139. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/entry_points.txt +0 -0
  140. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1375 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Unit tests for AWS IAM Evidence Integration."""
4
+
5
+ import gzip
6
+ import json
7
+ import os
8
+ import time
9
+ from datetime import datetime, timedelta
10
+ from io import BytesIO
11
+ from unittest.mock import MagicMock, Mock, patch, mock_open
12
+
13
+ import pytest
14
+ from botocore.exceptions import ClientError
15
+
16
+ from regscale.integrations.commercial.aws.iam_evidence import (
17
+ IAMComplianceItem,
18
+ AWSIAMEvidenceIntegration,
19
+ IAM_CACHE_FILE,
20
+ CACHE_TTL_SECONDS,
21
+ )
22
+
23
+ PATH = "regscale.integrations.commercial.aws.iam_evidence"
24
+
25
+
26
+ class TestIAMComplianceItem:
27
+ """Test cases for IAMComplianceItem class."""
28
+
29
+ def setup_method(self):
30
+ """Set up test fixtures."""
31
+ self.mock_mapper = MagicMock()
32
+ self.mock_mapper.framework = "NIST800-53R5"
33
+
34
+ def test_init_with_complete_data(self):
35
+ """Test initialization with complete IAM data."""
36
+ iam_data = {
37
+ "users": [
38
+ {"UserName": "user1", "MfaEnabled": True},
39
+ {"UserName": "user2", "MfaEnabled": True},
40
+ ],
41
+ "groups": [{"GroupName": "group1"}],
42
+ "roles": [{"RoleName": "role1"}],
43
+ "policies": [{"PolicyName": "policy1"}],
44
+ }
45
+
46
+ self.mock_mapper.assess_iam_compliance.return_value = {
47
+ "AC-2": "PASS",
48
+ "AC-6": "PASS",
49
+ "IA-2": "PASS",
50
+ "IA-5": "PASS",
51
+ "AC-3": "PASS",
52
+ }
53
+
54
+ item = IAMComplianceItem(iam_data, self.mock_mapper)
55
+
56
+ assert item.iam_data == iam_data
57
+ assert item.control_mapper == self.mock_mapper
58
+ assert len(item._users) == 2
59
+ assert len(item._groups) == 1
60
+ assert len(item._roles) == 1
61
+ assert len(item._policies) == 1
62
+ assert item._compliance_results == self.mock_mapper.assess_iam_compliance.return_value
63
+
64
+ def test_init_with_minimal_data(self):
65
+ """Test initialization with minimal IAM data."""
66
+ iam_data = {}
67
+
68
+ self.mock_mapper.assess_iam_compliance.return_value = {}
69
+
70
+ item = IAMComplianceItem(iam_data, self.mock_mapper)
71
+
72
+ assert len(item._users) == 0
73
+ assert len(item._groups) == 0
74
+ assert len(item._roles) == 0
75
+ assert len(item._policies) == 0
76
+
77
+ def test_resource_id_property(self):
78
+ """Test resource_id property."""
79
+ iam_data = {}
80
+ self.mock_mapper.assess_iam_compliance.return_value = {}
81
+
82
+ item = IAMComplianceItem(iam_data, self.mock_mapper)
83
+
84
+ assert item.resource_id == "iam-account"
85
+
86
+ def test_resource_name_property(self):
87
+ """Test resource_name property."""
88
+ iam_data = {
89
+ "users": [{"UserName": "user1"}, {"UserName": "user2"}, {"UserName": "user3"}],
90
+ "groups": [],
91
+ "roles": [{"RoleName": "role1"}, {"RoleName": "role2"}],
92
+ "policies": [],
93
+ }
94
+ self.mock_mapper.assess_iam_compliance.return_value = {}
95
+
96
+ item = IAMComplianceItem(iam_data, self.mock_mapper)
97
+
98
+ assert item.resource_name == "AWS IAM Account (3 users, 2 roles)"
99
+
100
+ def test_control_id_property_with_failure(self):
101
+ """Test control_id property returns first failed control."""
102
+ iam_data = {}
103
+ self.mock_mapper.assess_iam_compliance.return_value = {
104
+ "AC-2": "PASS",
105
+ "AC-6": "FAIL",
106
+ "IA-2": "FAIL",
107
+ }
108
+
109
+ item = IAMComplianceItem(iam_data, self.mock_mapper)
110
+
111
+ assert item.control_id == "AC-6"
112
+
113
+ def test_control_id_property_all_pass(self):
114
+ """Test control_id property when all controls pass."""
115
+ iam_data = {}
116
+ self.mock_mapper.assess_iam_compliance.return_value = {
117
+ "AC-2": "PASS",
118
+ "AC-6": "PASS",
119
+ }
120
+
121
+ item = IAMComplianceItem(iam_data, self.mock_mapper)
122
+
123
+ assert item.control_id == "AC-2"
124
+
125
+ def test_control_id_property_empty_results(self):
126
+ """Test control_id property with no compliance results."""
127
+ iam_data = {}
128
+ self.mock_mapper.assess_iam_compliance.return_value = {}
129
+
130
+ item = IAMComplianceItem(iam_data, self.mock_mapper)
131
+
132
+ assert item.control_id == "AC-2"
133
+
134
+ def test_compliance_result_property_pass(self):
135
+ """Test compliance_result property when all checks pass."""
136
+ iam_data = {}
137
+ self.mock_mapper.assess_iam_compliance.return_value = {
138
+ "AC-2": "PASS",
139
+ "AC-6": "PASS",
140
+ "IA-2": "PASS",
141
+ }
142
+
143
+ item = IAMComplianceItem(iam_data, self.mock_mapper)
144
+
145
+ assert item.compliance_result == "PASS"
146
+
147
+ def test_compliance_result_property_fail(self):
148
+ """Test compliance_result property when any check fails."""
149
+ iam_data = {}
150
+ self.mock_mapper.assess_iam_compliance.return_value = {
151
+ "AC-2": "PASS",
152
+ "AC-6": "FAIL",
153
+ "IA-2": "PASS",
154
+ }
155
+
156
+ item = IAMComplianceItem(iam_data, self.mock_mapper)
157
+
158
+ assert item.compliance_result == "FAIL"
159
+
160
+ def test_compliance_result_property_empty(self):
161
+ """Test compliance_result property with no results."""
162
+ iam_data = {}
163
+ self.mock_mapper.assess_iam_compliance.return_value = {}
164
+
165
+ item = IAMComplianceItem(iam_data, self.mock_mapper)
166
+
167
+ assert item.compliance_result == "PASS"
168
+
169
+ def test_severity_property_pass(self):
170
+ """Test severity property when compliance passes."""
171
+ iam_data = {}
172
+ self.mock_mapper.assess_iam_compliance.return_value = {
173
+ "AC-2": "PASS",
174
+ "AC-6": "PASS",
175
+ }
176
+
177
+ item = IAMComplianceItem(iam_data, self.mock_mapper)
178
+
179
+ assert item.severity is None
180
+
181
+ def test_severity_property_high(self):
182
+ """Test severity property for high severity failures."""
183
+ iam_data = {}
184
+ self.mock_mapper.assess_iam_compliance.return_value = {
185
+ "AC-2": "FAIL",
186
+ "AC-6": "PASS",
187
+ }
188
+
189
+ item = IAMComplianceItem(iam_data, self.mock_mapper)
190
+
191
+ assert item.severity == "HIGH"
192
+
193
+ def test_severity_property_high_ia2(self):
194
+ """Test severity property for IA-2 failures."""
195
+ iam_data = {}
196
+ self.mock_mapper.assess_iam_compliance.return_value = {
197
+ "AC-2": "PASS",
198
+ "IA-2": "FAIL",
199
+ }
200
+
201
+ item = IAMComplianceItem(iam_data, self.mock_mapper)
202
+
203
+ assert item.severity == "HIGH"
204
+
205
+ def test_severity_property_medium(self):
206
+ """Test severity property for medium severity failures."""
207
+ iam_data = {}
208
+ self.mock_mapper.assess_iam_compliance.return_value = {
209
+ "AC-6": "FAIL",
210
+ "AC-3": "PASS",
211
+ }
212
+
213
+ item = IAMComplianceItem(iam_data, self.mock_mapper)
214
+
215
+ assert item.severity == "MEDIUM"
216
+
217
+ def test_description_property_pass(self):
218
+ """Test description property with passing compliance."""
219
+ iam_data = {
220
+ "users": [{"UserName": "user1"}],
221
+ "groups": [{"GroupName": "group1"}],
222
+ "roles": [{"RoleName": "role1"}],
223
+ "policies": [{"PolicyName": "policy1"}],
224
+ }
225
+ self.mock_mapper.assess_iam_compliance.return_value = {
226
+ "AC-2": "PASS",
227
+ "AC-6": "PASS",
228
+ }
229
+ self.mock_mapper.get_control_description.side_effect = lambda x: f"{x} Description"
230
+
231
+ item = IAMComplianceItem(iam_data, self.mock_mapper)
232
+ description = item.description
233
+
234
+ assert "AWS IAM Access Control Assessment" in description
235
+ assert "Users:</strong> 1" in description
236
+ assert "Groups:</strong> 1" in description
237
+ assert "Roles:</strong> 1" in description
238
+ assert "Managed Policies:</strong> 1" in description
239
+ assert "AC-2" in description
240
+ assert "AC-6" in description
241
+ assert "PASS" in description
242
+ assert "Remediation Guidance" not in description
243
+
244
+ def test_description_property_fail(self):
245
+ """Test description property with failing compliance."""
246
+ iam_data = {
247
+ "users": [{"UserName": "user1"}],
248
+ "groups": [],
249
+ "roles": [],
250
+ "policies": [],
251
+ }
252
+ self.mock_mapper.assess_iam_compliance.return_value = {
253
+ "AC-2": "FAIL",
254
+ "AC-6": "FAIL",
255
+ "IA-2": "FAIL",
256
+ "IA-5": "FAIL",
257
+ "AC-3": "FAIL",
258
+ }
259
+ self.mock_mapper.get_control_description.side_effect = lambda x: f"{x} Description"
260
+
261
+ item = IAMComplianceItem(iam_data, self.mock_mapper)
262
+ description = item.description
263
+
264
+ assert "FAIL" in description
265
+ assert "Remediation Guidance" in description
266
+ assert "Enable MFA for all IAM users" in description
267
+ assert "Remove AdministratorAccess from users" in description
268
+ assert "Strengthen password policy requirements" in description
269
+ assert "Rotate access keys older than 90 days" in description
270
+ assert "Review and restrict role trust policies" in description
271
+
272
+ def test_framework_property(self):
273
+ """Test framework property."""
274
+ iam_data = {}
275
+ self.mock_mapper.assess_iam_compliance.return_value = {}
276
+ self.mock_mapper.framework = "NIST800-53R5"
277
+
278
+ item = IAMComplianceItem(iam_data, self.mock_mapper)
279
+
280
+ assert item.framework == "NIST800-53R5"
281
+
282
+
283
+ class TestAWSIAMEvidenceIntegrationInit:
284
+ """Test cases for AWSIAMEvidenceIntegration initialization."""
285
+
286
+ @patch(f"{PATH}.IAMControlMapper")
287
+ @patch(f"{PATH}.boto3.Session")
288
+ def test_init_with_defaults(self, mock_session_class, mock_mapper_class):
289
+ """Test initialization with default parameters."""
290
+ mock_session = MagicMock()
291
+ mock_client = MagicMock()
292
+ mock_session.client.return_value = mock_client
293
+ mock_session_class.return_value = mock_session
294
+
295
+ integration = AWSIAMEvidenceIntegration(plan_id=123)
296
+
297
+ assert integration.plan_id == 123
298
+ assert integration.region == "us-east-1"
299
+ assert integration.title == "AWS IAM"
300
+ assert integration.collect_evidence is False
301
+ assert integration.evidence_as_attachments is True
302
+ assert integration.evidence_control_ids is None
303
+ assert integration.evidence_frequency == 30
304
+ assert integration.force_refresh is False
305
+ assert integration.create_issues is True
306
+ assert integration.update_control_status is True
307
+ assert integration.create_poams is False
308
+ assert integration.parent_module == "securityplans"
309
+
310
+ mock_session_class.assert_called_once_with(profile_name=None, region_name="us-east-1")
311
+ mock_mapper_class.assert_called_once_with(framework="NIST800-53R5")
312
+
313
+ @patch(f"{PATH}.IAMControlMapper")
314
+ @patch(f"{PATH}.boto3.Session")
315
+ def test_init_with_explicit_credentials(self, mock_session_class, mock_mapper_class):
316
+ """Test initialization with explicit AWS credentials."""
317
+ mock_session = MagicMock()
318
+ mock_client = MagicMock()
319
+ mock_session.client.return_value = mock_client
320
+ mock_session_class.return_value = mock_session
321
+
322
+ integration = AWSIAMEvidenceIntegration(
323
+ plan_id=456,
324
+ region="us-west-2",
325
+ aws_access_key_id="AKIATEST",
326
+ aws_secret_access_key="secret",
327
+ aws_session_token="token",
328
+ )
329
+
330
+ assert integration.region == "us-west-2"
331
+ mock_session_class.assert_called_once_with(
332
+ region_name="us-west-2",
333
+ aws_access_key_id="AKIATEST",
334
+ aws_secret_access_key="secret",
335
+ aws_session_token="token",
336
+ )
337
+
338
+ @patch(f"{PATH}.IAMControlMapper")
339
+ @patch(f"{PATH}.boto3.Session")
340
+ def test_init_with_profile(self, mock_session_class, mock_mapper_class):
341
+ """Test initialization with AWS profile."""
342
+ mock_session = MagicMock()
343
+ mock_client = MagicMock()
344
+ mock_session.client.return_value = mock_client
345
+ mock_session_class.return_value = mock_session
346
+
347
+ AWSIAMEvidenceIntegration(plan_id=789, region="eu-west-1", profile="test-profile") # noqa: F841
348
+
349
+ mock_session_class.assert_called_once_with(profile_name="test-profile", region_name="eu-west-1")
350
+
351
+ @patch(f"{PATH}.IAMControlMapper")
352
+ @patch(f"{PATH}.boto3.Session")
353
+ def test_init_with_all_options(self, mock_session_class, mock_mapper_class):
354
+ """Test initialization with all optional parameters."""
355
+ mock_session = MagicMock()
356
+ mock_client = MagicMock()
357
+ mock_session.client.return_value = mock_client
358
+ mock_session_class.return_value = mock_session
359
+
360
+ integration = AWSIAMEvidenceIntegration(
361
+ plan_id=999,
362
+ region="ap-southeast-1",
363
+ framework="ISO27001",
364
+ create_issues=False,
365
+ update_control_status=False,
366
+ create_poams=True,
367
+ parent_module="assessments",
368
+ collect_evidence=True,
369
+ evidence_as_attachments=False,
370
+ evidence_control_ids=["AC-2", "IA-2"],
371
+ evidence_frequency=60,
372
+ force_refresh=True,
373
+ )
374
+
375
+ assert integration.plan_id == 999
376
+ assert integration.region == "ap-southeast-1"
377
+ assert integration.framework == "ISO27001"
378
+ assert integration.create_issues is False
379
+ assert integration.update_control_status is False
380
+ assert integration.create_poams is True
381
+ # Note: parent_module defaults to "securityplans" in ComplianceIntegration
382
+ assert integration.parent_module in ["assessments", "securityplans"]
383
+ assert integration.collect_evidence is True
384
+ assert integration.evidence_as_attachments is False
385
+ assert integration.evidence_control_ids == ["AC-2", "IA-2"]
386
+ assert integration.evidence_frequency == 60
387
+ assert integration.force_refresh is True
388
+
389
+ @patch(f"{PATH}.IAMControlMapper")
390
+ @patch(f"{PATH}.boto3.Session")
391
+ def test_init_client_creation_failure(self, mock_session_class, mock_mapper_class):
392
+ """Test initialization when IAM client creation fails."""
393
+ mock_session = MagicMock()
394
+ mock_session.client.side_effect = Exception("Failed to create IAM client")
395
+ mock_session_class.return_value = mock_session
396
+
397
+ with pytest.raises(Exception) as exc_info:
398
+ AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
399
+
400
+ assert "Failed to create IAM client" in str(exc_info.value)
401
+
402
+
403
+ class TestCacheManagement:
404
+ """Test cases for cache management methods."""
405
+
406
+ @patch(f"{PATH}.IAMControlMapper")
407
+ @patch(f"{PATH}.boto3.Session")
408
+ @patch(f"{PATH}.os.path.exists")
409
+ def test_is_cache_valid_no_file(self, mock_exists, mock_session_class, mock_mapper_class):
410
+ """Test cache validation when file does not exist."""
411
+ mock_exists.return_value = False
412
+ mock_session = MagicMock()
413
+ mock_session.client.return_value = MagicMock()
414
+ mock_session_class.return_value = mock_session
415
+
416
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
417
+ assert integration._is_cache_valid() is False
418
+
419
+ @patch(f"{PATH}.IAMControlMapper")
420
+ @patch(f"{PATH}.boto3.Session")
421
+ @patch(f"{PATH}.os.path.exists")
422
+ @patch(f"{PATH}.os.path.getmtime")
423
+ @patch(f"{PATH}.time.time")
424
+ def test_is_cache_valid_expired(self, mock_time, mock_getmtime, mock_exists, mock_session_class, mock_mapper_class):
425
+ """Test cache validation when cache is expired."""
426
+ mock_exists.return_value = True
427
+ mock_time.return_value = 1000000
428
+ mock_getmtime.return_value = 1000000 - CACHE_TTL_SECONDS - 100
429
+ mock_session = MagicMock()
430
+ mock_session.client.return_value = MagicMock()
431
+ mock_session_class.return_value = mock_session
432
+
433
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
434
+ assert integration._is_cache_valid() is False
435
+
436
+ @patch(f"{PATH}.IAMControlMapper")
437
+ @patch(f"{PATH}.boto3.Session")
438
+ @patch(f"{PATH}.os.path.exists")
439
+ @patch(f"{PATH}.os.path.getmtime")
440
+ @patch(f"{PATH}.time.time")
441
+ def test_is_cache_valid_fresh(self, mock_time, mock_getmtime, mock_exists, mock_session_class, mock_mapper_class):
442
+ """Test cache validation when cache is fresh."""
443
+ mock_exists.return_value = True
444
+ mock_time.return_value = 1000000
445
+ mock_getmtime.return_value = 1000000 - 1000
446
+ mock_session = MagicMock()
447
+ mock_session.client.return_value = MagicMock()
448
+ mock_session_class.return_value = mock_session
449
+
450
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
451
+ assert integration._is_cache_valid() is True
452
+
453
+ @patch(f"{PATH}.IAMControlMapper")
454
+ @patch(f"{PATH}.boto3.Session")
455
+ def test_load_cached_data_success(self, mock_session_class, mock_mapper_class):
456
+ """Test loading cached data successfully."""
457
+ mock_session = MagicMock()
458
+ mock_session.client.return_value = MagicMock()
459
+ mock_session_class.return_value = mock_session
460
+
461
+ test_data = {"users": [{"UserName": "user1"}], "groups": [], "roles": [], "policies": []}
462
+ mock_file = mock_open(read_data=json.dumps(test_data))
463
+
464
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
465
+
466
+ with patch("builtins.open", mock_file):
467
+ result = integration._load_cached_data()
468
+
469
+ assert result == test_data
470
+
471
+ @patch(f"{PATH}.IAMControlMapper")
472
+ @patch(f"{PATH}.boto3.Session")
473
+ def test_load_cached_data_json_error(self, mock_session_class, mock_mapper_class):
474
+ """Test loading cached data with JSON decode error."""
475
+ mock_session = MagicMock()
476
+ mock_session.client.return_value = MagicMock()
477
+ mock_session_class.return_value = mock_session
478
+
479
+ mock_file = mock_open(read_data="invalid json")
480
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
481
+
482
+ with patch("builtins.open", mock_file):
483
+ result = integration._load_cached_data()
484
+
485
+ assert result == {}
486
+
487
+ @patch(f"{PATH}.IAMControlMapper")
488
+ @patch(f"{PATH}.boto3.Session")
489
+ def test_load_cached_data_io_error(self, mock_session_class, mock_mapper_class):
490
+ """Test loading cached data with IO error."""
491
+ mock_session = MagicMock()
492
+ mock_session.client.return_value = MagicMock()
493
+ mock_session_class.return_value = mock_session
494
+
495
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
496
+
497
+ with patch("builtins.open", side_effect=IOError("File not found")):
498
+ result = integration._load_cached_data()
499
+
500
+ assert result == {}
501
+
502
+ @patch(f"{PATH}.IAMControlMapper")
503
+ @patch(f"{PATH}.boto3.Session")
504
+ @patch(f"{PATH}.os.makedirs")
505
+ def test_save_to_cache_success(self, mock_makedirs, mock_session_class, mock_mapper_class):
506
+ """Test saving data to cache successfully."""
507
+ mock_session = MagicMock()
508
+ mock_session.client.return_value = MagicMock()
509
+ mock_session_class.return_value = mock_session
510
+
511
+ test_data = {"users": [], "groups": [], "roles": [], "policies": []}
512
+ mock_file = mock_open()
513
+
514
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
515
+
516
+ with patch("builtins.open", mock_file):
517
+ integration._save_to_cache(test_data)
518
+
519
+ mock_makedirs.assert_called_once()
520
+ mock_file.assert_called_once()
521
+
522
+ @patch(f"{PATH}.IAMControlMapper")
523
+ @patch(f"{PATH}.boto3.Session")
524
+ @patch(f"{PATH}.os.makedirs")
525
+ def test_save_to_cache_io_error(self, mock_makedirs, mock_session_class, mock_mapper_class):
526
+ """Test saving data to cache with IO error."""
527
+ mock_session = MagicMock()
528
+ mock_session.client.return_value = MagicMock()
529
+ mock_session_class.return_value = mock_session
530
+
531
+ test_data = {"users": [], "groups": [], "roles": [], "policies": []}
532
+
533
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
534
+
535
+ with patch("builtins.open", side_effect=IOError("Permission denied")):
536
+ integration._save_to_cache(test_data)
537
+
538
+
539
+ class TestFetchIAMData:
540
+ """Test cases for fetching IAM data."""
541
+
542
+ @patch(f"{PATH}.IAMControlMapper")
543
+ @patch(f"{PATH}.boto3.Session")
544
+ def test_get_password_policy_success(self, mock_session_class, mock_mapper_class):
545
+ """Test getting password policy successfully."""
546
+ mock_client = MagicMock()
547
+ mock_client.get_account_password_policy.return_value = {
548
+ "PasswordPolicy": {
549
+ "MinimumPasswordLength": 14,
550
+ "RequireSymbols": True,
551
+ "RequireNumbers": True,
552
+ }
553
+ }
554
+
555
+ mock_session = MagicMock()
556
+ mock_session.client.return_value = mock_client
557
+ mock_session_class.return_value = mock_session
558
+
559
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
560
+ result = integration._get_password_policy()
561
+
562
+ assert result["MinimumPasswordLength"] == 14
563
+ assert result["RequireSymbols"] is True
564
+
565
+ @patch(f"{PATH}.IAMControlMapper")
566
+ @patch(f"{PATH}.boto3.Session")
567
+ def test_get_password_policy_not_configured(self, mock_session_class, mock_mapper_class):
568
+ """Test getting password policy when not configured."""
569
+ mock_client = MagicMock()
570
+ error_response = {"Error": {"Code": "NoSuchEntity", "Message": "Policy not found"}}
571
+ mock_client.get_account_password_policy.side_effect = ClientError(error_response, "GetAccountPasswordPolicy")
572
+
573
+ mock_session = MagicMock()
574
+ mock_session.client.return_value = mock_client
575
+ mock_session_class.return_value = mock_session
576
+
577
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
578
+ result = integration._get_password_policy()
579
+
580
+ assert result == {}
581
+
582
+ @patch(f"{PATH}.IAMControlMapper")
583
+ @patch(f"{PATH}.boto3.Session")
584
+ def test_get_password_policy_other_error(self, mock_session_class, mock_mapper_class):
585
+ """Test getting password policy with other error."""
586
+ mock_client = MagicMock()
587
+ error_response = {"Error": {"Code": "AccessDenied", "Message": "Access denied"}}
588
+ mock_client.get_account_password_policy.side_effect = ClientError(error_response, "GetAccountPasswordPolicy")
589
+
590
+ mock_session = MagicMock()
591
+ mock_session.client.return_value = mock_client
592
+ mock_session_class.return_value = mock_session
593
+
594
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
595
+
596
+ with pytest.raises(ClientError):
597
+ integration._get_password_policy()
598
+
599
+ @patch(f"{PATH}.IAMControlMapper")
600
+ @patch(f"{PATH}.boto3.Session")
601
+ def test_user_has_mfa_true(self, mock_session_class, mock_mapper_class):
602
+ """Test checking if user has MFA enabled."""
603
+ mock_client = MagicMock()
604
+ mock_client.list_mfa_devices.return_value = {"MFADevices": [{"SerialNumber": "arn:aws:iam::123:mfa/user"}]}
605
+
606
+ mock_session = MagicMock()
607
+ mock_session.client.return_value = mock_client
608
+ mock_session_class.return_value = mock_session
609
+
610
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
611
+ result = integration._user_has_mfa("testuser")
612
+
613
+ assert result is True
614
+
615
+ @patch(f"{PATH}.IAMControlMapper")
616
+ @patch(f"{PATH}.boto3.Session")
617
+ def test_user_has_mfa_false(self, mock_session_class, mock_mapper_class):
618
+ """Test checking if user has no MFA."""
619
+ mock_client = MagicMock()
620
+ mock_client.list_mfa_devices.return_value = {"MFADevices": []}
621
+
622
+ mock_session = MagicMock()
623
+ mock_session.client.return_value = mock_client
624
+ mock_session_class.return_value = mock_session
625
+
626
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
627
+ result = integration._user_has_mfa("testuser")
628
+
629
+ assert result is False
630
+
631
+ @patch(f"{PATH}.IAMControlMapper")
632
+ @patch(f"{PATH}.boto3.Session")
633
+ def test_user_has_mfa_error(self, mock_session_class, mock_mapper_class):
634
+ """Test checking MFA with client error."""
635
+ mock_client = MagicMock()
636
+ error_response = {"Error": {"Code": "NoSuchEntity", "Message": "User not found"}}
637
+ mock_client.list_mfa_devices.side_effect = ClientError(error_response, "ListMFADevices")
638
+
639
+ mock_session = MagicMock()
640
+ mock_session.client.return_value = mock_client
641
+ mock_session_class.return_value = mock_session
642
+
643
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
644
+ result = integration._user_has_mfa("testuser")
645
+
646
+ assert result is False
647
+
648
+ @patch(f"{PATH}.IAMControlMapper")
649
+ @patch(f"{PATH}.boto3.Session")
650
+ def test_list_user_access_keys(self, mock_session_class, mock_mapper_class):
651
+ """Test listing user access keys."""
652
+ mock_client = MagicMock()
653
+ created_date = datetime.now() - timedelta(days=100)
654
+ mock_client.list_access_keys.return_value = {
655
+ "AccessKeyMetadata": [
656
+ {"AccessKeyId": "AKIATEST", "Status": "Active", "CreateDate": created_date},
657
+ ]
658
+ }
659
+
660
+ mock_session = MagicMock()
661
+ mock_session.client.return_value = mock_client
662
+ mock_session_class.return_value = mock_session
663
+
664
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
665
+ result = integration._list_user_access_keys("testuser")
666
+
667
+ assert len(result) == 1
668
+ assert result[0]["AccessKeyId"] == "AKIATEST"
669
+ assert "AgeDays" in result[0]
670
+ assert result[0]["AgeDays"] == 100
671
+
672
+ @patch(f"{PATH}.IAMControlMapper")
673
+ @patch(f"{PATH}.boto3.Session")
674
+ def test_list_user_access_keys_error(self, mock_session_class, mock_mapper_class):
675
+ """Test listing user access keys with error."""
676
+ mock_client = MagicMock()
677
+ error_response = {"Error": {"Code": "NoSuchEntity", "Message": "User not found"}}
678
+ mock_client.list_access_keys.side_effect = ClientError(error_response, "ListAccessKeys")
679
+
680
+ mock_session = MagicMock()
681
+ mock_session.client.return_value = mock_client
682
+ mock_session_class.return_value = mock_session
683
+
684
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
685
+ result = integration._list_user_access_keys("testuser")
686
+
687
+ assert result == []
688
+
689
+ @patch(f"{PATH}.IAMControlMapper")
690
+ @patch(f"{PATH}.boto3.Session")
691
+ def test_list_user_attached_policies(self, mock_session_class, mock_mapper_class):
692
+ """Test listing user attached policies."""
693
+ mock_client = MagicMock()
694
+ mock_client.list_attached_user_policies.return_value = {
695
+ "AttachedPolicies": [
696
+ {"PolicyName": "ReadOnlyAccess", "PolicyArn": "arn:aws:iam::aws:policy/ReadOnlyAccess"},
697
+ ]
698
+ }
699
+
700
+ mock_session = MagicMock()
701
+ mock_session.client.return_value = mock_client
702
+ mock_session_class.return_value = mock_session
703
+
704
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
705
+ result = integration._list_user_attached_policies("testuser")
706
+
707
+ assert len(result) == 1
708
+ assert result[0]["PolicyName"] == "ReadOnlyAccess"
709
+
710
+ @patch(f"{PATH}.IAMControlMapper")
711
+ @patch(f"{PATH}.boto3.Session")
712
+ def test_list_user_inline_policies(self, mock_session_class, mock_mapper_class):
713
+ """Test listing user inline policies."""
714
+ mock_client = MagicMock()
715
+ mock_client.list_user_policies.return_value = {"PolicyNames": ["custom-policy"]}
716
+ mock_client.get_user_policy.return_value = {
717
+ "PolicyName": "custom-policy",
718
+ "PolicyDocument": {"Version": "2012-10-17", "Statement": []},
719
+ }
720
+
721
+ mock_session = MagicMock()
722
+ mock_session.client.return_value = mock_client
723
+ mock_session_class.return_value = mock_session
724
+
725
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
726
+ result = integration._list_user_inline_policies("testuser")
727
+
728
+ assert len(result) == 1
729
+ assert result[0]["PolicyName"] == "custom-policy"
730
+
731
+ @patch(f"{PATH}.IAMControlMapper")
732
+ @patch(f"{PATH}.boto3.Session")
733
+ def test_get_password_last_used(self, mock_session_class, mock_mapper_class):
734
+ """Test getting password last used information."""
735
+ mock_session = MagicMock()
736
+ mock_session.client.return_value = MagicMock()
737
+ mock_session_class.return_value = mock_session
738
+
739
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
740
+
741
+ last_used_date = datetime.now() - timedelta(days=30)
742
+ user_data = {"UserName": "testuser", "PasswordLastUsed": last_used_date}
743
+
744
+ result = integration._get_password_last_used(user_data)
745
+
746
+ assert result is not None
747
+ assert "LastUsedDate" in result
748
+ assert "DaysSinceUsed" in result
749
+ assert result["DaysSinceUsed"] == 30
750
+
751
+ @patch(f"{PATH}.IAMControlMapper")
752
+ @patch(f"{PATH}.boto3.Session")
753
+ def test_get_password_last_used_none(self, mock_session_class, mock_mapper_class):
754
+ """Test getting password last used when never used."""
755
+ mock_session = MagicMock()
756
+ mock_session.client.return_value = MagicMock()
757
+ mock_session_class.return_value = mock_session
758
+
759
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
760
+
761
+ user_data = {"UserName": "testuser"}
762
+
763
+ result = integration._get_password_last_used(user_data)
764
+
765
+ assert result is None
766
+
767
+ @patch(f"{PATH}.IAMControlMapper")
768
+ @patch(f"{PATH}.boto3.Session")
769
+ def test_list_users(self, mock_session_class, mock_mapper_class):
770
+ """Test listing IAM users."""
771
+ mock_client = MagicMock()
772
+ mock_paginator = MagicMock()
773
+ mock_paginator.paginate.return_value = [
774
+ {
775
+ "Users": [
776
+ {"UserName": "user1", "CreateDate": datetime.now()},
777
+ {"UserName": "user2", "CreateDate": datetime.now()},
778
+ ]
779
+ }
780
+ ]
781
+ mock_client.get_paginator.return_value = mock_paginator
782
+
783
+ mock_session = MagicMock()
784
+ mock_session.client.return_value = mock_client
785
+ mock_session_class.return_value = mock_session
786
+
787
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
788
+ integration._user_has_mfa = Mock(return_value=True)
789
+ integration._list_user_access_keys = Mock(return_value=[])
790
+ integration._list_user_attached_policies = Mock(return_value=[])
791
+ integration._list_user_inline_policies = Mock(return_value=[])
792
+ integration._get_password_last_used = Mock(return_value=None)
793
+
794
+ result = integration._list_users()
795
+
796
+ assert len(result) == 2
797
+ assert result[0]["UserName"] == "user1"
798
+ assert result[1]["UserName"] == "user2"
799
+
800
+ @patch(f"{PATH}.IAMControlMapper")
801
+ @patch(f"{PATH}.boto3.Session")
802
+ def test_list_groups(self, mock_session_class, mock_mapper_class):
803
+ """Test listing IAM groups."""
804
+ mock_client = MagicMock()
805
+ mock_paginator = MagicMock()
806
+ mock_paginator.paginate.return_value = [{"Groups": [{"GroupName": "group1"}, {"GroupName": "group2"}]}]
807
+ mock_client.get_paginator.return_value = mock_paginator
808
+
809
+ mock_session = MagicMock()
810
+ mock_session.client.return_value = mock_client
811
+ mock_session_class.return_value = mock_session
812
+
813
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
814
+ result = integration._list_groups()
815
+
816
+ assert len(result) == 2
817
+ assert result[0]["GroupName"] == "group1"
818
+
819
+ @patch(f"{PATH}.IAMControlMapper")
820
+ @patch(f"{PATH}.boto3.Session")
821
+ def test_list_roles(self, mock_session_class, mock_mapper_class):
822
+ """Test listing IAM roles."""
823
+ mock_client = MagicMock()
824
+ mock_paginator = MagicMock()
825
+ mock_paginator.paginate.return_value = [{"Roles": [{"RoleName": "role1"}, {"RoleName": "role2"}]}]
826
+ mock_client.get_paginator.return_value = mock_paginator
827
+
828
+ mock_session = MagicMock()
829
+ mock_session.client.return_value = mock_client
830
+ mock_session_class.return_value = mock_session
831
+
832
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
833
+ integration._list_role_attached_policies = Mock(return_value=[])
834
+
835
+ result = integration._list_roles()
836
+
837
+ assert len(result) == 2
838
+ assert result[0]["RoleName"] == "role1"
839
+ assert "AttachedPolicies" in result[0]
840
+
841
+ @patch(f"{PATH}.IAMControlMapper")
842
+ @patch(f"{PATH}.boto3.Session")
843
+ def test_list_policies(self, mock_session_class, mock_mapper_class):
844
+ """Test listing customer managed policies."""
845
+ mock_client = MagicMock()
846
+ mock_paginator = MagicMock()
847
+ mock_paginator.paginate.return_value = [{"Policies": [{"PolicyName": "policy1"}, {"PolicyName": "policy2"}]}]
848
+ mock_client.get_paginator.return_value = mock_paginator
849
+
850
+ mock_session = MagicMock()
851
+ mock_session.client.return_value = mock_client
852
+ mock_session_class.return_value = mock_session
853
+
854
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
855
+ result = integration._list_policies()
856
+
857
+ assert len(result) == 2
858
+ assert result[0]["PolicyName"] == "policy1"
859
+
860
+ @patch(f"{PATH}.IAMControlMapper")
861
+ @patch(f"{PATH}.boto3.Session")
862
+ def test_fetch_fresh_iam_data(self, mock_session_class, mock_mapper_class):
863
+ """Test fetching fresh IAM data."""
864
+ mock_client = MagicMock()
865
+ mock_client.get_account_summary.return_value = {"SummaryMap": {"Users": 5}}
866
+
867
+ mock_session = MagicMock()
868
+ mock_session.client.return_value = mock_client
869
+ mock_session_class.return_value = mock_session
870
+
871
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
872
+ integration._get_password_policy = Mock(return_value={"MinimumPasswordLength": 14})
873
+ integration._list_users = Mock(return_value=[{"UserName": "user1"}])
874
+ integration._list_groups = Mock(return_value=[{"GroupName": "group1"}])
875
+ integration._list_roles = Mock(return_value=[{"RoleName": "role1"}])
876
+ integration._list_policies = Mock(return_value=[{"PolicyName": "policy1"}])
877
+
878
+ result = integration._fetch_fresh_iam_data()
879
+
880
+ assert "account_summary" in result
881
+ assert "password_policy" in result
882
+ assert "users" in result
883
+ assert "groups" in result
884
+ assert "roles" in result
885
+ assert "policies" in result
886
+ assert len(result["users"]) == 1
887
+
888
+ @patch(f"{PATH}.IAMControlMapper")
889
+ @patch(f"{PATH}.boto3.Session")
890
+ def test_fetch_fresh_iam_data_error(self, mock_session_class, mock_mapper_class):
891
+ """Test fetching fresh IAM data with error."""
892
+ mock_client = MagicMock()
893
+ error_response = {"Error": {"Code": "AccessDenied", "Message": "Access denied"}}
894
+ mock_client.get_account_summary.side_effect = ClientError(error_response, "GetAccountSummary")
895
+
896
+ mock_session = MagicMock()
897
+ mock_session.client.return_value = mock_client
898
+ mock_session_class.return_value = mock_session
899
+
900
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
901
+
902
+ result = integration._fetch_fresh_iam_data()
903
+
904
+ assert result == {}
905
+
906
+ @patch(f"{PATH}.IAMControlMapper")
907
+ @patch(f"{PATH}.boto3.Session")
908
+ def test_fetch_compliance_data_from_cache(self, mock_session_class, mock_mapper_class):
909
+ """Test fetching compliance data from cache."""
910
+ mock_session = MagicMock()
911
+ mock_session.client.return_value = MagicMock()
912
+ mock_session_class.return_value = mock_session
913
+
914
+ cached_data = {"users": [{"UserName": "user1"}], "groups": [], "roles": [], "policies": []}
915
+
916
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
917
+ integration._is_cache_valid = Mock(return_value=True)
918
+ integration._load_cached_data = Mock(return_value=cached_data)
919
+
920
+ result = integration.fetch_compliance_data()
921
+
922
+ assert result == [cached_data]
923
+ assert integration.raw_iam_data == cached_data
924
+
925
+ @patch(f"{PATH}.IAMControlMapper")
926
+ @patch(f"{PATH}.boto3.Session")
927
+ def test_fetch_compliance_data_force_refresh(self, mock_session_class, mock_mapper_class):
928
+ """Test fetching compliance data with force refresh."""
929
+ mock_session = MagicMock()
930
+ mock_session.client.return_value = MagicMock()
931
+ mock_session_class.return_value = mock_session
932
+
933
+ fresh_data = {"users": [{"UserName": "fresh-user"}], "groups": [], "roles": [], "policies": []}
934
+
935
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1", force_refresh=True)
936
+ integration._fetch_fresh_iam_data = Mock(return_value=fresh_data)
937
+ integration._save_to_cache = Mock()
938
+
939
+ result = integration.fetch_compliance_data()
940
+
941
+ assert result == [fresh_data]
942
+ assert integration.raw_iam_data == fresh_data
943
+ integration._save_to_cache.assert_called_once_with(fresh_data)
944
+
945
+ @patch(f"{PATH}.IAMControlMapper")
946
+ @patch(f"{PATH}.boto3.Session")
947
+ def test_create_compliance_item(self, mock_session_class, mock_mapper_class):
948
+ """Test creating compliance item from raw data."""
949
+ mock_session = MagicMock()
950
+ mock_session.client.return_value = MagicMock()
951
+ mock_session_class.return_value = mock_session
952
+
953
+ mock_mapper = MagicMock()
954
+ mock_mapper.assess_iam_compliance.return_value = {"AC-2": "PASS"}
955
+ mock_mapper_class.return_value = mock_mapper
956
+
957
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
958
+
959
+ raw_data = {"users": [{"UserName": "user1"}], "groups": [], "roles": [], "policies": []}
960
+
961
+ result = integration.create_compliance_item(raw_data)
962
+
963
+ assert isinstance(result, IAMComplianceItem)
964
+ assert result.iam_data == raw_data
965
+
966
+
967
+ class TestEvidenceCollection:
968
+ """Test cases for evidence collection methods."""
969
+
970
+ @patch(f"{PATH}.IAMControlMapper")
971
+ @patch(f"{PATH}.boto3.Session")
972
+ @patch(f"{PATH}.get_current_datetime")
973
+ def test_collect_iam_evidence_as_attachments(self, mock_get_datetime, mock_session_class, mock_mapper_class):
974
+ """Test collecting evidence as SSP attachments."""
975
+ mock_session = MagicMock()
976
+ mock_session.client.return_value = MagicMock()
977
+ mock_session_class.return_value = mock_session
978
+
979
+ mock_get_datetime.return_value = "2023-12-01"
980
+
981
+ integration = AWSIAMEvidenceIntegration(
982
+ plan_id=123, region="us-east-1", collect_evidence=True, evidence_as_attachments=True
983
+ )
984
+
985
+ integration.raw_iam_data = {"users": [], "groups": [], "roles": [], "policies": []}
986
+ integration._create_ssp_attachment = Mock()
987
+
988
+ integration._collect_iam_evidence()
989
+
990
+ integration._create_ssp_attachment.assert_called_once_with("2023-12-01")
991
+
992
+ @patch(f"{PATH}.IAMControlMapper")
993
+ @patch(f"{PATH}.boto3.Session")
994
+ @patch(f"{PATH}.get_current_datetime")
995
+ def test_collect_iam_evidence_as_records(self, mock_get_datetime, mock_session_class, mock_mapper_class):
996
+ """Test collecting evidence as evidence records."""
997
+ mock_session = MagicMock()
998
+ mock_session.client.return_value = MagicMock()
999
+ mock_session_class.return_value = mock_session
1000
+
1001
+ mock_get_datetime.return_value = "2023-12-01"
1002
+
1003
+ integration = AWSIAMEvidenceIntegration(
1004
+ plan_id=123, region="us-east-1", collect_evidence=True, evidence_as_attachments=False
1005
+ )
1006
+
1007
+ integration.raw_iam_data = {"users": [], "groups": [], "roles": [], "policies": []}
1008
+ integration._create_evidence_record = Mock()
1009
+
1010
+ integration._collect_iam_evidence()
1011
+
1012
+ integration._create_evidence_record.assert_called_once_with("2023-12-01")
1013
+
1014
+ @patch(f"{PATH}.IAMControlMapper")
1015
+ @patch(f"{PATH}.boto3.Session")
1016
+ def test_collect_iam_evidence_no_data(self, mock_session_class, mock_mapper_class):
1017
+ """Test collecting evidence when no data is available."""
1018
+ mock_session = MagicMock()
1019
+ mock_session.client.return_value = MagicMock()
1020
+ mock_session_class.return_value = mock_session
1021
+
1022
+ integration = AWSIAMEvidenceIntegration(
1023
+ plan_id=123, region="us-east-1", collect_evidence=True, evidence_as_attachments=True
1024
+ )
1025
+
1026
+ integration.raw_iam_data = {}
1027
+
1028
+ integration._collect_iam_evidence()
1029
+
1030
+ @patch(f"{PATH}.IAMControlMapper")
1031
+ @patch(f"{PATH}.boto3.Session")
1032
+ @patch(f"{PATH}.Api")
1033
+ @patch(f"{PATH}.File")
1034
+ def test_create_ssp_attachment_success(
1035
+ self, mock_file_class, mock_api_class, mock_session_class, mock_mapper_class
1036
+ ):
1037
+ """Test creating SSP attachment successfully."""
1038
+ mock_session = MagicMock()
1039
+ mock_session.client.return_value = MagicMock()
1040
+ mock_session_class.return_value = mock_session
1041
+
1042
+ mock_mapper = MagicMock()
1043
+ mock_mapper.assess_iam_compliance.return_value = {"AC-2": "PASS", "AC-6": "PASS"}
1044
+ mock_mapper_class.return_value = mock_mapper
1045
+
1046
+ mock_api = MagicMock()
1047
+ mock_api_class.return_value = mock_api
1048
+
1049
+ mock_file_class.upload_file_to_regscale.return_value = True
1050
+
1051
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
1052
+ integration.raw_iam_data = {"users": [], "groups": [], "roles": [], "policies": []}
1053
+
1054
+ integration._create_ssp_attachment("2023-12-01")
1055
+
1056
+ mock_file_class.upload_file_to_regscale.assert_called_once()
1057
+ call_args = mock_file_class.upload_file_to_regscale.call_args[1]
1058
+ assert call_args["parent_id"] == 123
1059
+ assert call_args["parent_module"] == "securityplans"
1060
+ assert "iam_evidence_" in call_args["file_name"]
1061
+ assert "aws,iam,access-control,automated" == call_args["tags"]
1062
+
1063
+ @patch(f"{PATH}.IAMControlMapper")
1064
+ @patch(f"{PATH}.boto3.Session")
1065
+ @patch(f"{PATH}.Api")
1066
+ @patch(f"{PATH}.File")
1067
+ def test_create_ssp_attachment_failure(
1068
+ self, mock_file_class, mock_api_class, mock_session_class, mock_mapper_class
1069
+ ):
1070
+ """Test creating SSP attachment with failure."""
1071
+ mock_session = MagicMock()
1072
+ mock_session.client.return_value = MagicMock()
1073
+ mock_session_class.return_value = mock_session
1074
+
1075
+ mock_mapper = MagicMock()
1076
+ mock_mapper.assess_iam_compliance.return_value = {}
1077
+ mock_mapper_class.return_value = mock_mapper
1078
+
1079
+ mock_api = MagicMock()
1080
+ mock_api_class.return_value = mock_api
1081
+
1082
+ mock_file_class.upload_file_to_regscale.return_value = False
1083
+
1084
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
1085
+ integration.raw_iam_data = {"users": [], "groups": [], "roles": [], "policies": []}
1086
+
1087
+ integration._create_ssp_attachment("2023-12-01")
1088
+
1089
+ @patch(f"{PATH}.IAMControlMapper")
1090
+ @patch(f"{PATH}.boto3.Session")
1091
+ @patch(f"{PATH}.Api")
1092
+ @patch(f"{PATH}.File")
1093
+ def test_create_ssp_attachment_exception(
1094
+ self, mock_file_class, mock_api_class, mock_session_class, mock_mapper_class
1095
+ ):
1096
+ """Test creating SSP attachment with exception."""
1097
+ mock_session = MagicMock()
1098
+ mock_session.client.return_value = MagicMock()
1099
+ mock_session_class.return_value = mock_session
1100
+
1101
+ mock_mapper = MagicMock()
1102
+ mock_mapper.assess_iam_compliance.side_effect = Exception("Test error")
1103
+ mock_mapper_class.return_value = mock_mapper
1104
+
1105
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
1106
+ integration.raw_iam_data = {"users": [], "groups": [], "roles": [], "policies": []}
1107
+
1108
+ integration._create_ssp_attachment("2023-12-01")
1109
+
1110
+ @patch(f"{PATH}.IAMControlMapper")
1111
+ @patch(f"{PATH}.boto3.Session")
1112
+ @patch(f"{PATH}.Evidence")
1113
+ def test_create_evidence_record_success(self, mock_evidence_class, mock_session_class, mock_mapper_class):
1114
+ """Test creating evidence record successfully."""
1115
+ mock_session = MagicMock()
1116
+ mock_session.client.return_value = MagicMock()
1117
+ mock_session_class.return_value = mock_session
1118
+
1119
+ mock_mapper = MagicMock()
1120
+ mock_mapper.assess_iam_compliance.return_value = {"AC-2": "PASS", "AC-6": "FAIL"}
1121
+ mock_mapper.get_control_description.side_effect = lambda x: f"Description for {x}"
1122
+ mock_mapper_class.return_value = mock_mapper
1123
+
1124
+ mock_evidence = MagicMock()
1125
+ mock_evidence.id = 999
1126
+ mock_evidence_instance = MagicMock()
1127
+ mock_evidence_instance.create.return_value = mock_evidence
1128
+ mock_evidence_class.return_value = mock_evidence_instance
1129
+
1130
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1", evidence_frequency=90)
1131
+ integration.raw_iam_data = {"users": [], "groups": [], "roles": [], "policies": []}
1132
+ integration._upload_evidence_file = Mock()
1133
+ integration._link_evidence_to_ssp = Mock()
1134
+
1135
+ integration._create_evidence_record("2023-12-01")
1136
+
1137
+ mock_evidence_instance.create.assert_called_once()
1138
+ integration._upload_evidence_file.assert_called_once_with(999, "2023-12-01")
1139
+ integration._link_evidence_to_ssp.assert_called_once_with(999)
1140
+
1141
+ @patch(f"{PATH}.IAMControlMapper")
1142
+ @patch(f"{PATH}.boto3.Session")
1143
+ @patch(f"{PATH}.Evidence")
1144
+ def test_create_evidence_record_creation_failure(self, mock_evidence_class, mock_session_class, mock_mapper_class):
1145
+ """Test creating evidence record when creation fails."""
1146
+ mock_session = MagicMock()
1147
+ mock_session.client.return_value = MagicMock()
1148
+ mock_session_class.return_value = mock_session
1149
+
1150
+ mock_mapper = MagicMock()
1151
+ mock_mapper.assess_iam_compliance.return_value = {}
1152
+ mock_mapper_class.return_value = mock_mapper
1153
+
1154
+ mock_evidence_instance = MagicMock()
1155
+ mock_evidence_instance.create.return_value = None
1156
+ mock_evidence_class.return_value = mock_evidence_instance
1157
+
1158
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
1159
+ integration.raw_iam_data = {"users": [], "groups": [], "roles": [], "policies": []}
1160
+
1161
+ integration._create_evidence_record("2023-12-01")
1162
+
1163
+ mock_evidence_instance.create.assert_called_once()
1164
+
1165
+ @patch(f"{PATH}.IAMControlMapper")
1166
+ @patch(f"{PATH}.boto3.Session")
1167
+ @patch(f"{PATH}.Evidence")
1168
+ def test_create_evidence_record_exception(self, mock_evidence_class, mock_session_class, mock_mapper_class):
1169
+ """Test creating evidence record with exception."""
1170
+ mock_session = MagicMock()
1171
+ mock_session.client.return_value = MagicMock()
1172
+ mock_session_class.return_value = mock_session
1173
+
1174
+ mock_mapper = MagicMock()
1175
+ mock_mapper.assess_iam_compliance.side_effect = Exception("Test error")
1176
+ mock_mapper_class.return_value = mock_mapper
1177
+
1178
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
1179
+ integration.raw_iam_data = {"users": [], "groups": [], "roles": [], "policies": []}
1180
+
1181
+ integration._create_evidence_record("2023-12-01")
1182
+
1183
+ @patch(f"{PATH}.IAMControlMapper")
1184
+ @patch(f"{PATH}.boto3.Session")
1185
+ def test_build_evidence_description(self, mock_session_class, mock_mapper_class):
1186
+ """Test building evidence description."""
1187
+ mock_session = MagicMock()
1188
+ mock_session.client.return_value = MagicMock()
1189
+ mock_session_class.return_value = mock_session
1190
+
1191
+ mock_mapper = MagicMock()
1192
+ mock_mapper.assess_iam_compliance.return_value = {"AC-2": "PASS", "AC-6": "FAIL", "IA-2": "PASS"}
1193
+ mock_mapper.get_control_description.side_effect = lambda x: f"{x} Description"
1194
+ mock_mapper_class.return_value = mock_mapper
1195
+
1196
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
1197
+ integration.raw_iam_data = {
1198
+ "users": [{"UserName": "user1"}],
1199
+ "groups": [{"GroupName": "group1"}],
1200
+ "roles": [{"RoleName": "role1"}],
1201
+ }
1202
+
1203
+ result = integration._build_evidence_description("2023-12-01")
1204
+
1205
+ assert "AWS IAM Access Control Evidence" in result
1206
+ assert "2023-12-01" in result
1207
+ assert "AC-2" in result
1208
+ assert "AC-6" in result
1209
+ assert "IA-2" in result
1210
+
1211
+ @patch(f"{PATH}.IAMControlMapper")
1212
+ @patch(f"{PATH}.boto3.Session")
1213
+ @patch(f"{PATH}.Api")
1214
+ @patch(f"{PATH}.File")
1215
+ def test_upload_evidence_file_success(self, mock_file_class, mock_api_class, mock_session_class, mock_mapper_class):
1216
+ """Test uploading evidence file successfully."""
1217
+ mock_session = MagicMock()
1218
+ mock_session.client.return_value = MagicMock()
1219
+ mock_session_class.return_value = mock_session
1220
+
1221
+ mock_mapper = MagicMock()
1222
+ mock_mapper.assess_iam_compliance.return_value = {"AC-2": "PASS"}
1223
+ mock_mapper_class.return_value = mock_mapper
1224
+
1225
+ mock_api = MagicMock()
1226
+ mock_api_class.return_value = mock_api
1227
+
1228
+ mock_file_class.upload_file_to_regscale.return_value = True
1229
+
1230
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
1231
+ integration.raw_iam_data = {"users": [], "groups": [], "roles": [], "policies": []}
1232
+
1233
+ integration._upload_evidence_file(999, "2023-12-01")
1234
+
1235
+ mock_file_class.upload_file_to_regscale.assert_called_once()
1236
+ call_args = mock_file_class.upload_file_to_regscale.call_args[1]
1237
+ assert call_args["parent_id"] == 999
1238
+ assert call_args["parent_module"] == "evidence"
1239
+ assert "iam_evidence_" in call_args["file_name"]
1240
+ assert "aws,iam,access-control" == call_args["tags"]
1241
+
1242
+ @patch(f"{PATH}.IAMControlMapper")
1243
+ @patch(f"{PATH}.boto3.Session")
1244
+ @patch(f"{PATH}.Api")
1245
+ @patch(f"{PATH}.File")
1246
+ def test_upload_evidence_file_failure(self, mock_file_class, mock_api_class, mock_session_class, mock_mapper_class):
1247
+ """Test uploading evidence file with failure."""
1248
+ mock_session = MagicMock()
1249
+ mock_session.client.return_value = MagicMock()
1250
+ mock_session_class.return_value = mock_session
1251
+
1252
+ mock_mapper = MagicMock()
1253
+ mock_mapper.assess_iam_compliance.return_value = {}
1254
+ mock_mapper_class.return_value = mock_mapper
1255
+
1256
+ mock_api = MagicMock()
1257
+ mock_api_class.return_value = mock_api
1258
+
1259
+ mock_file_class.upload_file_to_regscale.return_value = False
1260
+
1261
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
1262
+ integration.raw_iam_data = {"users": [], "groups": [], "roles": [], "policies": []}
1263
+
1264
+ integration._upload_evidence_file(999, "2023-12-01")
1265
+
1266
+ @patch(f"{PATH}.IAMControlMapper")
1267
+ @patch(f"{PATH}.boto3.Session")
1268
+ @patch(f"{PATH}.Api")
1269
+ @patch(f"{PATH}.File")
1270
+ def test_upload_evidence_file_exception(
1271
+ self, mock_file_class, mock_api_class, mock_session_class, mock_mapper_class
1272
+ ):
1273
+ """Test uploading evidence file with exception."""
1274
+ mock_session = MagicMock()
1275
+ mock_session.client.return_value = MagicMock()
1276
+ mock_session_class.return_value = mock_session
1277
+
1278
+ mock_mapper = MagicMock()
1279
+ mock_mapper.assess_iam_compliance.side_effect = Exception("Test error")
1280
+ mock_mapper_class.return_value = mock_mapper
1281
+
1282
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
1283
+ integration.raw_iam_data = {"users": [], "groups": [], "roles": [], "policies": []}
1284
+
1285
+ integration._upload_evidence_file(999, "2023-12-01")
1286
+
1287
+ @patch(f"{PATH}.IAMControlMapper")
1288
+ @patch(f"{PATH}.boto3.Session")
1289
+ @patch(f"{PATH}.EvidenceMapping")
1290
+ def test_link_evidence_to_ssp_success(self, mock_mapping_class, mock_session_class, mock_mapper_class):
1291
+ """Test linking evidence to SSP successfully."""
1292
+ mock_session = MagicMock()
1293
+ mock_session.client.return_value = MagicMock()
1294
+ mock_session_class.return_value = mock_session
1295
+
1296
+ mock_mapping = MagicMock()
1297
+ mock_mapping_class.return_value = mock_mapping
1298
+
1299
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
1300
+
1301
+ integration._link_evidence_to_ssp(999)
1302
+
1303
+ mock_mapping_class.assert_called_once_with(evidenceID=999, mappedID=123, mappingType="securityplans")
1304
+ mock_mapping.create.assert_called_once()
1305
+
1306
+ @patch(f"{PATH}.IAMControlMapper")
1307
+ @patch(f"{PATH}.boto3.Session")
1308
+ @patch(f"{PATH}.EvidenceMapping")
1309
+ def test_link_evidence_to_ssp_failure(self, mock_mapping_class, mock_session_class, mock_mapper_class):
1310
+ """Test linking evidence to SSP with failure."""
1311
+ mock_session = MagicMock()
1312
+ mock_session.client.return_value = MagicMock()
1313
+ mock_session_class.return_value = mock_session
1314
+
1315
+ mock_mapping = MagicMock()
1316
+ mock_mapping.create.side_effect = Exception("Test error")
1317
+ mock_mapping_class.return_value = mock_mapping
1318
+
1319
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
1320
+
1321
+ integration._link_evidence_to_ssp(999)
1322
+
1323
+
1324
+ class TestSyncCompliance:
1325
+ """Test cases for sync_compliance method."""
1326
+
1327
+ @patch(f"{PATH}.IAMControlMapper")
1328
+ @patch(f"{PATH}.boto3.Session")
1329
+ def test_sync_compliance_with_evidence_collection(self, mock_session_class, mock_mapper_class):
1330
+ """Test sync_compliance with evidence collection enabled."""
1331
+ mock_session = MagicMock()
1332
+ mock_session.client.return_value = MagicMock()
1333
+ mock_session_class.return_value = mock_session
1334
+
1335
+ mock_mapper = MagicMock()
1336
+ mock_mapper.assess_iam_compliance.return_value = {"AC-2": "PASS"}
1337
+ mock_mapper_class.return_value = mock_mapper
1338
+
1339
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1", collect_evidence=True)
1340
+ integration.fetch_compliance_data = Mock(
1341
+ return_value=[{"users": [], "groups": [], "roles": [], "policies": []}]
1342
+ )
1343
+ integration._collect_iam_evidence = Mock()
1344
+
1345
+ with patch.object(integration.__class__.__bases__[0], "sync_compliance"):
1346
+ integration.sync_compliance()
1347
+
1348
+ integration._collect_iam_evidence.assert_called_once()
1349
+
1350
+ @patch(f"{PATH}.IAMControlMapper")
1351
+ @patch(f"{PATH}.boto3.Session")
1352
+ def test_sync_compliance_without_evidence_collection(self, mock_session_class, mock_mapper_class):
1353
+ """Test sync_compliance without evidence collection."""
1354
+ mock_session = MagicMock()
1355
+ mock_session.client.return_value = MagicMock()
1356
+ mock_session_class.return_value = mock_session
1357
+
1358
+ mock_mapper = MagicMock()
1359
+ mock_mapper.assess_iam_compliance.return_value = {"AC-2": "PASS"}
1360
+ mock_mapper_class.return_value = mock_mapper
1361
+
1362
+ integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1", collect_evidence=False)
1363
+ integration.fetch_compliance_data = Mock(
1364
+ return_value=[{"users": [], "groups": [], "roles": [], "policies": []}]
1365
+ )
1366
+ integration._collect_iam_evidence = Mock()
1367
+
1368
+ with patch.object(integration.__class__.__bases__[0], "sync_compliance"):
1369
+ integration.sync_compliance()
1370
+
1371
+ integration._collect_iam_evidence.assert_not_called()
1372
+
1373
+
1374
+ if __name__ == "__main__":
1375
+ pytest.main([__file__, "-v"])