regscale-cli 6.25.1.0__py3-none-any.whl → 6.27.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 (146) hide show
  1. regscale/_version.py +1 -1
  2. regscale/airflow/hierarchy.py +2 -2
  3. regscale/core/app/application.py +19 -4
  4. regscale/core/app/internal/evidence.py +419 -2
  5. regscale/core/app/internal/login.py +0 -1
  6. regscale/core/app/utils/catalog_utils/common.py +1 -1
  7. regscale/dev/code_gen.py +24 -20
  8. regscale/integrations/commercial/jira.py +367 -126
  9. regscale/integrations/commercial/qualys/__init__.py +7 -8
  10. regscale/integrations/commercial/qualys/scanner.py +8 -3
  11. regscale/integrations/commercial/sicura/api.py +14 -13
  12. regscale/integrations/commercial/sicura/commands.py +8 -2
  13. regscale/integrations/commercial/sicura/scanner.py +49 -39
  14. regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
  15. regscale/integrations/commercial/synqly/assets.py +17 -0
  16. regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
  17. regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
  18. regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
  19. regscale/integrations/commercial/tenablev2/commands.py +142 -1
  20. regscale/integrations/commercial/tenablev2/scanner.py +0 -1
  21. regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
  22. regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
  23. regscale/integrations/commercial/wizv2/click.py +64 -79
  24. regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
  25. regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
  26. regscale/integrations/commercial/wizv2/compliance_report.py +161 -165
  27. regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
  28. regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
  29. regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
  30. regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
  31. regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
  32. regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
  33. regscale/integrations/commercial/wizv2/issue.py +1 -1
  34. regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
  35. regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
  36. regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
  37. regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
  38. regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
  39. regscale/integrations/commercial/wizv2/reports.py +1 -1
  40. regscale/integrations/commercial/wizv2/sbom.py +1 -1
  41. regscale/integrations/commercial/wizv2/scanner.py +39 -99
  42. regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
  43. regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
  44. regscale/integrations/commercial/wizv2/variables.py +89 -3
  45. regscale/integrations/compliance_integration.py +60 -41
  46. regscale/integrations/control_matcher.py +377 -0
  47. regscale/integrations/due_date_handler.py +14 -8
  48. regscale/integrations/milestone_manager.py +291 -0
  49. regscale/integrations/public/__init__.py +1 -0
  50. regscale/integrations/public/cci_importer.py +37 -38
  51. regscale/integrations/public/fedramp/click.py +60 -2
  52. regscale/integrations/public/fedramp/docx_parser.py +10 -1
  53. regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
  54. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  55. regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
  56. regscale/integrations/scanner_integration.py +277 -153
  57. regscale/models/integration_models/cisa_kev_data.json +282 -9
  58. regscale/models/integration_models/nexpose.py +36 -10
  59. regscale/models/integration_models/qualys.py +3 -4
  60. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  61. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
  62. regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
  63. regscale/models/locking.py +12 -8
  64. regscale/models/platform.py +1 -2
  65. regscale/models/regscale_models/control_implementation.py +47 -22
  66. regscale/models/regscale_models/issue.py +256 -95
  67. regscale/models/regscale_models/milestone.py +1 -1
  68. regscale/models/regscale_models/regscale_model.py +6 -1
  69. regscale/templates/__init__.py +0 -0
  70. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/METADATA +1 -17
  71. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/RECORD +145 -65
  72. tests/regscale/integrations/commercial/__init__.py +0 -0
  73. tests/regscale/integrations/commercial/conftest.py +28 -0
  74. tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
  75. tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
  76. tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
  77. tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
  78. tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
  79. tests/regscale/integrations/commercial/test_aws.py +3731 -0
  80. tests/regscale/integrations/commercial/test_burp.py +48 -0
  81. tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
  82. tests/regscale/integrations/commercial/test_dependabot.py +341 -0
  83. tests/regscale/integrations/commercial/test_gcp.py +1543 -0
  84. tests/regscale/integrations/commercial/test_gitlab.py +549 -0
  85. tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
  86. tests/regscale/integrations/commercial/test_jira.py +2204 -0
  87. tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
  88. tests/regscale/integrations/commercial/test_okta.py +1228 -0
  89. tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
  90. tests/regscale/integrations/commercial/test_sicura.py +350 -0
  91. tests/regscale/integrations/commercial/test_snow.py +423 -0
  92. tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
  93. tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
  94. tests/regscale/integrations/commercial/test_stig.py +33 -0
  95. tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
  96. tests/regscale/integrations/commercial/test_stigv2.py +406 -0
  97. tests/regscale/integrations/commercial/test_wiz.py +1365 -0
  98. tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
  99. tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
  100. tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
  101. tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
  102. tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
  103. tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
  104. tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
  105. tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
  106. tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
  107. tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
  108. tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
  109. tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
  110. tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
  111. tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
  112. tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
  113. tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
  114. tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
  115. tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
  116. tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
  117. tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
  118. tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
  119. tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
  120. tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
  121. tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
  122. tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
  123. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
  124. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1394 -0
  125. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
  126. tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
  127. tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
  128. tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
  129. tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
  130. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +1132 -0
  131. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +519 -0
  132. tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
  133. tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
  134. tests/regscale/integrations/public/fedramp/__init__.py +1 -0
  135. tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
  136. tests/regscale/integrations/public/test_fedramp.py +301 -0
  137. tests/regscale/integrations/test_control_matcher.py +1397 -0
  138. tests/regscale/integrations/test_control_matching.py +155 -0
  139. tests/regscale/integrations/test_milestone_manager.py +408 -0
  140. tests/regscale/models/test_issue.py +378 -1
  141. regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
  142. /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
  143. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/LICENSE +0 -0
  144. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/WHEEL +0 -0
  145. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/entry_points.txt +0 -0
  146. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,805 @@
