regscale-cli 6.27.3.0__py3-none-any.whl → 6.28.1.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 (113) 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/commercial/synqly/ticketing.py +27 -0
  59. regscale/integrations/compliance_integration.py +308 -38
  60. regscale/integrations/due_date_handler.py +3 -0
  61. regscale/integrations/scanner_integration.py +399 -84
  62. regscale/models/integration_models/cisa_kev_data.json +65 -5
  63. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  64. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +17 -9
  65. regscale/models/regscale_models/assessment.py +2 -1
  66. regscale/models/regscale_models/control_objective.py +74 -5
  67. regscale/models/regscale_models/file.py +2 -0
  68. regscale/models/regscale_models/issue.py +2 -5
  69. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/METADATA +1 -1
  70. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/RECORD +113 -34
  71. tests/regscale/integrations/commercial/aws/__init__.py +0 -0
  72. tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
  73. tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
  74. tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
  75. tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
  76. tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
  77. tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
  78. tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
  79. tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
  80. tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
  81. tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
  82. tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
  83. tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
  84. tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
  85. tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
  86. tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
  87. tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
  88. tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
  89. tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
  90. tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
  91. tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
  92. tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
  93. tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
  94. tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
  95. tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
  96. tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
  97. tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
  98. tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
  99. tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
  100. tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
  101. tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
  102. tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
  103. tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
  104. tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
  105. tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
  106. tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
  107. tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
  108. tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
  109. tests/regscale/integrations/commercial/test_aws.py +55 -56
  110. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/LICENSE +0 -0
  111. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/WHEEL +0 -0
  112. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/entry_points.txt +0 -0
  113. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1240 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Unit tests for AWS Organizations 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.org_evidence import (
17
+ OrgComplianceItem,
18
+ AWSOrganizationsEvidenceIntegration,
19
+ ORG_CACHE_FILE,
20
+ CACHE_TTL_SECONDS,
21
+ )
22
+
23
+ PATH = "regscale.integrations.commercial.aws.org_evidence"
24
+
25
+
26
+ class TestOrgComplianceItem:
27
+ """Test cases for OrgComplianceItem 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 organization data."""
36
+ org_data = {
37
+ "Id": "o-abc123xyz456",
38
+ "Arn": "arn:aws:organizations::123456789012:organization/o-abc123xyz456",
39
+ "MasterAccountId": "123456789012",
40
+ "accounts": [
41
+ {"Id": "111111111111", "Name": "Account1", "Status": "ACTIVE", "Email": "account1@example.com"},
42
+ {"Id": "222222222222", "Name": "Account2", "Status": "ACTIVE", "Email": "account2@example.com"},
43
+ ],
44
+ "organizational_units": [
45
+ {"Id": "ou-abc1-11111111", "Name": "Production"},
46
+ {"Id": "ou-abc1-22222222", "Name": "Development"},
47
+ ],
48
+ "service_control_policies": [
49
+ {"Id": "p-FullAWSAccess", "Name": "FullAWSAccess", "Type": "SERVICE_CONTROL_POLICY"},
50
+ {"Id": "p-DenyS3", "Name": "DenyS3Access", "Type": "SERVICE_CONTROL_POLICY"},
51
+ ],
52
+ }
53
+
54
+ self.mock_mapper.assess_organization_compliance.return_value = {
55
+ "AC-1": "PASS",
56
+ "PM-9": "PASS",
57
+ "AC-2": "PASS",
58
+ "AC-6": "PASS",
59
+ }
60
+
61
+ item = OrgComplianceItem(org_data, self.mock_mapper)
62
+
63
+ assert item.org_data == org_data
64
+ assert item.control_mapper == self.mock_mapper
65
+ assert item._org_id == "o-abc123xyz456"
66
+ assert item._org_arn == "arn:aws:organizations::123456789012:organization/o-abc123xyz456"
67
+ assert item._master_account_id == "123456789012"
68
+ assert len(item._accounts) == 2
69
+ assert len(item._ous) == 2
70
+ assert len(item._scps) == 2
71
+ assert item._compliance_results == self.mock_mapper.assess_organization_compliance.return_value
72
+
73
+ def test_init_with_minimal_data(self):
74
+ """Test initialization with minimal organization data."""
75
+ org_data = {}
76
+
77
+ self.mock_mapper.assess_organization_compliance.return_value = {}
78
+
79
+ item = OrgComplianceItem(org_data, self.mock_mapper)
80
+
81
+ assert item._org_id == ""
82
+ assert item._org_arn == ""
83
+ assert item._master_account_id == ""
84
+ assert len(item._accounts) == 0
85
+ assert len(item._ous) == 0
86
+ assert len(item._scps) == 0
87
+
88
+ def test_resource_id_property(self):
89
+ """Test resource_id property."""
90
+ org_data = {"Id": "o-testorg123"}
91
+ self.mock_mapper.assess_organization_compliance.return_value = {}
92
+
93
+ item = OrgComplianceItem(org_data, self.mock_mapper)
94
+
95
+ assert item.resource_id == "o-testorg123"
96
+
97
+ def test_resource_name_property(self):
98
+ """Test resource_name property."""
99
+ org_data = {"Id": "o-abc123xyz456789"}
100
+ self.mock_mapper.assess_organization_compliance.return_value = {}
101
+
102
+ item = OrgComplianceItem(org_data, self.mock_mapper)
103
+
104
+ assert item.resource_name == "AWS Organization o-abc123xyz4..."
105
+
106
+ def test_control_id_property_with_failure(self):
107
+ """Test control_id property returns first failed control."""
108
+ org_data = {}
109
+ self.mock_mapper.assess_organization_compliance.return_value = {
110
+ "AC-1": "PASS",
111
+ "PM-9": "FAIL",
112
+ "AC-2": "PASS",
113
+ }
114
+
115
+ item = OrgComplianceItem(org_data, self.mock_mapper)
116
+
117
+ assert item.control_id == "PM-9"
118
+
119
+ def test_control_id_property_all_pass(self):
120
+ """Test control_id property when all controls pass."""
121
+ org_data = {}
122
+ self.mock_mapper.assess_organization_compliance.return_value = {
123
+ "AC-1": "PASS",
124
+ "PM-9": "PASS",
125
+ }
126
+
127
+ item = OrgComplianceItem(org_data, self.mock_mapper)
128
+
129
+ assert item.control_id == "AC-1"
130
+
131
+ def test_control_id_property_empty_results(self):
132
+ """Test control_id property with no compliance results."""
133
+ org_data = {}
134
+ self.mock_mapper.assess_organization_compliance.return_value = {}
135
+
136
+ item = OrgComplianceItem(org_data, self.mock_mapper)
137
+
138
+ assert item.control_id == "AC-1"
139
+
140
+ def test_compliance_result_property_pass(self):
141
+ """Test compliance_result property when all checks pass."""
142
+ org_data = {}
143
+ self.mock_mapper.assess_organization_compliance.return_value = {
144
+ "AC-1": "PASS",
145
+ "PM-9": "PASS",
146
+ "AC-2": "PASS",
147
+ }
148
+
149
+ item = OrgComplianceItem(org_data, self.mock_mapper)
150
+
151
+ assert item.compliance_result == "PASS"
152
+
153
+ def test_compliance_result_property_fail(self):
154
+ """Test compliance_result property when any check fails."""
155
+ org_data = {}
156
+ self.mock_mapper.assess_organization_compliance.return_value = {
157
+ "AC-1": "PASS",
158
+ "PM-9": "FAIL",
159
+ "AC-2": "PASS",
160
+ }
161
+
162
+ item = OrgComplianceItem(org_data, self.mock_mapper)
163
+
164
+ assert item.compliance_result == "FAIL"
165
+
166
+ def test_compliance_result_property_empty(self):
167
+ """Test compliance_result property with no results."""
168
+ org_data = {}
169
+ self.mock_mapper.assess_organization_compliance.return_value = {}
170
+
171
+ item = OrgComplianceItem(org_data, self.mock_mapper)
172
+
173
+ assert item.compliance_result == "PASS"
174
+
175
+ def test_severity_property_pass(self):
176
+ """Test severity property when compliance passes."""
177
+ org_data = {}
178
+ self.mock_mapper.assess_organization_compliance.return_value = {
179
+ "AC-1": "PASS",
180
+ "PM-9": "PASS",
181
+ }
182
+
183
+ item = OrgComplianceItem(org_data, self.mock_mapper)
184
+
185
+ assert item.severity is None
186
+
187
+ def test_severity_property_high_ac1_fail(self):
188
+ """Test severity property for AC-1 failures."""
189
+ org_data = {}
190
+ self.mock_mapper.assess_organization_compliance.return_value = {
191
+ "AC-1": "FAIL",
192
+ "PM-9": "PASS",
193
+ }
194
+
195
+ item = OrgComplianceItem(org_data, self.mock_mapper)
196
+
197
+ assert item.severity == "HIGH"
198
+
199
+ def test_severity_property_high_pm9_fail(self):
200
+ """Test severity property for PM-9 failures."""
201
+ org_data = {}
202
+ self.mock_mapper.assess_organization_compliance.return_value = {
203
+ "AC-1": "PASS",
204
+ "PM-9": "FAIL",
205
+ }
206
+
207
+ item = OrgComplianceItem(org_data, self.mock_mapper)
208
+
209
+ assert item.severity == "HIGH"
210
+
211
+ def test_severity_property_medium_ac6_fail(self):
212
+ """Test severity property for AC-6 failures."""
213
+ org_data = {}
214
+ self.mock_mapper.assess_organization_compliance.return_value = {
215
+ "AC-1": "PASS",
216
+ "PM-9": "PASS",
217
+ "AC-6": "FAIL",
218
+ }
219
+
220
+ item = OrgComplianceItem(org_data, self.mock_mapper)
221
+
222
+ assert item.severity == "MEDIUM"
223
+
224
+ def test_severity_property_medium_ac2_fail(self):
225
+ """Test severity property for AC-2 failures."""
226
+ org_data = {}
227
+ self.mock_mapper.assess_organization_compliance.return_value = {
228
+ "AC-1": "PASS",
229
+ "AC-2": "FAIL",
230
+ }
231
+
232
+ item = OrgComplianceItem(org_data, self.mock_mapper)
233
+
234
+ assert item.severity == "MEDIUM"
235
+
236
+ def test_description_property_pass(self):
237
+ """Test description property with passing compliance."""
238
+ org_data = {
239
+ "Id": "o-testorg",
240
+ "Arn": "arn:aws:organizations::123456789012:organization/o-testorg",
241
+ "MasterAccountId": "123456789012",
242
+ "accounts": [{"Id": "111111111111"}],
243
+ "organizational_units": [{"Id": "ou-1"}],
244
+ "service_control_policies": [{"Id": "p-1"}],
245
+ }
246
+ self.mock_mapper.assess_organization_compliance.return_value = {
247
+ "AC-1": "PASS",
248
+ "PM-9": "PASS",
249
+ }
250
+ self.mock_mapper.get_control_description.side_effect = lambda x: f"{x} Description"
251
+
252
+ item = OrgComplianceItem(org_data, self.mock_mapper)
253
+ description = item.description
254
+
255
+ assert "AWS Organizations Governance Assessment" in description
256
+ assert "o-testorg" in description
257
+ assert "123456789012" in description
258
+ assert "Total Accounts:</strong> 1" in description
259
+ assert "Organizational Units:</strong> 1" in description
260
+ assert "Service Control Policies:</strong> 1" in description
261
+ assert "AC-1" in description
262
+ assert "PM-9" in description
263
+ assert "PASS" in description
264
+ assert "Remediation Guidance" not in description
265
+
266
+ def test_description_property_fail_with_remediation(self):
267
+ """Test description property with failing compliance and remediation guidance."""
268
+ org_data = {
269
+ "Id": "o-testorg",
270
+ "Arn": "arn:aws:organizations::123456789012:organization/o-testorg",
271
+ "MasterAccountId": "123456789012",
272
+ "accounts": [
273
+ {"Id": "111111111111", "Status": "ACTIVE"},
274
+ {"Id": "222222222222", "Status": "SUSPENDED"},
275
+ ],
276
+ "organizational_units": [{"Id": "ou-root"}],
277
+ "service_control_policies": [{"Id": "p-FullAWSAccess", "Name": "FullAWSAccess"}],
278
+ }
279
+ self.mock_mapper.assess_organization_compliance.return_value = {
280
+ "AC-1": "FAIL",
281
+ "PM-9": "FAIL",
282
+ "AC-2": "FAIL",
283
+ "AC-6": "FAIL",
284
+ }
285
+ self.mock_mapper.get_control_description.side_effect = lambda x: f"{x} Description"
286
+
287
+ item = OrgComplianceItem(org_data, self.mock_mapper)
288
+ description = item.description
289
+
290
+ assert "FAIL" in description
291
+ assert "Remediation Guidance" in description
292
+ assert "Create organizational units (OUs) for governance structure" in description
293
+ assert "Attach Service Control Policies to enforce access controls" in description
294
+ assert "Organize accounts by risk profile" in description
295
+ assert "Review and activate or remove 1 suspended accounts" in description
296
+ assert "Implement least privilege SCPs" in description
297
+
298
+ def test_framework_property(self):
299
+ """Test framework property."""
300
+ org_data = {}
301
+ self.mock_mapper.assess_organization_compliance.return_value = {}
302
+ self.mock_mapper.framework = "NIST800-53R5"
303
+
304
+ item = OrgComplianceItem(org_data, self.mock_mapper)
305
+
306
+ assert item.framework == "NIST800-53R5"
307
+
308
+
309
+ class TestAWSOrganizationsEvidenceIntegrationInit:
310
+ """Test cases for AWSOrganizationsEvidenceIntegration initialization."""
311
+
312
+ @patch(f"{PATH}.OrgControlMapper")
313
+ @patch(f"{PATH}.boto3.Session")
314
+ def test_init_with_defaults(self, mock_session_class, mock_mapper_class):
315
+ """Test initialization with default parameters."""
316
+ mock_session = MagicMock()
317
+ mock_client = MagicMock()
318
+ mock_session.client.return_value = mock_client
319
+ mock_session_class.return_value = mock_session
320
+
321
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123)
322
+
323
+ assert integration.plan_id == 123
324
+ assert integration.region == "us-east-1"
325
+ assert integration.title == "AWS Organizations"
326
+ assert integration.collect_evidence is False
327
+ assert integration.evidence_as_attachments is True
328
+ assert integration.evidence_control_ids is None
329
+ assert integration.evidence_frequency == 30
330
+ assert integration.force_refresh is False
331
+ assert integration.create_issues is True
332
+ assert integration.update_control_status is True
333
+ assert integration.create_poams is False
334
+ assert integration.parent_module == "securityplans"
335
+
336
+ mock_session_class.assert_called_once_with(profile_name=None, region_name="us-east-1")
337
+ mock_mapper_class.assert_called_once_with(framework="NIST800-53R5")
338
+
339
+ @patch(f"{PATH}.OrgControlMapper")
340
+ @patch(f"{PATH}.boto3.Session")
341
+ def test_init_with_explicit_credentials(self, mock_session_class, mock_mapper_class):
342
+ """Test initialization with explicit AWS credentials."""
343
+ mock_session = MagicMock()
344
+ mock_client = MagicMock()
345
+ mock_session.client.return_value = mock_client
346
+ mock_session_class.return_value = mock_session
347
+
348
+ integration = AWSOrganizationsEvidenceIntegration(
349
+ plan_id=456,
350
+ region="us-west-2",
351
+ aws_access_key_id="AKIATEST",
352
+ aws_secret_access_key="secret",
353
+ aws_session_token="token",
354
+ )
355
+
356
+ assert integration.region == "us-west-2"
357
+ mock_session_class.assert_called_once_with(
358
+ region_name="us-west-2",
359
+ aws_access_key_id="AKIATEST",
360
+ aws_secret_access_key="secret",
361
+ aws_session_token="token",
362
+ )
363
+
364
+ @patch(f"{PATH}.OrgControlMapper")
365
+ @patch(f"{PATH}.boto3.Session")
366
+ def test_init_with_profile(self, mock_session_class, mock_mapper_class):
367
+ """Test initialization with AWS profile."""
368
+ mock_session = MagicMock()
369
+ mock_client = MagicMock()
370
+ mock_session.client.return_value = mock_client
371
+ mock_session_class.return_value = mock_session
372
+
373
+ AWSOrganizationsEvidenceIntegration(plan_id=789, region="eu-west-1", profile="test-profile") # noqa: F841
374
+
375
+ mock_session_class.assert_called_once_with(profile_name="test-profile", region_name="eu-west-1")
376
+
377
+ @patch(f"{PATH}.OrgControlMapper")
378
+ @patch(f"{PATH}.boto3.Session")
379
+ def test_init_with_all_options(self, mock_session_class, mock_mapper_class):
380
+ """Test initialization with all optional parameters."""
381
+ mock_session = MagicMock()
382
+ mock_client = MagicMock()
383
+ mock_session.client.return_value = mock_client
384
+ mock_session_class.return_value = mock_session
385
+
386
+ integration = AWSOrganizationsEvidenceIntegration(
387
+ plan_id=999,
388
+ region="ap-southeast-1",
389
+ framework="ISO27001",
390
+ create_issues=False,
391
+ update_control_status=False,
392
+ create_poams=True,
393
+ parent_module="assessments",
394
+ collect_evidence=True,
395
+ evidence_as_attachments=False,
396
+ evidence_control_ids=["AC-1", "PM-9"],
397
+ evidence_frequency=60,
398
+ force_refresh=True,
399
+ )
400
+
401
+ assert integration.plan_id == 999
402
+ assert integration.region == "ap-southeast-1"
403
+ assert integration.framework == "ISO27001"
404
+ assert integration.create_issues is False
405
+ assert integration.update_control_status is False
406
+ assert integration.create_poams is True
407
+ assert integration.collect_evidence is True
408
+ assert integration.evidence_as_attachments is False
409
+ assert integration.evidence_control_ids == ["AC-1", "PM-9"]
410
+ assert integration.evidence_frequency == 60
411
+ assert integration.force_refresh is True
412
+
413
+ @patch(f"{PATH}.OrgControlMapper")
414
+ @patch(f"{PATH}.boto3.Session")
415
+ def test_init_client_creation_failure(self, mock_session_class, mock_mapper_class):
416
+ """Test initialization when Organizations client creation fails."""
417
+ mock_session = MagicMock()
418
+ mock_session.client.side_effect = Exception("Failed to create Organizations client")
419
+ mock_session_class.return_value = mock_session
420
+
421
+ with pytest.raises(Exception) as exc_info:
422
+ AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
423
+
424
+ assert "Failed to create Organizations client" in str(exc_info.value)
425
+
426
+
427
+ class TestCacheManagement:
428
+ """Test cases for cache management methods."""
429
+
430
+ @patch(f"{PATH}.OrgControlMapper")
431
+ @patch(f"{PATH}.boto3.Session")
432
+ @patch(f"{PATH}.os.path.exists")
433
+ def test_is_cache_valid_no_file(self, mock_exists, mock_session_class, mock_mapper_class):
434
+ """Test cache validation when file does not exist."""
435
+ mock_exists.return_value = False
436
+ mock_session = MagicMock()
437
+ mock_session.client.return_value = MagicMock()
438
+ mock_session_class.return_value = mock_session
439
+
440
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
441
+ assert integration._is_cache_valid() is False
442
+
443
+ @patch(f"{PATH}.OrgControlMapper")
444
+ @patch(f"{PATH}.boto3.Session")
445
+ @patch(f"{PATH}.os.path.exists")
446
+ @patch(f"{PATH}.os.path.getmtime")
447
+ @patch(f"{PATH}.time.time")
448
+ def test_is_cache_valid_expired(self, mock_time, mock_getmtime, mock_exists, mock_session_class, mock_mapper_class):
449
+ """Test cache validation when cache is expired."""
450
+ mock_exists.return_value = True
451
+ mock_time.return_value = 1000000
452
+ mock_getmtime.return_value = 1000000 - CACHE_TTL_SECONDS - 100
453
+ mock_session = MagicMock()
454
+ mock_session.client.return_value = MagicMock()
455
+ mock_session_class.return_value = mock_session
456
+
457
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
458
+ assert integration._is_cache_valid() is False
459
+
460
+ @patch(f"{PATH}.OrgControlMapper")
461
+ @patch(f"{PATH}.boto3.Session")
462
+ @patch(f"{PATH}.os.path.exists")
463
+ @patch(f"{PATH}.os.path.getmtime")
464
+ @patch(f"{PATH}.time.time")
465
+ def test_is_cache_valid_fresh(self, mock_time, mock_getmtime, mock_exists, mock_session_class, mock_mapper_class):
466
+ """Test cache validation when cache is fresh."""
467
+ mock_exists.return_value = True
468
+ mock_time.return_value = 1000000
469
+ mock_getmtime.return_value = 1000000 - 1000
470
+ mock_session = MagicMock()
471
+ mock_session.client.return_value = MagicMock()
472
+ mock_session_class.return_value = mock_session
473
+
474
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
475
+ assert integration._is_cache_valid() is True
476
+
477
+ @patch(f"{PATH}.OrgControlMapper")
478
+ @patch(f"{PATH}.boto3.Session")
479
+ def test_load_cached_data_success(self, mock_session_class, mock_mapper_class):
480
+ """Test loading cached data successfully."""
481
+ mock_session = MagicMock()
482
+ mock_session.client.return_value = MagicMock()
483
+ mock_session_class.return_value = mock_session
484
+
485
+ test_data = {
486
+ "Id": "o-testorg",
487
+ "accounts": [{"Id": "111111111111"}],
488
+ "organizational_units": [],
489
+ "service_control_policies": [],
490
+ }
491
+ mock_file = mock_open(read_data=json.dumps(test_data))
492
+
493
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
494
+
495
+ with patch("builtins.open", mock_file):
496
+ result = integration._load_cached_data()
497
+
498
+ assert result == test_data
499
+
500
+ @patch(f"{PATH}.OrgControlMapper")
501
+ @patch(f"{PATH}.boto3.Session")
502
+ def test_load_cached_data_json_error(self, mock_session_class, mock_mapper_class):
503
+ """Test loading cached data with JSON decode error."""
504
+ mock_session = MagicMock()
505
+ mock_session.client.return_value = MagicMock()
506
+ mock_session_class.return_value = mock_session
507
+
508
+ mock_file = mock_open(read_data="invalid json")
509
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
510
+
511
+ with patch("builtins.open", mock_file):
512
+ result = integration._load_cached_data()
513
+
514
+ assert result == {}
515
+
516
+ @patch(f"{PATH}.OrgControlMapper")
517
+ @patch(f"{PATH}.boto3.Session")
518
+ def test_load_cached_data_io_error(self, mock_session_class, mock_mapper_class):
519
+ """Test loading cached data with IO error."""
520
+ mock_session = MagicMock()
521
+ mock_session.client.return_value = MagicMock()
522
+ mock_session_class.return_value = mock_session
523
+
524
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
525
+
526
+ with patch("builtins.open", side_effect=IOError("File not found")):
527
+ result = integration._load_cached_data()
528
+
529
+ assert result == {}
530
+
531
+ @patch(f"{PATH}.OrgControlMapper")
532
+ @patch(f"{PATH}.boto3.Session")
533
+ @patch(f"{PATH}.os.makedirs")
534
+ def test_save_to_cache_success(self, mock_makedirs, mock_session_class, mock_mapper_class):
535
+ """Test saving data to cache successfully."""
536
+ mock_session = MagicMock()
537
+ mock_session.client.return_value = MagicMock()
538
+ mock_session_class.return_value = mock_session
539
+
540
+ test_data = {"Id": "o-testorg", "accounts": [], "organizational_units": [], "service_control_policies": []}
541
+ mock_file = mock_open()
542
+
543
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
544
+
545
+ with patch("builtins.open", mock_file):
546
+ integration._save_to_cache(test_data)
547
+
548
+ mock_makedirs.assert_called_once()
549
+ mock_file.assert_called_once()
550
+
551
+ @patch(f"{PATH}.OrgControlMapper")
552
+ @patch(f"{PATH}.boto3.Session")
553
+ @patch(f"{PATH}.os.makedirs")
554
+ def test_save_to_cache_io_error(self, mock_makedirs, mock_session_class, mock_mapper_class):
555
+ """Test saving data to cache with IO error."""
556
+ mock_session = MagicMock()
557
+ mock_session.client.return_value = MagicMock()
558
+ mock_session_class.return_value = mock_session
559
+
560
+ test_data = {"Id": "o-testorg"}
561
+
562
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
563
+
564
+ with patch("builtins.open", side_effect=IOError("Permission denied")):
565
+ integration._save_to_cache(test_data)
566
+
567
+
568
+ class TestFetchOrganizationData:
569
+ """Test cases for fetching organization data."""
570
+
571
+ @patch(f"{PATH}.OrgControlMapper")
572
+ @patch(f"{PATH}.boto3.Session")
573
+ def test_list_organizational_units_success(self, mock_session_class, mock_mapper_class):
574
+ """Test listing organizational units successfully."""
575
+ mock_client = MagicMock()
576
+ mock_paginator = MagicMock()
577
+ mock_paginator.paginate.side_effect = [
578
+ [{"OrganizationalUnits": [{"Id": "ou-1", "Name": "OU1"}]}],
579
+ [{"OrganizationalUnits": []}],
580
+ ]
581
+ mock_client.get_paginator.return_value = mock_paginator
582
+ mock_client.list_roots.return_value = {"Roots": [{"Id": "r-root"}]}
583
+
584
+ mock_session = MagicMock()
585
+ mock_session.client.return_value = mock_client
586
+ mock_session_class.return_value = mock_session
587
+
588
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
589
+ result = integration._list_organizational_units()
590
+
591
+ assert len(result) == 1
592
+ assert result[0]["Id"] == "ou-1"
593
+
594
+ @patch(f"{PATH}.OrgControlMapper")
595
+ @patch(f"{PATH}.boto3.Session")
596
+ def test_list_organizational_units_error(self, mock_session_class, mock_mapper_class):
597
+ """Test listing organizational units with error."""
598
+ mock_client = MagicMock()
599
+ error_response = {"Error": {"Code": "AccessDenied", "Message": "Access denied"}}
600
+ mock_client.list_roots.side_effect = ClientError(error_response, "ListRoots")
601
+
602
+ mock_session = MagicMock()
603
+ mock_session.client.return_value = mock_client
604
+ mock_session_class.return_value = mock_session
605
+
606
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
607
+ result = integration._list_organizational_units()
608
+
609
+ assert result == []
610
+
611
+ @patch(f"{PATH}.OrgControlMapper")
612
+ @patch(f"{PATH}.boto3.Session")
613
+ def test_list_service_control_policies_success(self, mock_session_class, mock_mapper_class):
614
+ """Test listing service control policies successfully."""
615
+ mock_client = MagicMock()
616
+ mock_paginator = MagicMock()
617
+ mock_paginator.paginate.return_value = [
618
+ {
619
+ "Policies": [
620
+ {"Id": "p-1", "Name": "Policy1"},
621
+ {"Id": "p-2", "Name": "Policy2"},
622
+ ]
623
+ }
624
+ ]
625
+ mock_client.get_paginator.return_value = mock_paginator
626
+ mock_client.describe_policy.side_effect = [
627
+ {"Policy": {"Id": "p-1", "Name": "Policy1", "Content": "{}"}},
628
+ {"Policy": {"Id": "p-2", "Name": "Policy2", "Content": "{}"}},
629
+ ]
630
+
631
+ mock_session = MagicMock()
632
+ mock_session.client.return_value = mock_client
633
+ mock_session_class.return_value = mock_session
634
+
635
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
636
+ result = integration._list_service_control_policies()
637
+
638
+ assert len(result) == 2
639
+ assert result[0]["Id"] == "p-1"
640
+
641
+ @patch(f"{PATH}.OrgControlMapper")
642
+ @patch(f"{PATH}.boto3.Session")
643
+ def test_list_service_control_policies_error(self, mock_session_class, mock_mapper_class):
644
+ """Test listing service control policies with error."""
645
+ mock_client = MagicMock()
646
+ error_response = {"Error": {"Code": "AccessDenied", "Message": "Access denied"}}
647
+ mock_paginator = MagicMock()
648
+ mock_paginator.paginate.side_effect = ClientError(error_response, "ListPolicies")
649
+ mock_client.get_paginator.return_value = mock_paginator
650
+
651
+ mock_session = MagicMock()
652
+ mock_session.client.return_value = mock_client
653
+ mock_session_class.return_value = mock_session
654
+
655
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
656
+ result = integration._list_service_control_policies()
657
+
658
+ assert result == []
659
+
660
+ @patch(f"{PATH}.OrgControlMapper")
661
+ @patch(f"{PATH}.boto3.Session")
662
+ def test_fetch_fresh_org_data_success(self, mock_session_class, mock_mapper_class):
663
+ """Test fetching fresh organization data successfully."""
664
+ mock_client = MagicMock()
665
+ mock_client.describe_organization.return_value = {
666
+ "Organization": {
667
+ "Id": "o-testorg",
668
+ "Arn": "arn:aws:organizations::123456789012:organization/o-testorg",
669
+ "MasterAccountId": "123456789012",
670
+ }
671
+ }
672
+ mock_paginator = MagicMock()
673
+ mock_paginator.paginate.return_value = [
674
+ {"Accounts": [{"Id": "111111111111", "Name": "Account1", "Status": "ACTIVE"}]}
675
+ ]
676
+ mock_client.get_paginator.return_value = mock_paginator
677
+
678
+ mock_session = MagicMock()
679
+ mock_session.client.return_value = mock_client
680
+ mock_session_class.return_value = mock_session
681
+
682
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
683
+ integration._list_organizational_units = Mock(return_value=[{"Id": "ou-1"}])
684
+ integration._list_service_control_policies = Mock(return_value=[{"Id": "p-1"}])
685
+
686
+ result = integration._fetch_fresh_org_data()
687
+
688
+ assert "Id" in result
689
+ assert result["Id"] == "o-testorg"
690
+ assert "accounts" in result
691
+ assert len(result["accounts"]) == 1
692
+ assert "organizational_units" in result
693
+ assert "service_control_policies" in result
694
+
695
+ @patch(f"{PATH}.OrgControlMapper")
696
+ @patch(f"{PATH}.boto3.Session")
697
+ def test_fetch_fresh_org_data_error(self, mock_session_class, mock_mapper_class):
698
+ """Test fetching fresh organization data with error."""
699
+ mock_client = MagicMock()
700
+ error_response = {"Error": {"Code": "AccessDenied", "Message": "Access denied"}}
701
+ mock_client.describe_organization.side_effect = ClientError(error_response, "DescribeOrganization")
702
+
703
+ mock_session = MagicMock()
704
+ mock_session.client.return_value = mock_client
705
+ mock_session_class.return_value = mock_session
706
+
707
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
708
+ result = integration._fetch_fresh_org_data()
709
+
710
+ assert result == {}
711
+
712
+ @patch(f"{PATH}.OrgControlMapper")
713
+ @patch(f"{PATH}.boto3.Session")
714
+ def test_fetch_compliance_data_from_cache(self, mock_session_class, mock_mapper_class):
715
+ """Test fetching compliance data from cache."""
716
+ mock_session = MagicMock()
717
+ mock_session.client.return_value = MagicMock()
718
+ mock_session_class.return_value = mock_session
719
+
720
+ cached_data = {"Id": "o-testorg", "accounts": [], "organizational_units": [], "service_control_policies": []}
721
+
722
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
723
+ integration._is_cache_valid = Mock(return_value=True)
724
+ integration._load_cached_data = Mock(return_value=cached_data)
725
+
726
+ result = integration.fetch_compliance_data()
727
+
728
+ assert result == [cached_data]
729
+ assert integration.raw_org_data == cached_data
730
+
731
+ @patch(f"{PATH}.OrgControlMapper")
732
+ @patch(f"{PATH}.boto3.Session")
733
+ def test_fetch_compliance_data_force_refresh(self, mock_session_class, mock_mapper_class):
734
+ """Test fetching compliance data with force refresh."""
735
+ mock_session = MagicMock()
736
+ mock_session.client.return_value = MagicMock()
737
+ mock_session_class.return_value = mock_session
738
+
739
+ fresh_data = {"Id": "o-freshorg", "accounts": [], "organizational_units": [], "service_control_policies": []}
740
+
741
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1", force_refresh=True)
742
+ integration._fetch_fresh_org_data = Mock(return_value=fresh_data)
743
+ integration._save_to_cache = Mock()
744
+
745
+ result = integration.fetch_compliance_data()
746
+
747
+ assert result == [fresh_data]
748
+ assert integration.raw_org_data == fresh_data
749
+ integration._save_to_cache.assert_called_once_with(fresh_data)
750
+
751
+ @patch(f"{PATH}.OrgControlMapper")
752
+ @patch(f"{PATH}.boto3.Session")
753
+ def test_create_compliance_item(self, mock_session_class, mock_mapper_class):
754
+ """Test creating compliance item from raw data."""
755
+ mock_session = MagicMock()
756
+ mock_session.client.return_value = MagicMock()
757
+ mock_session_class.return_value = mock_session
758
+
759
+ mock_mapper = MagicMock()
760
+ mock_mapper.assess_organization_compliance.return_value = {"AC-1": "PASS"}
761
+ mock_mapper_class.return_value = mock_mapper
762
+
763
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
764
+
765
+ raw_data = {"Id": "o-testorg", "accounts": [], "organizational_units": [], "service_control_policies": []}
766
+
767
+ result = integration.create_compliance_item(raw_data)
768
+
769
+ assert isinstance(result, OrgComplianceItem)
770
+ assert result.org_data == raw_data
771
+
772
+
773
+ class TestEvidenceCollection:
774
+ """Test cases for evidence collection methods."""
775
+
776
+ @patch(f"{PATH}.OrgControlMapper")
777
+ @patch(f"{PATH}.boto3.Session")
778
+ @patch(f"{PATH}.get_current_datetime")
779
+ def test_collect_org_evidence_as_attachments(self, mock_get_datetime, mock_session_class, mock_mapper_class):
780
+ """Test collecting evidence as SSP attachments."""
781
+ mock_session = MagicMock()
782
+ mock_session.client.return_value = MagicMock()
783
+ mock_session_class.return_value = mock_session
784
+
785
+ mock_get_datetime.return_value = "2023-12-01"
786
+
787
+ integration = AWSOrganizationsEvidenceIntegration(
788
+ plan_id=123, region="us-east-1", collect_evidence=True, evidence_as_attachments=True
789
+ )
790
+
791
+ integration.raw_org_data = {
792
+ "Id": "o-testorg",
793
+ "accounts": [],
794
+ "organizational_units": [],
795
+ "service_control_policies": [],
796
+ }
797
+ integration._create_ssp_attachment = Mock()
798
+
799
+ integration._collect_org_evidence()
800
+
801
+ integration._create_ssp_attachment.assert_called_once_with("2023-12-01")
802
+
803
+ @patch(f"{PATH}.OrgControlMapper")
804
+ @patch(f"{PATH}.boto3.Session")
805
+ @patch(f"{PATH}.get_current_datetime")
806
+ def test_collect_org_evidence_as_records(self, mock_get_datetime, mock_session_class, mock_mapper_class):
807
+ """Test collecting evidence as evidence records."""
808
+ mock_session = MagicMock()
809
+ mock_session.client.return_value = MagicMock()
810
+ mock_session_class.return_value = mock_session
811
+
812
+ mock_get_datetime.return_value = "2023-12-01"
813
+
814
+ integration = AWSOrganizationsEvidenceIntegration(
815
+ plan_id=123, region="us-east-1", collect_evidence=True, evidence_as_attachments=False
816
+ )
817
+
818
+ integration.raw_org_data = {
819
+ "Id": "o-testorg",
820
+ "accounts": [],
821
+ "organizational_units": [],
822
+ "service_control_policies": [],
823
+ }
824
+ integration._create_evidence_record = Mock()
825
+
826
+ integration._collect_org_evidence()
827
+
828
+ integration._create_evidence_record.assert_called_once_with("2023-12-01")
829
+
830
+ @patch(f"{PATH}.OrgControlMapper")
831
+ @patch(f"{PATH}.boto3.Session")
832
+ def test_collect_org_evidence_no_data(self, mock_session_class, mock_mapper_class):
833
+ """Test collecting evidence when no data is available."""
834
+ mock_session = MagicMock()
835
+ mock_session.client.return_value = MagicMock()
836
+ mock_session_class.return_value = mock_session
837
+
838
+ integration = AWSOrganizationsEvidenceIntegration(
839
+ plan_id=123, region="us-east-1", collect_evidence=True, evidence_as_attachments=True
840
+ )
841
+
842
+ integration.raw_org_data = {}
843
+
844
+ integration._collect_org_evidence()
845
+
846
+ @patch(f"{PATH}.OrgControlMapper")
847
+ @patch(f"{PATH}.boto3.Session")
848
+ @patch(f"{PATH}.Api")
849
+ @patch(f"{PATH}.File")
850
+ def test_create_ssp_attachment_success(
851
+ self, mock_file_class, mock_api_class, mock_session_class, mock_mapper_class
852
+ ):
853
+ """Test creating SSP attachment successfully."""
854
+ mock_session = MagicMock()
855
+ mock_session.client.return_value = MagicMock()
856
+ mock_session_class.return_value = mock_session
857
+
858
+ mock_mapper = MagicMock()
859
+ mock_mapper.assess_organization_compliance.return_value = {"AC-1": "PASS", "PM-9": "PASS"}
860
+ mock_mapper_class.return_value = mock_mapper
861
+
862
+ mock_api = MagicMock()
863
+ mock_api_class.return_value = mock_api
864
+
865
+ mock_file_class.upload_file_to_regscale.return_value = True
866
+
867
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
868
+ integration.raw_org_data = {
869
+ "Id": "o-testorg",
870
+ "accounts": [],
871
+ "organizational_units": [],
872
+ "service_control_policies": [],
873
+ }
874
+
875
+ integration._create_ssp_attachment("2023-12-01")
876
+
877
+ mock_file_class.upload_file_to_regscale.assert_called_once()
878
+ call_args = mock_file_class.upload_file_to_regscale.call_args[1]
879
+ assert call_args["parent_id"] == 123
880
+ assert call_args["parent_module"] == "securityplans"
881
+ assert "org_evidence_" in call_args["file_name"]
882
+ assert "aws,organizations,governance,automated" == call_args["tags"]
883
+
884
+ @patch(f"{PATH}.OrgControlMapper")
885
+ @patch(f"{PATH}.boto3.Session")
886
+ @patch(f"{PATH}.Api")
887
+ @patch(f"{PATH}.File")
888
+ def test_create_ssp_attachment_failure(
889
+ self, mock_file_class, mock_api_class, mock_session_class, mock_mapper_class
890
+ ):
891
+ """Test creating SSP attachment with failure."""
892
+ mock_session = MagicMock()
893
+ mock_session.client.return_value = MagicMock()
894
+ mock_session_class.return_value = mock_session
895
+
896
+ mock_mapper = MagicMock()
897
+ mock_mapper.assess_organization_compliance.return_value = {}
898
+ mock_mapper_class.return_value = mock_mapper
899
+
900
+ mock_api = MagicMock()
901
+ mock_api_class.return_value = mock_api
902
+
903
+ mock_file_class.upload_file_to_regscale.return_value = False
904
+
905
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
906
+ integration.raw_org_data = {
907
+ "Id": "o-testorg",
908
+ "accounts": [],
909
+ "organizational_units": [],
910
+ "service_control_policies": [],
911
+ }
912
+
913
+ integration._create_ssp_attachment("2023-12-01")
914
+
915
+ @patch(f"{PATH}.OrgControlMapper")
916
+ @patch(f"{PATH}.boto3.Session")
917
+ @patch(f"{PATH}.Api")
918
+ @patch(f"{PATH}.File")
919
+ def test_create_ssp_attachment_exception(
920
+ self, mock_file_class, mock_api_class, mock_session_class, mock_mapper_class
921
+ ):
922
+ """Test creating SSP attachment with exception."""
923
+ mock_session = MagicMock()
924
+ mock_session.client.return_value = MagicMock()
925
+ mock_session_class.return_value = mock_session
926
+
927
+ mock_mapper = MagicMock()
928
+ mock_mapper.assess_organization_compliance.side_effect = Exception("Test error")
929
+ mock_mapper_class.return_value = mock_mapper
930
+
931
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
932
+ integration.raw_org_data = {
933
+ "Id": "o-testorg",
934
+ "accounts": [],
935
+ "organizational_units": [],
936
+ "service_control_policies": [],
937
+ }
938
+
939
+ integration._create_ssp_attachment("2023-12-01")
940
+
941
+ @patch(f"{PATH}.OrgControlMapper")
942
+ @patch(f"{PATH}.boto3.Session")
943
+ @patch(f"{PATH}.Evidence")
944
+ def test_create_evidence_record_success(self, mock_evidence_class, mock_session_class, mock_mapper_class):
945
+ """Test creating evidence record successfully."""
946
+ mock_session = MagicMock()
947
+ mock_session.client.return_value = MagicMock()
948
+ mock_session_class.return_value = mock_session
949
+
950
+ mock_mapper = MagicMock()
951
+ mock_mapper.assess_organization_compliance.return_value = {"AC-1": "PASS", "PM-9": "FAIL"}
952
+ mock_mapper.get_control_description.side_effect = lambda x: f"Description for {x}"
953
+ mock_mapper_class.return_value = mock_mapper
954
+
955
+ mock_evidence = MagicMock()
956
+ mock_evidence.id = 999
957
+ mock_evidence_instance = MagicMock()
958
+ mock_evidence_instance.create.return_value = mock_evidence
959
+ mock_evidence_class.return_value = mock_evidence_instance
960
+
961
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1", evidence_frequency=90)
962
+ integration.raw_org_data = {
963
+ "Id": "o-testorg",
964
+ "accounts": [],
965
+ "organizational_units": [],
966
+ "service_control_policies": [],
967
+ }
968
+ integration._upload_evidence_file = Mock()
969
+ integration._link_evidence_to_ssp = Mock()
970
+
971
+ integration._create_evidence_record("2023-12-01")
972
+
973
+ mock_evidence_instance.create.assert_called_once()
974
+ integration._upload_evidence_file.assert_called_once_with(999, "2023-12-01")
975
+ integration._link_evidence_to_ssp.assert_called_once_with(999)
976
+
977
+ @patch(f"{PATH}.OrgControlMapper")
978
+ @patch(f"{PATH}.boto3.Session")
979
+ @patch(f"{PATH}.Evidence")
980
+ def test_create_evidence_record_creation_failure(self, mock_evidence_class, mock_session_class, mock_mapper_class):
981
+ """Test creating evidence record when creation fails."""
982
+ mock_session = MagicMock()
983
+ mock_session.client.return_value = MagicMock()
984
+ mock_session_class.return_value = mock_session
985
+
986
+ mock_mapper = MagicMock()
987
+ mock_mapper.assess_organization_compliance.return_value = {}
988
+ mock_mapper_class.return_value = mock_mapper
989
+
990
+ mock_evidence_instance = MagicMock()
991
+ mock_evidence_instance.create.return_value = None
992
+ mock_evidence_class.return_value = mock_evidence_instance
993
+
994
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
995
+ integration.raw_org_data = {
996
+ "Id": "o-testorg",
997
+ "accounts": [],
998
+ "organizational_units": [],
999
+ "service_control_policies": [],
1000
+ }
1001
+
1002
+ integration._create_evidence_record("2023-12-01")
1003
+
1004
+ mock_evidence_instance.create.assert_called_once()
1005
+
1006
+ @patch(f"{PATH}.OrgControlMapper")
1007
+ @patch(f"{PATH}.boto3.Session")
1008
+ @patch(f"{PATH}.Evidence")
1009
+ def test_create_evidence_record_exception(self, mock_evidence_class, mock_session_class, mock_mapper_class):
1010
+ """Test creating evidence record with exception."""
1011
+ mock_session = MagicMock()
1012
+ mock_session.client.return_value = MagicMock()
1013
+ mock_session_class.return_value = mock_session
1014
+
1015
+ mock_mapper = MagicMock()
1016
+ mock_mapper.assess_organization_compliance.side_effect = Exception("Test error")
1017
+ mock_mapper_class.return_value = mock_mapper
1018
+
1019
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
1020
+ integration.raw_org_data = {
1021
+ "Id": "o-testorg",
1022
+ "accounts": [],
1023
+ "organizational_units": [],
1024
+ "service_control_policies": [],
1025
+ }
1026
+
1027
+ integration._create_evidence_record("2023-12-01")
1028
+
1029
+ @patch(f"{PATH}.OrgControlMapper")
1030
+ @patch(f"{PATH}.boto3.Session")
1031
+ def test_build_evidence_description(self, mock_session_class, mock_mapper_class):
1032
+ """Test building evidence description."""
1033
+ mock_session = MagicMock()
1034
+ mock_session.client.return_value = MagicMock()
1035
+ mock_session_class.return_value = mock_session
1036
+
1037
+ mock_mapper = MagicMock()
1038
+ mock_mapper.assess_organization_compliance.return_value = {"AC-1": "PASS", "PM-9": "FAIL", "AC-2": "PASS"}
1039
+ mock_mapper.get_control_description.side_effect = lambda x: f"{x} Description"
1040
+ mock_mapper_class.return_value = mock_mapper
1041
+
1042
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
1043
+ integration.raw_org_data = {
1044
+ "accounts": [{"Id": "111111111111"}],
1045
+ "organizational_units": [{"Id": "ou-1"}],
1046
+ "service_control_policies": [{"Id": "p-1"}],
1047
+ }
1048
+
1049
+ result = integration._build_evidence_description("2023-12-01")
1050
+
1051
+ assert "AWS Organizations Governance Evidence" in result
1052
+ assert "2023-12-01" in result
1053
+ assert "AC-1" in result
1054
+ assert "PM-9" in result
1055
+ assert "AC-2" in result
1056
+
1057
+ @patch(f"{PATH}.OrgControlMapper")
1058
+ @patch(f"{PATH}.boto3.Session")
1059
+ @patch(f"{PATH}.Api")
1060
+ @patch(f"{PATH}.File")
1061
+ def test_upload_evidence_file_success(self, mock_file_class, mock_api_class, mock_session_class, mock_mapper_class):
1062
+ """Test uploading evidence file successfully."""
1063
+ mock_session = MagicMock()
1064
+ mock_session.client.return_value = MagicMock()
1065
+ mock_session_class.return_value = mock_session
1066
+
1067
+ mock_mapper = MagicMock()
1068
+ mock_mapper.assess_organization_compliance.return_value = {"AC-1": "PASS"}
1069
+ mock_mapper_class.return_value = mock_mapper
1070
+
1071
+ mock_api = MagicMock()
1072
+ mock_api_class.return_value = mock_api
1073
+
1074
+ mock_file_class.upload_file_to_regscale.return_value = True
1075
+
1076
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
1077
+ integration.raw_org_data = {
1078
+ "Id": "o-testorg",
1079
+ "accounts": [],
1080
+ "organizational_units": [],
1081
+ "service_control_policies": [],
1082
+ }
1083
+
1084
+ integration._upload_evidence_file(999, "2023-12-01")
1085
+
1086
+ mock_file_class.upload_file_to_regscale.assert_called_once()
1087
+ call_args = mock_file_class.upload_file_to_regscale.call_args[1]
1088
+ assert call_args["parent_id"] == 999
1089
+ assert call_args["parent_module"] == "evidence"
1090
+ assert "org_evidence_" in call_args["file_name"]
1091
+ assert "aws,organizations,governance" == call_args["tags"]
1092
+
1093
+ @patch(f"{PATH}.OrgControlMapper")
1094
+ @patch(f"{PATH}.boto3.Session")
1095
+ @patch(f"{PATH}.Api")
1096
+ @patch(f"{PATH}.File")
1097
+ def test_upload_evidence_file_failure(self, mock_file_class, mock_api_class, mock_session_class, mock_mapper_class):
1098
+ """Test uploading evidence file with failure."""
1099
+ mock_session = MagicMock()
1100
+ mock_session.client.return_value = MagicMock()
1101
+ mock_session_class.return_value = mock_session
1102
+
1103
+ mock_mapper = MagicMock()
1104
+ mock_mapper.assess_organization_compliance.return_value = {}
1105
+ mock_mapper_class.return_value = mock_mapper
1106
+
1107
+ mock_api = MagicMock()
1108
+ mock_api_class.return_value = mock_api
1109
+
1110
+ mock_file_class.upload_file_to_regscale.return_value = False
1111
+
1112
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
1113
+ integration.raw_org_data = {
1114
+ "Id": "o-testorg",
1115
+ "accounts": [],
1116
+ "organizational_units": [],
1117
+ "service_control_policies": [],
1118
+ }
1119
+
1120
+ integration._upload_evidence_file(999, "2023-12-01")
1121
+
1122
+ @patch(f"{PATH}.OrgControlMapper")
1123
+ @patch(f"{PATH}.boto3.Session")
1124
+ @patch(f"{PATH}.Api")
1125
+ @patch(f"{PATH}.File")
1126
+ def test_upload_evidence_file_exception(
1127
+ self, mock_file_class, mock_api_class, mock_session_class, mock_mapper_class
1128
+ ):
1129
+ """Test uploading evidence file with exception."""
1130
+ mock_session = MagicMock()
1131
+ mock_session.client.return_value = MagicMock()
1132
+ mock_session_class.return_value = mock_session
1133
+
1134
+ mock_mapper = MagicMock()
1135
+ mock_mapper.assess_organization_compliance.side_effect = Exception("Test error")
1136
+ mock_mapper_class.return_value = mock_mapper
1137
+
1138
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
1139
+ integration.raw_org_data = {
1140
+ "Id": "o-testorg",
1141
+ "accounts": [],
1142
+ "organizational_units": [],
1143
+ "service_control_policies": [],
1144
+ }
1145
+
1146
+ integration._upload_evidence_file(999, "2023-12-01")
1147
+
1148
+ @patch(f"{PATH}.OrgControlMapper")
1149
+ @patch(f"{PATH}.boto3.Session")
1150
+ @patch(f"{PATH}.EvidenceMapping")
1151
+ def test_link_evidence_to_ssp_success(self, mock_mapping_class, mock_session_class, mock_mapper_class):
1152
+ """Test linking evidence to SSP successfully."""
1153
+ mock_session = MagicMock()
1154
+ mock_session.client.return_value = MagicMock()
1155
+ mock_session_class.return_value = mock_session
1156
+
1157
+ mock_mapping = MagicMock()
1158
+ mock_mapping_class.return_value = mock_mapping
1159
+
1160
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
1161
+
1162
+ integration._link_evidence_to_ssp(999)
1163
+
1164
+ mock_mapping_class.assert_called_once_with(evidenceID=999, mappedID=123, mappingType="securityplans")
1165
+ mock_mapping.create.assert_called_once()
1166
+
1167
+ @patch(f"{PATH}.OrgControlMapper")
1168
+ @patch(f"{PATH}.boto3.Session")
1169
+ @patch(f"{PATH}.EvidenceMapping")
1170
+ def test_link_evidence_to_ssp_failure(self, mock_mapping_class, mock_session_class, mock_mapper_class):
1171
+ """Test linking evidence to SSP with failure."""
1172
+ mock_session = MagicMock()
1173
+ mock_session.client.return_value = MagicMock()
1174
+ mock_session_class.return_value = mock_session
1175
+
1176
+ mock_mapping = MagicMock()
1177
+ mock_mapping.create.side_effect = Exception("Test error")
1178
+ mock_mapping_class.return_value = mock_mapping
1179
+
1180
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1")
1181
+
1182
+ integration._link_evidence_to_ssp(999)
1183
+
1184
+
1185
+ class TestSyncCompliance:
1186
+ """Test cases for sync_compliance method."""
1187
+
1188
+ @patch(f"{PATH}.OrgControlMapper")
1189
+ @patch(f"{PATH}.boto3.Session")
1190
+ def test_sync_compliance_with_evidence_collection(self, mock_session_class, mock_mapper_class):
1191
+ """Test sync_compliance with evidence collection enabled."""
1192
+ mock_session = MagicMock()
1193
+ mock_session.client.return_value = MagicMock()
1194
+ mock_session_class.return_value = mock_session
1195
+
1196
+ mock_mapper = MagicMock()
1197
+ mock_mapper.assess_organization_compliance.return_value = {"AC-1": "PASS"}
1198
+ mock_mapper_class.return_value = mock_mapper
1199
+
1200
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1", collect_evidence=True)
1201
+ integration.fetch_compliance_data = Mock(
1202
+ return_value=[
1203
+ {"Id": "o-testorg", "accounts": [], "organizational_units": [], "service_control_policies": []}
1204
+ ]
1205
+ )
1206
+ integration._collect_org_evidence = Mock()
1207
+
1208
+ with patch.object(integration.__class__.__bases__[0], "sync_compliance"):
1209
+ integration.sync_compliance()
1210
+
1211
+ integration._collect_org_evidence.assert_called_once()
1212
+
1213
+ @patch(f"{PATH}.OrgControlMapper")
1214
+ @patch(f"{PATH}.boto3.Session")
1215
+ def test_sync_compliance_without_evidence_collection(self, mock_session_class, mock_mapper_class):
1216
+ """Test sync_compliance without evidence collection."""
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_organization_compliance.return_value = {"AC-1": "PASS"}
1223
+ mock_mapper_class.return_value = mock_mapper
1224
+
1225
+ integration = AWSOrganizationsEvidenceIntegration(plan_id=123, region="us-east-1", collect_evidence=False)
1226
+ integration.fetch_compliance_data = Mock(
1227
+ return_value=[
1228
+ {"Id": "o-testorg", "accounts": [], "organizational_units": [], "service_control_policies": []}
1229
+ ]
1230
+ )
1231
+ integration._collect_org_evidence = Mock()
1232
+
1233
+ with patch.object(integration.__class__.__bases__[0], "sync_compliance"):
1234
+ integration.sync_compliance()
1235
+
1236
+ integration._collect_org_evidence.assert_not_called()
1237
+
1238
+
1239
+ if __name__ == "__main__":
1240
+ pytest.main([__file__, "-v"])