regscale-cli 6.25.1.0__py3-none-any.whl → 6.26.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 (80) hide show
  1. regscale/_version.py +1 -1
  2. regscale/airflow/hierarchy.py +2 -2
  3. regscale/core/app/application.py +18 -3
  4. regscale/core/app/internal/login.py +0 -1
  5. regscale/core/app/utils/catalog_utils/common.py +1 -1
  6. regscale/integrations/commercial/sicura/api.py +14 -13
  7. regscale/integrations/commercial/sicura/commands.py +8 -2
  8. regscale/integrations/commercial/sicura/scanner.py +49 -39
  9. regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
  10. regscale/integrations/commercial/wizv2/click.py +26 -26
  11. regscale/integrations/commercial/wizv2/compliance_report.py +152 -157
  12. regscale/integrations/commercial/wizv2/scanner.py +3 -3
  13. regscale/integrations/compliance_integration.py +67 -2
  14. regscale/integrations/control_matcher.py +358 -0
  15. regscale/integrations/milestone_manager.py +291 -0
  16. regscale/integrations/public/__init__.py +1 -0
  17. regscale/integrations/public/cci_importer.py +37 -38
  18. regscale/integrations/public/fedramp/click.py +60 -2
  19. regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
  20. regscale/integrations/scanner_integration.py +150 -96
  21. regscale/models/integration_models/cisa_kev_data.json +154 -4
  22. regscale/models/integration_models/nexpose.py +36 -10
  23. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  24. regscale/models/locking.py +12 -8
  25. regscale/models/platform.py +1 -2
  26. regscale/models/regscale_models/control_implementation.py +46 -21
  27. regscale/models/regscale_models/issue.py +256 -94
  28. regscale/models/regscale_models/milestone.py +1 -1
  29. regscale/models/regscale_models/regscale_model.py +6 -1
  30. regscale/templates/__init__.py +0 -0
  31. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/METADATA +1 -1
  32. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/RECORD +80 -33
  33. tests/regscale/integrations/commercial/__init__.py +0 -0
  34. tests/regscale/integrations/commercial/conftest.py +28 -0
  35. tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
  36. tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
  37. tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
  38. tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
  39. tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
  40. tests/regscale/integrations/commercial/test_aws.py +3731 -0
  41. tests/regscale/integrations/commercial/test_burp.py +48 -0
  42. tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
  43. tests/regscale/integrations/commercial/test_dependabot.py +341 -0
  44. tests/regscale/integrations/commercial/test_gcp.py +1543 -0
  45. tests/regscale/integrations/commercial/test_gitlab.py +549 -0
  46. tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
  47. tests/regscale/integrations/commercial/test_jira.py +1814 -0
  48. tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
  49. tests/regscale/integrations/commercial/test_okta.py +1228 -0
  50. tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
  51. tests/regscale/integrations/commercial/test_sicura.py +350 -0
  52. tests/regscale/integrations/commercial/test_snow.py +423 -0
  53. tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
  54. tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
  55. tests/regscale/integrations/commercial/test_stig.py +33 -0
  56. tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
  57. tests/regscale/integrations/commercial/test_stigv2.py +406 -0
  58. tests/regscale/integrations/commercial/test_wiz.py +1469 -0
  59. tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
  60. tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
  61. tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
  62. tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
  63. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
  64. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1351 -0
  65. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
  66. tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
  67. tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +750 -0
  68. tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
  69. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +264 -0
  70. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +624 -0
  71. tests/regscale/integrations/public/fedramp/__init__.py +1 -0
  72. tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
  73. tests/regscale/integrations/test_control_matcher.py +1314 -0
  74. tests/regscale/integrations/test_control_matching.py +155 -0
  75. tests/regscale/integrations/test_milestone_manager.py +408 -0
  76. tests/regscale/models/test_issue.py +378 -1
  77. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/LICENSE +0 -0
  78. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/WHEEL +0 -0
  79. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/entry_points.txt +0 -0
  80. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,624 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Unit tests for Wiz V2 utility functions."""
4
+
5
+ import logging
6
+ import unittest
7
+ from unittest.mock import patch, MagicMock
8
+
9
+ from regscale.integrations.commercial.wizv2.utils import get_report_url_and_status, get_or_create_report_id
10
+
11
+ logger = logging.getLogger("regscale")
12
+
13
+
14
+ class TestWizUtils(unittest.TestCase):
15
+ """Test cases for Wiz utility functions."""
16
+
17
+ @patch("regscale.integrations.commercial.wizv2.utils.download_report")
18
+ @patch("regscale.integrations.commercial.wizv2.utils.rerun_expired_report")
19
+ @patch("regscale.integrations.commercial.wizv2.utils.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
20
+ @patch("regscale.integrations.commercial.wizv2.utils.MAX_RETRIES", 3)
21
+ def test_get_report_url_and_status_completed(self, mock_rerun_report, mock_download_report):
22
+ """Test get_report_url_and_status with COMPLETED status."""
23
+ # Mock response for completed report
24
+ mock_response = MagicMock()
25
+ mock_response.ok = True
26
+ mock_response.json.return_value = {
27
+ "data": {"report": {"lastRun": {"status": "COMPLETED", "url": "https://example.com/report.csv"}}}
28
+ }
29
+ mock_download_report.return_value = mock_response
30
+
31
+ # Call the function
32
+ result = get_report_url_and_status("test-report-id")
33
+
34
+ # Verify the result
35
+ self.assertEqual(result, "https://example.com/report.csv")
36
+ mock_download_report.assert_called_once_with({"reportId": "test-report-id"})
37
+ mock_rerun_report.assert_not_called()
38
+
39
+ @patch("regscale.integrations.commercial.wizv2.utils.download_report")
40
+ @patch("regscale.integrations.commercial.wizv2.utils.rerun_expired_report")
41
+ @patch("regscale.integrations.commercial.wizv2.utils.get_report_url_and_status")
42
+ @patch("regscale.integrations.commercial.wizv2.utils.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
43
+ @patch("regscale.integrations.commercial.wizv2.utils.MAX_RETRIES", 3)
44
+ def test_get_report_url_and_status_expired(self, mock_recursive_call, mock_rerun_report, mock_download_report):
45
+ """Test get_report_url_and_status with EXPIRED status."""
46
+ # Mock response for expired report
47
+ mock_response = MagicMock()
48
+ mock_response.ok = True
49
+ mock_response.json.return_value = {"data": {"report": {"lastRun": {"status": "EXPIRED"}}}}
50
+ mock_download_report.return_value = mock_response
51
+
52
+ # Mock rerun response
53
+ mock_rerun_response = MagicMock()
54
+ mock_rerun_response.ok = True
55
+ mock_rerun_report.return_value = mock_rerun_response
56
+
57
+ # Mock recursive call to return final URL
58
+ mock_recursive_call.return_value = "https://example.com/new-report.csv"
59
+
60
+ # Call the function
61
+ result = get_report_url_and_status("test-report-id")
62
+
63
+ # Verify the result
64
+ self.assertEqual(result, "https://example.com/new-report.csv")
65
+ mock_download_report.assert_called_once_with({"reportId": "test-report-id"})
66
+ mock_rerun_report.assert_called_once_with({"reportId": "test-report-id"})
67
+ mock_recursive_call.assert_called_once_with("test-report-id")
68
+
69
+ @patch("regscale.integrations.commercial.wizv2.utils.download_report")
70
+ @patch("regscale.integrations.commercial.wizv2.utils.rerun_expired_report")
71
+ @patch("regscale.integrations.commercial.wizv2.utils.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
72
+ @patch("regscale.integrations.commercial.wizv2.utils.MAX_RETRIES", 5)
73
+ def test_get_report_url_and_status_rate_limit(self, mock_rerun_report, mock_download_report):
74
+ """Test get_report_url_and_status with rate limit error."""
75
+ from regscale.integrations.commercial.wizv2.constants import RATE_LIMIT_MSG
76
+
77
+ # Mock response with rate limit error
78
+ mock_response = MagicMock()
79
+ mock_response.ok = True
80
+ mock_response.json.return_value = {
81
+ "errors": [
82
+ {"message": "Rate limit exceeded", "extensions": {"retryAfter": 0.001}} # Reduced to milliseconds
83
+ ]
84
+ }
85
+ mock_download_report.return_value = mock_response
86
+
87
+ # Mock second response for successful completion
88
+ mock_response2 = MagicMock()
89
+ mock_response2.ok = True
90
+ mock_response2.json.return_value = {
91
+ "data": {"report": {"lastRun": {"status": "COMPLETED", "url": "https://example.com/report.csv"}}}
92
+ }
93
+
94
+ # Configure mock to return different responses on subsequent calls
95
+ mock_download_report.side_effect = [mock_response, mock_response2]
96
+
97
+ with patch("time.sleep") as mock_sleep:
98
+ # Call the function
99
+ result = get_report_url_and_status("test-report-id")
100
+
101
+ # Verify the result
102
+ self.assertEqual(result, "https://example.com/report.csv")
103
+ self.assertEqual(mock_download_report.call_count, 2)
104
+ mock_rerun_report.assert_not_called()
105
+ self.assertEqual(mock_sleep.call_count, 5)
106
+
107
+ @patch("regscale.integrations.commercial.wizv2.utils.download_report")
108
+ @patch("regscale.integrations.commercial.wizv2.utils.rerun_expired_report")
109
+ @patch("regscale.integrations.commercial.wizv2.utils.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
110
+ @patch("regscale.integrations.commercial.wizv2.utils.MAX_RETRIES", 3)
111
+ def test_get_report_url_and_status_failed_response(self, mock_rerun_report, mock_download_report):
112
+ """Test get_report_url_and_status with failed response."""
113
+ # Mock failed response
114
+ mock_response = MagicMock()
115
+ mock_response.ok = False
116
+ mock_download_report.return_value = mock_response
117
+
118
+ # Call the function and expect exception
119
+ with self.assertRaises(Exception) as context:
120
+ get_report_url_and_status("test-report-id")
121
+
122
+ self.assertIn("Failed to download report", str(context.exception))
123
+ mock_download_report.assert_called_once_with({"reportId": "test-report-id"})
124
+ mock_rerun_report.assert_not_called()
125
+
126
+ @patch("regscale.integrations.commercial.wizv2.utils.download_report")
127
+ @patch("regscale.integrations.commercial.wizv2.utils.rerun_expired_report")
128
+ @patch("regscale.integrations.commercial.wizv2.utils.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
129
+ @patch("regscale.integrations.commercial.wizv2.utils.MAX_RETRIES", 3)
130
+ def test_get_report_url_and_status_none_response(self, mock_rerun_report, mock_download_report):
131
+ """Test get_report_url_and_status with None response."""
132
+ # Mock None response
133
+ mock_download_report.return_value = None
134
+
135
+ # Call the function and expect exception
136
+ with self.assertRaises(Exception) as context:
137
+ get_report_url_and_status("test-report-id")
138
+
139
+ self.assertIn("Failed to download report", str(context.exception))
140
+ mock_download_report.assert_called_once_with({"reportId": "test-report-id"})
141
+ mock_rerun_report.assert_not_called()
142
+
143
+ @patch("regscale.integrations.commercial.wizv2.utils.download_report")
144
+ @patch("regscale.integrations.commercial.wizv2.utils.rerun_expired_report")
145
+ @patch("regscale.integrations.commercial.wizv2.utils.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
146
+ @patch("regscale.integrations.commercial.wizv2.utils.MAX_RETRIES", 3) # Reduce retries for faster testing
147
+ def test_get_report_url_and_status_other_error(self, mock_rerun_report, mock_download_report):
148
+ """Test get_report_url_and_status with other error in response."""
149
+ # Mock response with other error
150
+ mock_response = MagicMock()
151
+ mock_response.ok = True
152
+ mock_response.json.return_value = {"errors": [{"message": "Some other error occurred"}]}
153
+ mock_download_report.return_value = mock_response
154
+
155
+ # Call the function and expect exception after max retries
156
+ with self.assertRaises(Exception) as context:
157
+ get_report_url_and_status("test-report-id")
158
+
159
+ self.assertIn("Download failed, exceeding the maximum number of retries", str(context.exception))
160
+ # Should be called MAX_RETRIES times (now 3)
161
+ self.assertEqual(mock_download_report.call_count, 3)
162
+ mock_rerun_report.assert_not_called()
163
+
164
+ @patch("regscale.integrations.commercial.wizv2.utils.download_report")
165
+ @patch("regscale.integrations.commercial.wizv2.utils.rerun_expired_report")
166
+ @patch("regscale.integrations.commercial.wizv2.utils.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
167
+ @patch("regscale.integrations.commercial.wizv2.utils.MAX_RETRIES", 3) # Reduce retries for faster testing
168
+ def test_get_report_url_and_status_unknown_status(self, mock_rerun_report, mock_download_report):
169
+ """Test get_report_url_and_status with unknown status."""
170
+ # Mock response with unknown status
171
+ mock_response = MagicMock()
172
+ mock_response.ok = True
173
+ mock_response.json.return_value = {"data": {"report": {"lastRun": {"status": "UNKNOWN_STATUS"}}}}
174
+ mock_download_report.return_value = mock_response
175
+
176
+ # Call the function and expect exception after max retries
177
+ with self.assertRaises(Exception) as context:
178
+ get_report_url_and_status("test-report-id")
179
+
180
+ self.assertIn("Download failed, exceeding the maximum number of retries", str(context.exception))
181
+ # Should be called MAX_RETRIES times (now 3)
182
+ self.assertEqual(mock_download_report.call_count, 3)
183
+ mock_rerun_report.assert_not_called()
184
+
185
+ @patch("regscale.integrations.commercial.wizv2.utils.download_report")
186
+ @patch("regscale.integrations.commercial.wizv2.utils.rerun_expired_report")
187
+ @patch("regscale.integrations.commercial.wizv2.utils.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
188
+ @patch("regscale.integrations.commercial.wizv2.utils.MAX_RETRIES", 3) # Reduce retries for faster testing
189
+ def test_get_report_url_and_status_missing_status(self, mock_rerun_report, mock_download_report):
190
+ """Test get_report_url_and_status with missing status in response."""
191
+ # Mock response with missing status
192
+ mock_response = MagicMock()
193
+ mock_response.ok = True
194
+ mock_response.json.return_value = {"data": {"report": {"lastRun": {}}}}
195
+ mock_download_report.return_value = mock_response
196
+
197
+ # Call the function and expect exception after max retries
198
+ with self.assertRaises(Exception) as context:
199
+ get_report_url_and_status("test-report-id")
200
+
201
+ self.assertIn("Download failed, exceeding the maximum number of retries", str(context.exception))
202
+ # Should be called MAX_RETRIES times (now 3)
203
+ self.assertEqual(mock_download_report.call_count, 3)
204
+ mock_rerun_report.assert_not_called()
205
+
206
+ @patch("regscale.integrations.commercial.wizv2.utils.download_report")
207
+ @patch("regscale.integrations.commercial.wizv2.utils.rerun_expired_report")
208
+ @patch("regscale.integrations.commercial.wizv2.utils.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
209
+ @patch("regscale.integrations.commercial.wizv2.utils.MAX_RETRIES", 3)
210
+ def test_get_report_url_and_status_multiple_attempts_before_completion(
211
+ self, mock_rerun_report, mock_download_report
212
+ ):
213
+ """Test get_report_url_and_status with multiple attempts before completion."""
214
+ # Mock responses: first two with unknown status, third with completed
215
+ mock_response1 = MagicMock()
216
+ mock_response1.ok = True
217
+ mock_response1.json.return_value = {"data": {"report": {"lastRun": {"status": "PROCESSING"}}}}
218
+
219
+ mock_response2 = MagicMock()
220
+ mock_response2.ok = True
221
+ mock_response2.json.return_value = {"data": {"report": {"lastRun": {"status": "PROCESSING"}}}}
222
+
223
+ mock_response3 = MagicMock()
224
+ mock_response3.ok = True
225
+ mock_response3.json.return_value = {
226
+ "data": {"report": {"lastRun": {"status": "COMPLETED", "url": "https://example.com/report.csv"}}}
227
+ }
228
+
229
+ # Configure mock to return different responses on subsequent calls
230
+ mock_download_report.side_effect = [mock_response1, mock_response2, mock_response3]
231
+
232
+ with patch("time.sleep") as mock_sleep:
233
+ # Call the function
234
+ result = get_report_url_and_status("test-report-id")
235
+
236
+ # Verify the result
237
+ self.assertEqual(result, "https://example.com/report.csv")
238
+ self.assertEqual(mock_download_report.call_count, 3)
239
+ # Should sleep twice (after first and second attempts)
240
+ self.assertEqual(mock_sleep.call_count, 2)
241
+ mock_rerun_report.assert_not_called()
242
+
243
+ @patch("regscale.integrations.commercial.wizv2.utils.download_report")
244
+ @patch("regscale.integrations.commercial.wizv2.utils.rerun_expired_report")
245
+ @patch("regscale.integrations.commercial.wizv2.utils.MAX_RETRIES", 3)
246
+ def test_get_report_url_and_status_rate_limit(self, mock_rerun_report, mock_download_report):
247
+ """Test get_report_url_and_status with rate limit error."""
248
+ from regscale.integrations.commercial.wizv2.constants import RATE_LIMIT_MSG
249
+ import time
250
+
251
+ # Mock response with rate limit error
252
+ mock_response = MagicMock()
253
+ mock_response.ok = True
254
+ mock_response.json.return_value = {
255
+ "errors": [{"message": "Rate limit exceeded", "extensions": {"retryAfter": 5}}]
256
+ }
257
+ mock_download_report.return_value = mock_response
258
+
259
+ # Mock second response for successful completion
260
+ mock_response2 = MagicMock()
261
+ mock_response2.ok = True
262
+ mock_response2.json.return_value = {
263
+ "data": {"report": {"lastRun": {"status": "COMPLETED", "url": "https://example.com/report.csv"}}}
264
+ }
265
+
266
+ # Configure mock to return different responses on subsequent calls
267
+ mock_download_report.side_effect = [mock_response, mock_response2]
268
+
269
+ with patch("time.sleep") as mock_sleep:
270
+ # Call the function
271
+ result = get_report_url_and_status("test-report-id")
272
+
273
+ # Verify the result
274
+ self.assertEqual(result, "https://example.com/report.csv")
275
+ self.assertEqual(mock_download_report.call_count, 2)
276
+ # Check that time.sleep was called with the rate limit retry value (5)
277
+ mock_sleep.assert_any_call(5)
278
+ mock_rerun_report.assert_not_called()
279
+
280
+ @patch("regscale.integrations.commercial.wizv2.utils.download_report")
281
+ @patch("regscale.integrations.commercial.wizv2.utils.rerun_expired_report")
282
+ @patch("regscale.integrations.commercial.wizv2.utils.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
283
+ def test_get_report_url_and_status_other_error(self, mock_rerun_report, mock_download_report):
284
+ """Test get_report_url_and_status with other error in response."""
285
+ # Mock response with other error
286
+ mock_response = MagicMock()
287
+ mock_response.ok = True
288
+ mock_response.json.return_value = {"errors": [{"message": "Some other error occurred"}]}
289
+ mock_download_report.return_value = mock_response
290
+
291
+ # Call the function and expect exception after max retries
292
+ with self.assertRaises(Exception) as context:
293
+ get_report_url_and_status("test-report-id")
294
+
295
+ self.assertIn("Download failed, exceeding the maximum number of retries", str(context.exception))
296
+ # Should be called MAX_RETRIES times
297
+ self.assertEqual(mock_download_report.call_count, 100)
298
+ mock_rerun_report.assert_not_called()
299
+
300
+ @patch("regscale.integrations.commercial.wizv2.utils.download_report")
301
+ @patch("regscale.integrations.commercial.wizv2.utils.rerun_expired_report")
302
+ @patch("regscale.integrations.commercial.wizv2.utils.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
303
+ def test_get_report_url_and_status_unknown_status(self, mock_rerun_report, mock_download_report):
304
+ """Test get_report_url_and_status with unknown status."""
305
+ # Mock response with unknown status
306
+ mock_response = MagicMock()
307
+ mock_response.ok = True
308
+ mock_response.json.return_value = {"data": {"report": {"lastRun": {"status": "UNKNOWN_STATUS"}}}}
309
+ mock_download_report.return_value = mock_response
310
+
311
+ # Call the function and expect exception after max retries
312
+ with self.assertRaises(Exception) as context:
313
+ get_report_url_and_status("test-report-id")
314
+
315
+ self.assertIn("Download failed, exceeding the maximum number of retries", str(context.exception))
316
+ # Should be called MAX_RETRIES times
317
+ self.assertEqual(mock_download_report.call_count, 100)
318
+ mock_rerun_report.assert_not_called()
319
+
320
+ @patch("regscale.integrations.commercial.wizv2.utils.download_report")
321
+ @patch("regscale.integrations.commercial.wizv2.utils.rerun_expired_report")
322
+ @patch("regscale.integrations.commercial.wizv2.utils.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
323
+ def test_get_report_url_and_status_missing_status(self, mock_rerun_report, mock_download_report):
324
+ """Test get_report_url_and_status with missing status in response."""
325
+ # Mock response with missing status
326
+ mock_response = MagicMock()
327
+ mock_response.ok = True
328
+ mock_response.json.return_value = {"data": {"report": {"lastRun": {}}}}
329
+ mock_download_report.return_value = mock_response
330
+
331
+ # Call the function and expect exception after max retries
332
+ with self.assertRaises(Exception) as context:
333
+ get_report_url_and_status("test-report-id")
334
+
335
+ self.assertIn("Download failed, exceeding the maximum number of retries", str(context.exception))
336
+ # Should be called MAX_RETRIES times
337
+ self.assertEqual(mock_download_report.call_count, 100)
338
+ mock_rerun_report.assert_not_called()
339
+
340
+ @patch("regscale.integrations.commercial.wizv2.utils.download_report")
341
+ @patch("regscale.integrations.commercial.wizv2.utils.rerun_expired_report")
342
+ @patch("regscale.integrations.commercial.wizv2.utils.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
343
+ def test_get_report_url_and_status_missing_data(self, mock_rerun_report, mock_download_report):
344
+ """Test get_report_url_and_status with missing data in response."""
345
+ # Mock response with missing data
346
+ mock_response = MagicMock()
347
+ mock_response.ok = True
348
+ mock_response.json.return_value = {}
349
+ mock_download_report.return_value = mock_response
350
+
351
+ # Call the function and expect exception after max retries
352
+ with self.assertRaises(Exception) as context:
353
+ get_report_url_and_status("test-report-id")
354
+
355
+ self.assertIn("Download failed, exceeding the maximum number of retries", str(context.exception))
356
+ # Should be called MAX_RETRIES times
357
+ self.assertEqual(mock_download_report.call_count, 100)
358
+ mock_rerun_report.assert_not_called()
359
+
360
+
361
+ class TestGetOrCreateReportId(unittest.TestCase):
362
+ """Test cases for get_or_create_report_id function."""
363
+
364
+ def setUp(self):
365
+ """Set up test fixtures."""
366
+ self.project_id = "test-project-123"
367
+ self.frameworks = ["NIST_SP_800-53_Revision_5", "NIST_CSF_v1.1"]
368
+ self.wiz_frameworks = [
369
+ {"id": "framework-1", "name": "NIST SP 800-53 Revision 5"},
370
+ {"id": "framework-2", "name": "NIST CSF v1.1"},
371
+ ]
372
+ self.target_framework = "NIST_SP_800-53_Revision_5"
373
+
374
+ @patch("regscale.integrations.commercial.wizv2.utils.Application")
375
+ @patch("regscale.integrations.commercial.wizv2.utils.is_report_expired")
376
+ def test_get_or_create_report_id_existing_valid_report(self, mock_is_expired, mock_application):
377
+ """Test returning existing report ID when report is valid (not expired)."""
378
+ # Mock Application
379
+ mock_app = MagicMock()
380
+ mock_app.config.get.return_value = 15
381
+ mock_application.return_value = mock_app
382
+
383
+ # Mock is_report_expired to return False (not expired)
384
+ mock_is_expired.return_value = False
385
+
386
+ # Mock existing reports with a valid report
387
+ existing_reports = [
388
+ {
389
+ "id": "existing-report-123",
390
+ "name": "NIST_SP_800-53_Revision_5_project_test-project-123",
391
+ "lastRun": {"runAt": "2023-07-15T14:37:55.450532Z"},
392
+ }
393
+ ]
394
+
395
+ result = get_or_create_report_id(
396
+ self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, self.target_framework
397
+ )
398
+
399
+ self.assertEqual(result, "existing-report-123")
400
+ mock_app.config.get.assert_called_once_with("wizReportAge", 15)
401
+ mock_is_expired.assert_called_once_with("2023-07-15T14:37:55.450532Z", 15)
402
+
403
+ @patch("regscale.integrations.commercial.wizv2.utils.Application")
404
+ @patch("regscale.integrations.commercial.wizv2.utils.is_report_expired")
405
+ @patch("regscale.integrations.commercial.wizv2.utils.create_compliance_report")
406
+ def test_get_or_create_report_id_existing_expired_report(
407
+ self, mock_create_report, mock_is_expired, mock_application
408
+ ):
409
+ """Test creating new report when existing report is expired."""
410
+ # Mock Application
411
+ mock_app = MagicMock()
412
+ mock_app.config.get.return_value = 15
413
+ mock_application.return_value = mock_app
414
+
415
+ # Mock is_report_expired to return True (expired)
416
+ mock_is_expired.return_value = True
417
+
418
+ # Mock create_compliance_report to return new report ID
419
+ mock_create_report.return_value = "new-report-456"
420
+
421
+ # Mock existing reports with an expired report
422
+ existing_reports = [
423
+ {
424
+ "id": "existing-report-123",
425
+ "name": "NIST_SP_800-53_Revision_5_project_test-project-123",
426
+ "lastRun": {"runAt": "2023-06-01T14:37:55.450532Z"}, # Old date
427
+ }
428
+ ]
429
+
430
+ result = get_or_create_report_id(
431
+ self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, self.target_framework
432
+ )
433
+
434
+ self.assertEqual(result, "new-report-456")
435
+ mock_app.config.get.assert_called_once_with("wizReportAge", 15)
436
+ mock_is_expired.assert_called_once_with("2023-06-01T14:37:55.450532Z", 15)
437
+ mock_create_report.assert_called_once_with(
438
+ wiz_project_id=self.project_id,
439
+ report_name="NIST_SP_800-53_Revision_5_project_test-project-123",
440
+ framework_id="framework-1",
441
+ )
442
+
443
+ @patch("regscale.integrations.commercial.wizv2.utils.Application")
444
+ @patch("regscale.integrations.commercial.wizv2.utils.create_compliance_report")
445
+ def test_get_or_create_report_id_no_existing_report(self, mock_create_report, mock_application):
446
+ """Test creating new report when no existing report is found."""
447
+ # Mock Application
448
+ mock_app = MagicMock()
449
+ mock_app.config.get.return_value = 15
450
+ mock_application.return_value = mock_app
451
+
452
+ # Mock create_compliance_report to return new report ID
453
+ mock_create_report.return_value = "new-report-789"
454
+
455
+ # Empty existing reports list
456
+ existing_reports = []
457
+
458
+ result = get_or_create_report_id(
459
+ self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, self.target_framework
460
+ )
461
+
462
+ self.assertEqual(result, "new-report-789")
463
+ mock_app.config.get.assert_called_once_with("wizReportAge", 15)
464
+ mock_create_report.assert_called_once_with(
465
+ wiz_project_id=self.project_id,
466
+ report_name="NIST_SP_800-53_Revision_5_project_test-project-123",
467
+ framework_id="framework-1",
468
+ )
469
+
470
+ @patch("regscale.integrations.commercial.wizv2.utils.Application")
471
+ @patch("regscale.integrations.commercial.wizv2.utils.is_report_expired")
472
+ def test_get_or_create_report_id_missing_run_at(self, mock_is_expired, mock_application):
473
+ """Test behavior when existing report has no runAt timestamp."""
474
+ # Mock Application
475
+ mock_app = MagicMock()
476
+ mock_app.config.get.return_value = 15
477
+ mock_application.return_value = mock_app
478
+
479
+ # Mock existing reports with missing runAt
480
+ existing_reports = [
481
+ {
482
+ "id": "existing-report-123",
483
+ "name": "NIST_SP_800-53_Revision_5_project_test-project-123",
484
+ "lastRun": {}, # No runAt timestamp
485
+ }
486
+ ]
487
+
488
+ result = get_or_create_report_id(
489
+ self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, self.target_framework
490
+ )
491
+
492
+ # When runAt is missing, the method returns the existing report
493
+ # because the condition `if run_at and is_report_expired(run_at, report_age_days):`
494
+ # is False when run_at is None/missing
495
+ self.assertEqual(result, "existing-report-123")
496
+ mock_app.config.get.assert_called_once_with("wizReportAge", 15)
497
+ # is_report_expired should not be called when runAt is missing
498
+ mock_is_expired.assert_not_called()
499
+
500
+ @patch("regscale.integrations.commercial.wizv2.utils.Application")
501
+ def test_get_or_create_report_id_framework_not_found(self, mock_application):
502
+ """Test ValueError when target framework is not in frameworks list."""
503
+ # Mock Application
504
+ mock_app = MagicMock()
505
+ mock_app.config.get.return_value = 15
506
+ mock_application.return_value = mock_app
507
+
508
+ # Use a framework not in the frameworks list
509
+ invalid_framework = "INVALID_FRAMEWORK"
510
+
511
+ existing_reports = []
512
+
513
+ with self.assertRaises(ValueError) as context:
514
+ get_or_create_report_id(
515
+ self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, invalid_framework
516
+ )
517
+
518
+ # The actual error message from list.index() is different
519
+ self.assertIn("is not in list", str(context.exception))
520
+
521
+ @patch("regscale.integrations.commercial.wizv2.utils.Application")
522
+ @patch("regscale.integrations.commercial.wizv2.utils.is_report_expired")
523
+ def test_get_or_create_report_id_custom_report_age(self, mock_is_expired, mock_application):
524
+ """Test using custom wizReportAge configuration."""
525
+ # Mock Application with custom age
526
+ mock_app = MagicMock()
527
+ mock_app.config.get.return_value = 30 # 30 days instead of default 15
528
+ mock_application.return_value = mock_app
529
+
530
+ # Mock is_report_expired to return False
531
+ mock_is_expired.return_value = False
532
+
533
+ # Mock existing reports with a valid report
534
+ existing_reports = [
535
+ {
536
+ "id": "existing-report-123",
537
+ "name": "NIST_SP_800-53_Revision_5_project_test-project-123",
538
+ "lastRun": {"runAt": "2023-07-15T14:37:55.450532Z"},
539
+ }
540
+ ]
541
+
542
+ result = get_or_create_report_id(
543
+ self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, self.target_framework
544
+ )
545
+
546
+ self.assertEqual(result, "existing-report-123")
547
+ mock_app.config.get.assert_called_once_with("wizReportAge", 15)
548
+ mock_is_expired.assert_called_once_with("2023-07-15T14:37:55.450532Z", 30)
549
+
550
+ @patch("regscale.integrations.commercial.wizv2.utils.Application")
551
+ @patch("regscale.integrations.commercial.wizv2.utils.is_report_expired")
552
+ @patch("regscale.integrations.commercial.wizv2.utils.create_compliance_report")
553
+ def test_get_or_create_report_id_multiple_reports_first_match(
554
+ self, mock_create_report, mock_is_expired, mock_application
555
+ ):
556
+ """Test behavior when multiple reports exist, should use first matching report."""
557
+ # Mock Application
558
+ mock_app = MagicMock()
559
+ mock_app.config.get.return_value = 15
560
+ mock_application.return_value = mock_app
561
+
562
+ # Mock is_report_expired to return False for first call
563
+ mock_is_expired.return_value = False
564
+
565
+ # Mock existing reports with multiple matching reports
566
+ existing_reports = [
567
+ {
568
+ "id": "first-report-123",
569
+ "name": "NIST_SP_800-53_Revision_5_project_test-project-123",
570
+ "lastRun": {"runAt": "2023-07-15T14:37:55.450532Z"},
571
+ },
572
+ {
573
+ "id": "second-report-456",
574
+ "name": "NIST_SP_800-53_Revision_5_project_test-project-123",
575
+ "lastRun": {"runAt": "2023-07-16T14:37:55.450532Z"},
576
+ },
577
+ ]
578
+
579
+ result = get_or_create_report_id(
580
+ self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, self.target_framework
581
+ )
582
+
583
+ # Should return the first matching report
584
+ self.assertEqual(result, "first-report-123")
585
+ mock_is_expired.assert_called_once_with("2023-07-15T14:37:55.450532Z", 15)
586
+ mock_create_report.assert_not_called()
587
+
588
+ @patch("regscale.integrations.commercial.wizv2.utils.Application")
589
+ @patch("regscale.integrations.commercial.wizv2.utils.is_report_expired")
590
+ @patch("regscale.integrations.commercial.wizv2.utils.create_compliance_report")
591
+ def test_get_or_create_report_id_different_report_names(
592
+ self, mock_create_report, mock_is_expired, mock_application
593
+ ):
594
+ """Test creating new report when existing reports have different names."""
595
+ # Mock Application
596
+ mock_app = MagicMock()
597
+ mock_app.config.get.return_value = 15
598
+ mock_application.return_value = mock_app
599
+
600
+ # Mock create_compliance_report to return new report ID
601
+ mock_create_report.return_value = "new-report-999"
602
+
603
+ # Mock existing reports with different names
604
+ existing_reports = [
605
+ {
606
+ "id": "other-report-123",
607
+ "name": "OTHER_FRAMEWORK_project_test-project-123",
608
+ "lastRun": {"runAt": "2023-07-15T14:37:55.450532Z"},
609
+ }
610
+ ]
611
+
612
+ result = get_or_create_report_id(
613
+ self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, self.target_framework
614
+ )
615
+
616
+ self.assertEqual(result, "new-report-999")
617
+ mock_app.config.get.assert_called_once_with("wizReportAge", 15)
618
+ # is_report_expired should not be called since no matching report name
619
+ mock_is_expired.assert_not_called()
620
+ mock_create_report.assert_called_once()
621
+
622
+
623
+ if __name__ == "__main__":
624
+ unittest.main()
@@ -0,0 +1 @@
1
+ """Tests for FedRAMP integrations"""