1
+ """
2
+ Comprehensive unit tests for policy_assessment.py module.
3
+
4
+ This test suite covers:
5
+ - WizDataCache: caching, TTL validation, file operations
6
+ - WizApiClient: async fetching, requests-based fetching, pagination, error handling
7
+ - PolicyAssessmentFetcher: main fetching logic, filtering, data cleaning
8
+ """
9
+
10
+ import json
11
+ import logging
12
+ import os
13
+ import tempfile
14
+ import unittest
15
+ from datetime import datetime, timedelta
16
+ from unittest.mock import MagicMock, Mock, patch, call
17
+
18
+ import pytest
19
+ import requests
20
+
21
+ from regscale.integrations.commercial.wizv2.fetchers.policy_assessment import (
22
+ WizDataCache,
23
+ WizApiClient,
24
+ PolicyAssessmentFetcher,
25
+ )
26
+ from regscale.integrations.commercial.wizv2.core.constants import WizVulnerabilityType, WIZ_POLICY_QUERY
27
+
28
+ logger = logging.getLogger("regscale")
29
+
30
+
31
+ class TestWizDataCache(unittest.TestCase):
32
+ """Test cases for WizDataCache class."""
33
+
34
+ def setUp(self):
35
+ """Set up test fixtures."""
36
+ self.temp_dir = tempfile.mkdtemp()
37
+ self.wiz_project_id = "test-project-123"
38
+ self.framework_id = "wf-id-4"
39
+
40
+ def tearDown(self):
41
+ """Clean up test artifacts."""
42
+ import shutil
43
+
44
+ if os.path.exists(self.temp_dir):
45
+ shutil.rmtree(self.temp_dir)
46
+
47
+ def test_init_default_values(self):
48
+ """Test WizDataCache initialization with default values."""
49
+ cache = WizDataCache(self.temp_dir)
50
+ self.assertEqual(cache.cache_dir, self.temp_dir)
51
+ self.assertEqual(cache.cache_duration_minutes, 0)
52
+ self.assertFalse(cache.force_refresh)
53
+
54
+ def test_init_custom_values(self):
55
+ """Test WizDataCache initialization with custom values."""
56
+ cache = WizDataCache(self.temp_dir, cache_duration_minutes=60)
57
+ self.assertEqual(cache.cache_dir, self.temp_dir)
58
+ self.assertEqual(cache.cache_duration_minutes, 60)
59
+ self.assertFalse(cache.force_refresh)
60
+
61
+ def test_get_cache_file_path(self):
62
+ """Test cache file path generation."""
63
+ cache = WizDataCache(self.temp_dir)
64
+ cache_path = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
65
+
66
+ expected_filename = f"policy_assessments_{self.wiz_project_id}_{self.framework_id}.json"
67
+ expected_path = os.path.join(self.temp_dir, expected_filename)
68
+
69
+ self.assertEqual(cache_path, expected_path)
70
+ self.assertTrue(os.path.exists(self.temp_dir))
71
+
72
+ def test_is_cache_valid_disabled(self):
73
+ """Test cache validation when caching is disabled (duration = 0)."""
74
+ cache = WizDataCache(self.temp_dir, cache_duration_minutes=0)
75
+ cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
76
+
77
+ self.assertFalse(cache.is_cache_valid(cache_file))
78
+
79
+ def test_is_cache_valid_force_refresh(self):
80
+ """Test cache validation when force_refresh is enabled."""
81
+ cache = WizDataCache(self.temp_dir, cache_duration_minutes=60)
82
+ cache.force_refresh = True
83
+ cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
84
+
85
+ self.assertFalse(cache.is_cache_valid(cache_file))
86
+
87
+ def test_is_cache_valid_file_not_exists(self):
88
+ """Test cache validation when cache file does not exist."""
89
+ cache = WizDataCache(self.temp_dir, cache_duration_minutes=60)
90
+ cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
91
+
92
+ self.assertFalse(cache.is_cache_valid(cache_file))
93
+
94
+ def test_is_cache_valid_fresh_file(self):
95
+ """Test cache validation with a fresh cache file."""
96
+ cache = WizDataCache(self.temp_dir, cache_duration_minutes=60)
97
+ cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
98
+
99
+ # Create a fresh cache file
100
+ with open(cache_file, "w", encoding="utf-8") as f:
101
+ json.dump({"timestamp": datetime.now().isoformat(), "nodes": []}, f)
102
+
103
+ self.assertTrue(cache.is_cache_valid(cache_file))
104
+
105
+ def test_is_cache_valid_expired_file(self):
106
+ """Test cache validation with an expired cache file."""
107
+ cache = WizDataCache(self.temp_dir, cache_duration_minutes=1)
108
+ cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
109
+
110
+ # Create cache file and modify timestamp to be old
111
+ with open(cache_file, "w", encoding="utf-8") as f:
112
+ json.dump({"timestamp": datetime.now().isoformat(), "nodes": []}, f)
113
+
114
+ # Modify file time to be 2 minutes old
115
+ old_time = (datetime.now() - timedelta(minutes=2)).timestamp()
116
+ os.utime(cache_file, (old_time, old_time))
117
+
118
+ self.assertFalse(cache.is_cache_valid(cache_file))
119
+
120
+ def test_is_cache_valid_exception_handling(self):
121
+ """Test cache validation handles exceptions gracefully."""
122
+ cache = WizDataCache(self.temp_dir, cache_duration_minutes=60)
123
+
124
+ # Test with invalid path that will cause exception
125
+ with patch("os.path.exists", return_value=True):
126
+ with patch("os.path.getmtime", side_effect=OSError("Permission denied")):
127
+ self.assertFalse(cache.is_cache_valid("/invalid/path/cache.json"))
128
+
129
+ def test_load_from_cache_success_nodes_key(self):
130
+ """Test loading cache with 'nodes' key."""
131
+ cache = WizDataCache(self.temp_dir)
132
+ cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
133
+
134
+ test_nodes = [{"id": "1", "name": "test"}]
135
+ with open(cache_file, "w", encoding="utf-8") as f:
136
+ json.dump({"timestamp": datetime.now().isoformat(), "nodes": test_nodes}, f)
137
+
138
+ loaded_nodes = cache.load_from_cache(cache_file)
139
+ self.assertEqual(loaded_nodes, test_nodes)
140
+
141
+ def test_load_from_cache_success_assessments_key(self):
142
+ """Test loading cache with 'assessments' key."""
143
+ cache = WizDataCache(self.temp_dir)
144
+ cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
145
+
146
+ test_nodes = [{"id": "2", "name": "assessment"}]
147
+ with open(cache_file, "w", encoding="utf-8") as f:
148
+ json.dump({"timestamp": datetime.now().isoformat(), "assessments": test_nodes}, f)
149
+
150
+ loaded_nodes = cache.load_from_cache(cache_file)
151
+ self.assertEqual(loaded_nodes, test_nodes)
152
+
153
+ def test_load_from_cache_empty_nodes(self):
154
+ """Test loading cache with empty nodes."""
155
+ cache = WizDataCache(self.temp_dir)
156
+ cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
157
+
158
+ with open(cache_file, "w", encoding="utf-8") as f:
159
+ json.dump({"timestamp": datetime.now().isoformat(), "nodes": []}, f)
160
+
161
+ loaded_nodes = cache.load_from_cache(cache_file)
162
+ self.assertEqual(loaded_nodes, [])
163
+
164
+ def test_load_from_cache_no_nodes_key(self):
165
+ """Test loading cache without nodes or assessments key."""
166
+ cache = WizDataCache(self.temp_dir)
167
+ cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
168
+
169
+ with open(cache_file, "w", encoding="utf-8") as f:
170
+ json.dump({"timestamp": datetime.now().isoformat()}, f)
171
+
172
+ loaded_nodes = cache.load_from_cache(cache_file)
173
+ self.assertEqual(loaded_nodes, [])
174
+
175
+ def test_load_from_cache_invalid_json(self):
176
+ """Test loading cache with invalid JSON."""
177
+ cache = WizDataCache(self.temp_dir)
178
+ cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
179
+
180
+ with open(cache_file, "w", encoding="utf-8") as f:
181
+ f.write("invalid json content")
182
+
183
+ loaded_nodes = cache.load_from_cache(cache_file)
184
+ self.assertIsNone(loaded_nodes)
185
+
186
+ def test_load_from_cache_file_not_exists(self):
187
+ """Test loading cache when file does not exist."""
188
+ cache = WizDataCache(self.temp_dir)
189
+ cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
190
+
191
+ loaded_nodes = cache.load_from_cache(cache_file)
192
+ self.assertIsNone(loaded_nodes)
193
+
194
+ def test_load_from_cache_non_list_nodes(self):
195
+ """Test loading cache when nodes is not a list."""
196
+ cache = WizDataCache(self.temp_dir)
197
+ cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
198
+
199
+ with open(cache_file, "w", encoding="utf-8") as f:
200
+ json.dump({"timestamp": datetime.now().isoformat(), "nodes": "not a list"}, f)
201
+
202
+ loaded_nodes = cache.load_from_cache(cache_file)
203
+ self.assertIsNone(loaded_nodes)
204
+
205
+ def test_save_to_cache_success(self):
206
+ """Test saving data to cache."""
207
+ cache = WizDataCache(self.temp_dir, cache_duration_minutes=60)
208
+ cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
209
+
210
+ test_nodes = [{"id": "3", "name": "save_test"}]
211
+ cache.save_to_cache(cache_file, test_nodes, self.wiz_project_id, self.framework_id)
212
+
213
+ self.assertTrue(os.path.exists(cache_file))
214
+
215
+ with open(cache_file, "r", encoding="utf-8") as f:
216
+ saved_data = json.load(f)
217
+
218
+ self.assertIn("timestamp", saved_data)
219
+ self.assertEqual(saved_data["wiz_project_id"], self.wiz_project_id)
220
+ self.assertEqual(saved_data["framework_id"], self.framework_id)
221
+ self.assertEqual(saved_data["nodes"], test_nodes)
222
+
223
+ def test_save_to_cache_disabled(self):
224
+ """Test saving to cache when caching is disabled."""
225
+ cache = WizDataCache(self.temp_dir, cache_duration_minutes=0)
226
+ cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
227
+
228
+ test_nodes = [{"id": "4", "name": "no_save_test"}]
229
+ cache.save_to_cache(cache_file, test_nodes, self.wiz_project_id, self.framework_id)
230
+
231
+ self.assertFalse(os.path.exists(cache_file))
232
+
233
+ def test_save_to_cache_exception_handling(self):
234
+ """Test save to cache handles exceptions gracefully."""
235
+ cache = WizDataCache(self.temp_dir, cache_duration_minutes=60)
236
+
237
+ # Use invalid path to trigger exception
238
+ with patch("builtins.open", side_effect=PermissionError("No permission")):
239
+ # Should not raise exception
240
+ cache.save_to_cache("/invalid/path/cache.json", [], self.wiz_project_id, self.framework_id)
241
+
242
+
243
+ class TestWizApiClient(unittest.TestCase):
244
+ """Test cases for WizApiClient class."""
245
+
246
+ def setUp(self):
247
+ """Set up test fixtures."""
248
+ self.endpoint = "https://api.wiz.io/graphql"
249
+ self.access_token = "test_token_12345"
250
+ self.client = WizApiClient(self.endpoint, self.access_token)
251
+
252
+ def test_init(self):
253
+ """Test WizApiClient initialization."""
254
+ self.assertEqual(self.client.endpoint, self.endpoint)
255
+ self.assertEqual(self.client.access_token, self.access_token)
256
+
257
+ def test_get_headers(self):
258
+ """Test header generation."""
259
+ headers = self.client.get_headers()
260
+
261
+ self.assertIn("Authorization", headers)
262
+ self.assertIn("Content-Type", headers)
263
+ self.assertEqual(headers["Authorization"], f"Bearer {self.access_token}")
264
+ self.assertEqual(headers["Content-Type"], "application/json")
265
+
266
+ @patch("regscale.integrations.commercial.wizv2.fetchers.policy_assessment.run_async_queries")
267
+ @patch("regscale.integrations.commercial.wizv2.utils.compliance_job_progress")
268
+ def test_fetch_policy_assessments_async_success(self, mock_progress, mock_run_async):
269
+ """Test async policy assessment fetching - success case."""
270
+ mock_progress.__enter__ = Mock(return_value=mock_progress)
271
+ mock_progress.__exit__ = Mock(return_value=False)
272
+ mock_progress.add_task = Mock(return_value="task_id")
273
+ mock_progress.update = Mock()
274
+
275
+ test_nodes = [{"id": "node1"}, {"id": "node2"}]
276
+ mock_run_async.return_value = [(WizVulnerabilityType.CONFIGURATION.value, test_nodes, None)]
277
+
278
+ result = self.client.fetch_policy_assessments_async()
279
+
280
+ self.assertEqual(result, test_nodes)
281
+ mock_run_async.assert_called_once()
282
+
283
+ call_args = mock_run_async.call_args
284
+ self.assertEqual(call_args.kwargs["endpoint"], self.endpoint)
285
+ self.assertIn("Authorization", call_args.kwargs["headers"])
286
+
287
+ @patch("regscale.integrations.commercial.wizv2.fetchers.policy_assessment.run_async_queries")
288
+ @patch("regscale.integrations.commercial.wizv2.utils.compliance_job_progress")
289
+ def test_fetch_policy_assessments_async_empty_results(self, mock_progress, mock_run_async):
290
+ """Test async policy assessment fetching - empty results."""
291
+ mock_progress.__enter__ = Mock(return_value=mock_progress)
292
+ mock_progress.__exit__ = Mock(return_value=False)
293
+ mock_progress.add_task = Mock(return_value="task_id")
294
+ mock_progress.update = Mock()
295
+
296
+ mock_run_async.return_value = []
297
+
298
+ result = self.client.fetch_policy_assessments_async()
299
+
300
+ self.assertEqual(result, [])
301
+
302
+ @patch("regscale.integrations.commercial.wizv2.fetchers.policy_assessment.run_async_queries")
303
+ @patch("regscale.integrations.commercial.wizv2.utils.compliance_job_progress")
304
+ def test_fetch_policy_assessments_async_with_error(self, mock_progress, mock_run_async):
305
+ """Test async policy assessment fetching - with error."""
306
+ mock_progress.__enter__ = Mock(return_value=mock_progress)
307
+ mock_progress.__exit__ = Mock(return_value=False)
308
+ mock_progress.add_task = Mock(return_value="task_id")
309
+ mock_progress.update = Mock()
310
+
311
+ mock_run_async.return_value = [(WizVulnerabilityType.CONFIGURATION.value, [], Exception("API Error"))]
312
+
313
+ result = self.client.fetch_policy_assessments_async()
314
+
315
+ self.assertEqual(result, [])
316
+
317
+ @patch("regscale.integrations.commercial.wizv2.fetchers.policy_assessment.run_async_queries")
318
+ @patch("regscale.integrations.commercial.wizv2.utils.compliance_job_progress")
319
+ def test_fetch_policy_assessments_async_exception(self, mock_progress, mock_run_async):
320
+ """Test async policy assessment fetching - exception raised."""
321
+ mock_progress.__enter__ = Mock(return_value=mock_progress)
322
+ mock_progress.__exit__ = Mock(return_value=False)
323
+ mock_progress.add_task = Mock(return_value="task_id")
324
+ mock_progress.update = Mock()
325
+
326
+ mock_run_async.side_effect = Exception("Async client error")
327
+
328
+ with self.assertRaises(Exception) as context:
329
+ self.client.fetch_policy_assessments_async()
330
+
331
+ self.assertIn("Async client error", str(context.exception))
332
+
333
+ def test_create_requests_session(self):
334
+ """Test requests session creation with retry logic."""
335
+ session = self.client._create_requests_session()
336
+
337
+ self.assertIsInstance(session, requests.Session)
338
+ # Verify retry adapter is configured
339
+ adapter = session.get_adapter("https://")
340
+ self.assertIsNotNone(adapter)
341
+
342
+ @patch.object(WizApiClient, "_execute_paginated_query")
343
+ def test_fetch_policy_assessments_requests_success_first_variant(self, mock_execute):
344
+ """Test requests-based fetching - success with first filter variant."""
345
+ test_nodes = [{"id": "req1"}, {"id": "req2"}]
346
+ mock_execute.return_value = test_nodes
347
+
348
+ base_variables = {"first": 100}
349
+ filter_variants = [{"project": ["proj1"]}, {"projectId": ["proj1"]}, None]
350
+
351
+ result = self.client.fetch_policy_assessments_requests(base_variables, filter_variants)
352
+
353
+ self.assertEqual(result, test_nodes)
354
+ mock_execute.assert_called_once()
355
+
356
+ @patch.object(WizApiClient, "_execute_paginated_query")
357
+ def test_fetch_policy_assessments_requests_fallback_to_second_variant(self, mock_execute):
358
+ """Test requests-based fetching - fallback to second filter variant."""
359
+ test_nodes = [{"id": "req3"}]
360
+
361
+ def side_effect(*args, **kwargs):
362
+ if mock_execute.call_count == 1:
363
+ raise Exception("First variant failed")
364
+ return test_nodes
365
+
366
+ mock_execute.side_effect = side_effect
367
+
368
+ base_variables = {"first": 100}
369
+ filter_variants = [{"project": ["proj1"]}, {"projectId": ["proj1"]}, None]
370
+
371
+ result = self.client.fetch_policy_assessments_requests(base_variables, filter_variants)
372
+
373
+ self.assertEqual(result, test_nodes)
374
+ self.assertEqual(mock_execute.call_count, 2)
375
+
376
+ @patch.object(WizApiClient, "_execute_paginated_query")
377
+ def test_fetch_policy_assessments_requests_all_variants_fail(self, mock_execute):
378
+ """Test requests-based fetching - all filter variants fail."""
379
+ mock_execute.side_effect = Exception("API Error")
380
+
381
+ base_variables = {"first": 100}
382
+ filter_variants = [{"project": ["proj1"]}, None]
383
+
384
+ with self.assertRaises(RuntimeError) as context:
385
+ self.client.fetch_policy_assessments_requests(base_variables, filter_variants)
386
+
387
+ self.assertIn("All filter variants failed", str(context.exception))
388
+
389
+ @patch.object(WizApiClient, "_execute_paginated_query")
390
+ def test_fetch_policy_assessments_requests_with_callback(self, mock_execute):
391
+ """Test requests-based fetching with progress callback."""
392
+ test_nodes = [{"id": "req4"}]
393
+ mock_execute.return_value = test_nodes
394
+
395
+ mock_callback = Mock()
396
+ base_variables = {"first": 100}
397
+ filter_variants = [{"project": ["proj1"]}]
398
+
399
+ result = self.client.fetch_policy_assessments_requests(base_variables, filter_variants, mock_callback)
400
+
401
+ self.assertEqual(result, test_nodes)
402
+
403
+ @patch("requests.Session.post")
404
+ def test_execute_paginated_query_single_page(self, mock_post):
405
+ """Test paginated query execution - single page."""
406
+ mock_response = Mock()
407
+ mock_response.status_code = 200
408
+ mock_response.json.return_value = {
409
+ "data": {
410
+ "policyAssessments": {
411
+ "nodes": [{"id": "page1_node1"}],
412
+ "pageInfo": {"hasNextPage": False, "endCursor": None},
413
+ }
414
+ }
415
+ }
416
+ mock_post.return_value = mock_response
417
+
418
+ session = self.client._create_requests_session()
419
+ variables = {"first": 100}
420
+
421
+ result = self.client._execute_paginated_query(session, variables)
422
+
423
+ self.assertEqual(len(result), 1)
424
+ self.assertEqual(result[0]["id"], "page1_node1")
425
+ mock_post.assert_called_once()
426
+
427
+ @patch("requests.Session.post")
428
+ def test_execute_paginated_query_multiple_pages(self, mock_post):
429
+ """Test paginated query execution - multiple pages."""
430
+ # First page response
431
+ response1 = Mock()
432
+ response1.status_code = 200
433
+ response1.json.return_value = {
434
+ "data": {
435
+ "policyAssessments": {
436
+ "nodes": [{"id": "page1_node1"}],
437
+ "pageInfo": {"hasNextPage": True, "endCursor": "cursor1"},
438
+ }
439
+ }
440
+ }
441
+
442
+ # Second page response
443
+ response2 = Mock()
444
+ response2.status_code = 200
445
+ response2.json.return_value = {
446
+ "data": {
447
+ "policyAssessments": {
448
+ "nodes": [{"id": "page2_node1"}],
449
+ "pageInfo": {"hasNextPage": False, "endCursor": None},
450
+ }
451
+ }
452
+ }
453
+
454
+ mock_post.side_effect = [response1, response2]
455
+
456
+ session = self.client._create_requests_session()
457
+ variables = {"first": 100}
458
+
459
+ result = self.client._execute_paginated_query(session, variables)
460
+
461
+ self.assertEqual(len(result), 2)
462
+ self.assertEqual(mock_post.call_count, 2)
463
+
464
+ @patch("requests.Session.post")
465
+ def test_execute_paginated_query_with_progress_callback(self, mock_post):
466
+ """Test paginated query execution with progress callback."""
467
+ mock_response = Mock()
468
+ mock_response.status_code = 200
469
+ mock_response.json.return_value = {
470
+ "data": {
471
+ "policyAssessments": {
472
+ "nodes": [{"id": "callback_node"}],
473
+ "pageInfo": {"hasNextPage": False, "endCursor": None},
474
+ }
475
+ }
476
+ }
477
+ mock_post.return_value = mock_response
478
+
479
+ mock_callback = Mock()
480
+ session = self.client._create_requests_session()
481
+ variables = {"first": 100}
482
+
483
+ result = self.client._execute_paginated_query(session, variables, mock_callback)
484
+
485
+ self.assertEqual(len(result), 1)
486
+ mock_callback.assert_called()
487
+
488
+ @patch("requests.Session.post")
489
+ def test_execute_paginated_query_http_error(self, mock_post):
490
+ """Test paginated query execution - HTTP error."""
491
+ mock_response = Mock()
492
+ mock_response.status_code = 500
493
+ mock_response.text = "Internal Server Error"
494
+ mock_post.return_value = mock_response
495
+
496
+ session = self.client._create_requests_session()
497
+ variables = {"first": 100}
498
+
499
+ with self.assertRaises(requests.HTTPError):
500
+ self.client._execute_paginated_query(session, variables)
501
+
502
+ @patch("requests.Session.post")
503
+ def test_execute_paginated_query_graphql_errors(self, mock_post):
504
+ """Test paginated query execution - GraphQL errors."""
505
+ mock_response = Mock()
506
+ mock_response.status_code = 200
507
+ mock_response.json.return_value = {"errors": [{"message": "GraphQL error"}]}
508
+ mock_post.return_value = mock_response
509
+
510
+ session = self.client._create_requests_session()
511
+ variables = {"first": 100}
512
+
513
+ with self.assertRaises(RuntimeError) as context:
514
+ self.client._execute_paginated_query(session, variables)
515
+
516
+ self.assertIn("GraphQL error", str(context.exception))
517
+
518
+ @patch("requests.Session.post")
519
+ def test_execute_paginated_query_callback_exception(self, mock_post):
520
+ """Test paginated query execution - callback raises exception (should be caught)."""
521
+ mock_response = Mock()
522
+ mock_response.status_code = 200
523
+ mock_response.json.return_value = {
524
+ "data": {
525
+ "policyAssessments": {
526
+ "nodes": [{"id": "callback_error_node"}],
527
+ "pageInfo": {"hasNextPage": False, "endCursor": None},
528
+ }
529
+ }
530
+ }
531
+ mock_post.return_value = mock_response
532
+
533
+ mock_callback = Mock(side_effect=Exception("Callback error"))
534
+ session = self.client._create_requests_session()
535
+ variables = {"first": 100}
536
+
537
+ # Should not raise exception despite callback error
538
+ result = self.client._execute_paginated_query(session, variables, mock_callback)
539
+
540
+ self.assertEqual(len(result), 1)
541
+
542
+
543
+ class TestPolicyAssessmentFetcher(unittest.TestCase):
544
+ """Test cases for PolicyAssessmentFetcher class."""
545
+
546
+ def setUp(self):
547
+ """Set up test fixtures."""
548
+ self.temp_dir = tempfile.mkdtemp()
549
+ self.wiz_endpoint = "https://api.wiz.io/graphql"
550
+ self.access_token = "test_token"
551
+ self.wiz_project_id = "proj-123"
552
+ self.framework_id = "wf-id-4"
553
+
554
+ # Create fetcher without mocking cache
555
+ self.fetcher = PolicyAssessmentFetcher(
556
+ self.wiz_endpoint, self.access_token, self.wiz_project_id, self.framework_id, cache_duration_minutes=0
557
+ )
558
+
559
+ def tearDown(self):
560
+ """Clean up test artifacts."""
561
+ import shutil
562
+
563
+ if os.path.exists(self.temp_dir):
564
+ shutil.rmtree(self.temp_dir)
565
+
566
+ def test_init(self):
567
+ """Test PolicyAssessmentFetcher initialization."""
568
+ self.assertIsNotNone(self.fetcher.api_client)
569
+ self.assertEqual(self.fetcher.wiz_project_id, self.wiz_project_id)
570
+ self.assertEqual(self.fetcher.framework_id, self.framework_id)
571
+ self.assertIsNotNone(self.fetcher.cache)
572
+
573
+ def test_fetch_policy_assessments_no_cache(self):
574
+ """Test fetching policy assessments without cache."""
575
+ test_nodes = [
576
+ {
577
+ "id": "assess1",
578
+ "policy": {
579
+ "securitySubCategories": [
580
+ {"externalId": " AC-1 ", "category": {"framework": {"id": self.framework_id}}}
581
+ ]
582
+ },
583
+ }
584
+ ]
585
+
586
+ with patch.object(self.fetcher.cache, "is_cache_valid", return_value=False):
587
+ with patch.object(self.fetcher.cache, "get_cache_file_path", return_value="/tmp/cache.json"):
588
+ with patch.object(self.fetcher.cache, "save_to_cache") as mock_save:
589
+ with patch.object(
590
+ self.fetcher.api_client, "fetch_policy_assessments_async", return_value=test_nodes
591
+ ):
592
+ result = self.fetcher.fetch_policy_assessments()
593
+
594
+ self.assertEqual(len(result), 1)
595
+ mock_save.assert_called_once()
596
+
597
+ def test_fetch_policy_assessments_with_valid_cache(self):
598
+ """Test fetching policy assessments with valid cache."""
599
+ cached_nodes = [{"id": "cached1"}]
600
+
601
+ with patch.object(self.fetcher.cache, "is_cache_valid", return_value=True):
602
+ with patch.object(self.fetcher.cache, "get_cache_file_path", return_value="/tmp/cache.json"):
603
+ with patch.object(self.fetcher.cache, "load_from_cache", return_value=cached_nodes) as mock_load:
604
+ result = self.fetcher.fetch_policy_assessments()
605
+
606
+ self.assertEqual(result, cached_nodes)
607
+ mock_load.assert_called_once()
608
+
609
+ def test_fetch_policy_assessments_async_fallback_to_requests(self):
610
+ """Test fallback to requests when async fails."""
611
+ test_nodes = [{"id": "fallback1"}]
612
+
613
+ with patch.object(self.fetcher.cache, "is_cache_valid", return_value=False):
614
+ with patch.object(self.fetcher.cache, "get_cache_file_path", return_value="/tmp/cache.json"):
615
+ with patch.object(self.fetcher.cache, "save_to_cache"):
616
+ with patch.object(
617
+ self.fetcher.api_client, "fetch_policy_assessments_async", side_effect=Exception("Async failed")
618
+ ) as mock_async:
619
+ with patch.object(
620
+ self.fetcher.api_client, "fetch_policy_assessments_requests", return_value=test_nodes
621
+ ) as mock_requests:
622
+ result = self.fetcher.fetch_policy_assessments()
623
+
624
+ self.assertEqual(len(result), 1)
625
+ mock_async.assert_called_once()
626
+ mock_requests.assert_called_once()
627
+
628
+ def test_filter_nodes_to_framework_matching(self):
629
+ """Test filtering nodes to framework - matching framework."""
630
+ nodes = [
631
+ {
632
+ "id": "node1",
633
+ "policy": {"securitySubCategories": [{"category": {"framework": {"id": self.framework_id}}}]},
634
+ },
635
+ {
636
+ "id": "node2",
637
+ "policy": {"securitySubCategories": [{"category": {"framework": {"id": "other-framework"}}}]},
638
+ },
639
+ ]
640
+
641
+ filtered = self.fetcher._filter_nodes_to_framework(nodes)
642
+
643
+ self.assertEqual(len(filtered), 1)
644
+ self.assertEqual(filtered[0]["id"], "node1")
645
+
646
+ def test_filter_nodes_to_framework_no_subcategories(self):
647
+ """Test filtering nodes to framework - no subcategories (should include)."""
648
+ nodes = [{"id": "node1", "policy": {"securitySubCategories": []}}]
649
+
650
+ filtered = self.fetcher._filter_nodes_to_framework(nodes)
651
+
652
+ self.assertEqual(len(filtered), 1)
653
+
654
+ def test_filter_nodes_to_framework_exception_handling(self):
655
+ """Test filtering nodes to framework - exception handling."""
656
+ nodes = [{"id": "node1", "policy": None}]
657
+
658
+ filtered = self.fetcher._filter_nodes_to_framework(nodes)
659
+
660
+ # Should include node on error (defensive)
661
+ self.assertEqual(len(filtered), 1)
662
+
663
+ def test_clean_node_data_trim_whitespace(self):
664
+ """Test cleaning node data - trim whitespace from externalId."""
665
+ nodes = [
666
+ {
667
+ "id": "node1",
668
+ "policy": {
669
+ "securitySubCategories": [
670
+ {"externalId": " AC-1 ", "category": {"framework": {"id": "fw1"}}},
671
+ {"externalId": "AC-2", "category": {"framework": {"id": "fw1"}}},
672
+ ]
673
+ },
674
+ }
675
+ ]
676
+
677
+ cleaned = self.fetcher._clean_node_data(nodes)
678
+
679
+ self.assertEqual(cleaned[0]["policy"]["securitySubCategories"][0]["externalId"], "AC-1")
680
+ self.assertEqual(cleaned[0]["policy"]["securitySubCategories"][1]["externalId"], "AC-2")
681
+
682
+ def test_clean_node_data_no_policy(self):
683
+ """Test cleaning node data - no policy field."""
684
+ nodes = [{"id": "node1"}]
685
+
686
+ cleaned = self.fetcher._clean_node_data(nodes)
687
+
688
+ self.assertEqual(len(cleaned), 1)
689
+ self.assertEqual(cleaned[0]["id"], "node1")
690
+
691
+ def test_clean_node_data_exception_handling(self):
692
+ """Test cleaning node data - exception handling."""
693
+ nodes = [{"id": "node1", "policy": {"securitySubCategories": "invalid"}}]
694
+
695
+ cleaned = self.fetcher._clean_node_data(nodes)
696
+
697
+ # Should include original node on error
698
+ self.assertEqual(len(cleaned), 1)
699
+
700
+ def test_clean_single_node(self):
701
+ """Test cleaning a single node."""
702
+ node = {
703
+ "id": "node1",
704
+ "policy": {"securitySubCategories": [{"externalId": " AC-1 "}]},
705
+ }
706
+
707
+ cleaned = self.fetcher._clean_single_node(node)
708
+
709
+ self.assertEqual(cleaned["policy"]["securitySubCategories"][0]["externalId"], "AC-1")
710
+
711
+ def test_should_clean_policy_true(self):
712
+ """Test should_clean_policy returns True."""
713
+ policy = {"securitySubCategories": []}
714
+
715
+ result = self.fetcher._should_clean_policy(policy)
716
+
717
+ self.assertTrue(result)
718
+
719
+ def test_should_clean_policy_false_no_policy(self):
720
+ """Test should_clean_policy returns False - no policy."""
721
+ result = self.fetcher._should_clean_policy(None)
722
+
723
+ self.assertFalse(result)
724
+
725
+ def test_should_clean_policy_false_no_subcategories(self):
726
+ """Test should_clean_policy returns False - no subcategories key."""
727
+ policy = {"other_key": "value"}
728
+
729
+ result = self.fetcher._should_clean_policy(policy)
730
+
731
+ self.assertFalse(result)
732
+
733
+ def test_clean_policy_subcategories(self):
734
+ """Test cleaning policy subcategories."""
735
+ policy = {
736
+ "id": "policy1",
737
+ "securitySubCategories": [
738
+ {"externalId": " AC-1 "},
739
+ {"externalId": " AC-2 "},
740
+ ],
741
+ }
742
+
743
+ cleaned = self.fetcher._clean_policy_subcategories(policy)
744
+
745
+ self.assertEqual(cleaned["securitySubCategories"][0]["externalId"], "AC-1")
746
+ self.assertEqual(cleaned["securitySubCategories"][1]["externalId"], "AC-2")
747
+ self.assertEqual(cleaned["id"], "policy1")
748
+
749
+ def test_clean_subcategory(self):
750
+ """Test cleaning a single subcategory."""
751
+ subcat = {"externalId": " AC-1 ", "title": "Access Control"}
752
+
753
+ cleaned = self.fetcher._clean_subcategory(subcat)
754
+
755
+ self.assertEqual(cleaned["externalId"], "AC-1")
756
+ self.assertEqual(cleaned["title"], "Access Control")
757
+
758
+ def test_clean_subcategory_no_external_id(self):
759
+ """Test cleaning subcategory without externalId."""
760
+ subcat = {"title": "Access Control"}
761
+
762
+ cleaned = self.fetcher._clean_subcategory(subcat)
763
+
764
+ self.assertNotIn("externalId", cleaned)
765
+
766
+ def test_clean_subcategory_non_string_external_id(self):
767
+ """Test cleaning subcategory with non-string externalId."""
768
+ subcat = {"externalId": 123, "title": "Test"}
769
+
770
+ cleaned = self.fetcher._clean_subcategory(subcat)
771
+
772
+ self.assertEqual(cleaned["externalId"], 123)
773
+
774
+ @patch.object(WizApiClient, "fetch_policy_assessments_async")
775
+ def test_fetch_with_async_client(self, mock_async):
776
+ """Test _fetch_with_async_client."""
777
+ test_nodes = [{"id": "async1"}]
778
+ mock_async.return_value = test_nodes
779
+
780
+ result = self.fetcher._fetch_with_async_client()
781
+
782
+ self.assertEqual(result, test_nodes)
783
+ mock_async.assert_called_once()
784
+
785
+ @patch.object(WizApiClient, "fetch_policy_assessments_requests")
786
+ def test_fetch_with_requests(self, mock_requests):
787
+ """Test _fetch_with_requests with multiple filter variants."""
788
+ test_nodes = [{"id": "req1"}]
789
+ mock_requests.return_value = test_nodes
790
+
791
+ result = self.fetcher._fetch_with_requests()
792
+
793
+ self.assertEqual(result, test_nodes)
794
+ mock_requests.assert_called_once()
795
+
796
+ # Verify filter variants were passed
797
+ call_args = mock_requests.call_args
798
+ filter_variants = call_args[0][1]
799
+ self.assertIn({"project": [self.wiz_project_id]}, filter_variants)
800
+ self.assertIn({"projectId": [self.wiz_project_id]}, filter_variants)
801
+ self.assertIn(None, filter_variants)
802
+
803
+
804
+ if __name__ == "__main__":
805
+ unittest.main()