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,519 @@
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.main.time.sleep")
18
+ @patch("regscale.integrations.commercial.wizv2.utils.main.download_report")
19
+ @patch("regscale.integrations.commercial.wizv2.utils.main.rerun_expired_report")
20
+ @patch("regscale.integrations.commercial.wizv2.utils.main.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
21
+ @patch("regscale.integrations.commercial.wizv2.utils.main.MAX_RETRIES", 3)
22
+ def test_get_report_url_and_status_completed(self, mock_rerun_report, mock_download_report, mock_sleep):
23
+ """Test get_report_url_and_status with COMPLETED status."""
24
+ # Mock response for completed report
25
+ mock_response = MagicMock()
26
+ mock_response.ok = True
27
+ mock_response.json.return_value = {
28
+ "data": {"report": {"lastRun": {"status": "COMPLETED", "url": "https://example.com/report.csv"}}}
29
+ }
30
+ mock_download_report.return_value = mock_response
31
+
32
+ # Call the function
33
+ result = get_report_url_and_status("test-report-id")
34
+
35
+ # Verify the result
36
+ self.assertEqual(result, "https://example.com/report.csv")
37
+ mock_download_report.assert_called_once_with({"reportId": "test-report-id"})
38
+ mock_rerun_report.assert_not_called()
39
+ mock_sleep.assert_not_called() # Should not sleep on first success
40
+
41
+ @patch("regscale.integrations.commercial.wizv2.utils.main.time.sleep")
42
+ @patch("regscale.integrations.commercial.wizv2.utils.main.download_report")
43
+ @patch("regscale.integrations.commercial.wizv2.utils.main.rerun_expired_report")
44
+ @patch("regscale.integrations.commercial.wizv2.utils.main.get_report_url_and_status")
45
+ @patch("regscale.integrations.commercial.wizv2.utils.main.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
46
+ @patch("regscale.integrations.commercial.wizv2.utils.main.MAX_RETRIES", 3)
47
+ def test_get_report_url_and_status_expired(
48
+ self, mock_recursive_call, mock_rerun_report, mock_download_report, mock_sleep
49
+ ):
50
+ """Test get_report_url_and_status with EXPIRED status."""
51
+ # Mock response for expired report
52
+ mock_response = MagicMock()
53
+ mock_response.ok = True
54
+ mock_response.json.return_value = {"data": {"report": {"lastRun": {"status": "EXPIRED"}}}}
55
+ mock_download_report.return_value = mock_response
56
+
57
+ # Mock rerun response
58
+ mock_rerun_response = MagicMock()
59
+ mock_rerun_response.ok = True
60
+ mock_rerun_report.return_value = mock_rerun_response
61
+
62
+ # Mock recursive call to return final URL
63
+ mock_recursive_call.return_value = "https://example.com/new-report.csv"
64
+
65
+ # Call the function
66
+ result = get_report_url_and_status("test-report-id")
67
+
68
+ # Verify the result
69
+ self.assertEqual(result, "https://example.com/new-report.csv")
70
+ mock_download_report.assert_called_once_with({"reportId": "test-report-id"})
71
+ mock_rerun_report.assert_called_once_with({"reportId": "test-report-id"})
72
+ mock_recursive_call.assert_called_once_with("test-report-id")
73
+ mock_sleep.assert_not_called() # Should not sleep on first call before recursion
74
+
75
+ @patch("regscale.integrations.commercial.wizv2.utils.main.time.sleep")
76
+ @patch("regscale.integrations.commercial.wizv2.utils.main.download_report")
77
+ @patch("regscale.integrations.commercial.wizv2.utils.main.rerun_expired_report")
78
+ @patch("regscale.integrations.commercial.wizv2.utils.main.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
79
+ @patch("regscale.integrations.commercial.wizv2.utils.main.MAX_RETRIES", 5)
80
+ def test_get_report_url_and_status_rate_limit(self, mock_rerun_report, mock_download_report, mock_sleep):
81
+ """Test get_report_url_and_status with rate limit error."""
82
+ from regscale.integrations.commercial.wizv2.core.constants import RATE_LIMIT_MSG
83
+
84
+ # Mock response with rate limit error
85
+ mock_response = MagicMock()
86
+ mock_response.ok = True
87
+ mock_response.json.return_value = {
88
+ "errors": [
89
+ {"message": "Rate limit exceeded", "extensions": {"retryAfter": 0.001}} # Reduced to milliseconds
90
+ ]
91
+ }
92
+ mock_download_report.return_value = mock_response
93
+
94
+ # Mock second response for successful completion
95
+ mock_response2 = MagicMock()
96
+ mock_response2.ok = True
97
+ mock_response2.json.return_value = {
98
+ "data": {"report": {"lastRun": {"status": "COMPLETED", "url": "https://example.com/report.csv"}}}
99
+ }
100
+
101
+ # Configure mock to return different responses on subsequent calls
102
+ mock_download_report.side_effect = [mock_response, mock_response2]
103
+
104
+ # Call the function
105
+ result = get_report_url_and_status("test-report-id")
106
+
107
+ # Verify the result
108
+ self.assertEqual(result, "https://example.com/report.csv")
109
+ self.assertEqual(mock_download_report.call_count, 2)
110
+ mock_rerun_report.assert_not_called()
111
+ # Sleep is called twice: once for rate limit, once for retry interval
112
+ self.assertEqual(mock_sleep.call_count, 2)
113
+
114
+ @patch("regscale.integrations.commercial.wizv2.utils.main.time.sleep")
115
+ @patch("regscale.integrations.commercial.wizv2.utils.main.download_report")
116
+ @patch("regscale.integrations.commercial.wizv2.utils.main.rerun_expired_report")
117
+ @patch("regscale.integrations.commercial.wizv2.utils.main.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
118
+ @patch("regscale.integrations.commercial.wizv2.utils.main.MAX_RETRIES", 3)
119
+ def test_get_report_url_and_status_failed_response(self, mock_rerun_report, mock_download_report, mock_sleep):
120
+ """Test get_report_url_and_status with failed response."""
121
+ # Mock failed response
122
+ mock_response = MagicMock()
123
+ mock_response.ok = False
124
+ mock_download_report.return_value = mock_response
125
+
126
+ # Call the function and expect exception
127
+ with self.assertRaises(Exception) as context:
128
+ get_report_url_and_status("test-report-id")
129
+
130
+ self.assertIn("Failed to download report", str(context.exception))
131
+ mock_download_report.assert_called_once_with({"reportId": "test-report-id"})
132
+ mock_rerun_report.assert_not_called()
133
+
134
+ @patch("regscale.integrations.commercial.wizv2.utils.main.time.sleep")
135
+ @patch("regscale.integrations.commercial.wizv2.utils.main.download_report")
136
+ @patch("regscale.integrations.commercial.wizv2.utils.main.rerun_expired_report")
137
+ @patch("regscale.integrations.commercial.wizv2.utils.main.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
138
+ @patch("regscale.integrations.commercial.wizv2.utils.main.MAX_RETRIES", 3)
139
+ def test_get_report_url_and_status_none_response(self, mock_rerun_report, mock_download_report, mock_sleep):
140
+ """Test get_report_url_and_status with None response."""
141
+ # Mock None response
142
+ mock_download_report.return_value = None
143
+
144
+ # Call the function and expect exception
145
+ with self.assertRaises(Exception) as context:
146
+ get_report_url_and_status("test-report-id")
147
+
148
+ self.assertIn("Failed to download report", str(context.exception))
149
+ mock_download_report.assert_called_once_with({"reportId": "test-report-id"})
150
+ mock_rerun_report.assert_not_called()
151
+
152
+ @patch("regscale.integrations.commercial.wizv2.utils.main.time.sleep")
153
+ @patch("regscale.integrations.commercial.wizv2.utils.main.download_report")
154
+ @patch("regscale.integrations.commercial.wizv2.utils.main.rerun_expired_report")
155
+ @patch("regscale.integrations.commercial.wizv2.utils.main.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
156
+ @patch("regscale.integrations.commercial.wizv2.utils.main.MAX_RETRIES", 3) # Reduce retries for faster testing
157
+ def test_get_report_url_and_status_other_error(self, mock_rerun_report, mock_download_report, mock_sleep):
158
+ """Test get_report_url_and_status with other error in response."""
159
+ # Mock response with other error
160
+ mock_response = MagicMock()
161
+ mock_response.ok = True
162
+ mock_response.json.return_value = {"errors": [{"message": "Some other error occurred"}]}
163
+ mock_download_report.return_value = mock_response
164
+
165
+ # Call the function and expect exception after max retries
166
+ with self.assertRaises(Exception) as context:
167
+ get_report_url_and_status("test-report-id")
168
+
169
+ self.assertIn("Download failed, exceeding the maximum number of retries", str(context.exception))
170
+ # Should be called MAX_RETRIES times (now 3)
171
+ self.assertEqual(mock_download_report.call_count, 3)
172
+ mock_rerun_report.assert_not_called()
173
+
174
+ @patch("regscale.integrations.commercial.wizv2.utils.main.time.sleep")
175
+ @patch("regscale.integrations.commercial.wizv2.utils.main.download_report")
176
+ @patch("regscale.integrations.commercial.wizv2.utils.main.rerun_expired_report")
177
+ @patch("regscale.integrations.commercial.wizv2.utils.main.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
178
+ @patch("regscale.integrations.commercial.wizv2.utils.main.MAX_RETRIES", 3) # Reduce retries for faster testing
179
+ def test_get_report_url_and_status_unknown_status(self, mock_rerun_report, mock_download_report, mock_sleep):
180
+ """Test get_report_url_and_status with unknown status."""
181
+ # Mock response with unknown status
182
+ mock_response = MagicMock()
183
+ mock_response.ok = True
184
+ mock_response.json.return_value = {"data": {"report": {"lastRun": {"status": "UNKNOWN_STATUS"}}}}
185
+ mock_download_report.return_value = mock_response
186
+
187
+ # Call the function and expect exception after max retries
188
+ with self.assertRaises(Exception) as context:
189
+ get_report_url_and_status("test-report-id")
190
+
191
+ self.assertIn("Download failed, exceeding the maximum number of retries", str(context.exception))
192
+ # Should be called MAX_RETRIES times (now 3)
193
+ self.assertEqual(mock_download_report.call_count, 3)
194
+ mock_rerun_report.assert_not_called()
195
+
196
+ @patch("regscale.integrations.commercial.wizv2.utils.main.time.sleep")
197
+ @patch("regscale.integrations.commercial.wizv2.utils.main.download_report")
198
+ @patch("regscale.integrations.commercial.wizv2.utils.main.rerun_expired_report")
199
+ @patch("regscale.integrations.commercial.wizv2.utils.main.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
200
+ @patch("regscale.integrations.commercial.wizv2.utils.main.MAX_RETRIES", 3) # Reduce retries for faster testing
201
+ def test_get_report_url_and_status_missing_status(self, mock_rerun_report, mock_download_report, mock_sleep):
202
+ """Test get_report_url_and_status with missing status in response."""
203
+ # Mock response with missing status
204
+ mock_response = MagicMock()
205
+ mock_response.ok = True
206
+ mock_response.json.return_value = {"data": {"report": {"lastRun": {}}}}
207
+ mock_download_report.return_value = mock_response
208
+
209
+ # Call the function and expect exception after max retries
210
+ with self.assertRaises(Exception) as context:
211
+ get_report_url_and_status("test-report-id")
212
+
213
+ self.assertIn("Download failed, exceeding the maximum number of retries", str(context.exception))
214
+ # Should be called MAX_RETRIES times (now 3)
215
+ self.assertEqual(mock_download_report.call_count, 3)
216
+ mock_rerun_report.assert_not_called()
217
+
218
+ @patch("regscale.integrations.commercial.wizv2.utils.main.time.sleep")
219
+ @patch("regscale.integrations.commercial.wizv2.utils.main.download_report")
220
+ @patch("regscale.integrations.commercial.wizv2.utils.main.rerun_expired_report")
221
+ @patch("regscale.integrations.commercial.wizv2.utils.main.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
222
+ @patch("regscale.integrations.commercial.wizv2.utils.main.MAX_RETRIES", 3)
223
+ def test_get_report_url_and_status_multiple_attempts_before_completion(
224
+ self, mock_rerun_report, mock_download_report, mock_sleep
225
+ ):
226
+ """Test get_report_url_and_status with multiple attempts before completion."""
227
+ # Mock responses: first two with unknown status, third with completed
228
+ mock_response1 = MagicMock()
229
+ mock_response1.ok = True
230
+ mock_response1.json.return_value = {"data": {"report": {"lastRun": {"status": "PROCESSING"}}}}
231
+
232
+ mock_response2 = MagicMock()
233
+ mock_response2.ok = True
234
+ mock_response2.json.return_value = {"data": {"report": {"lastRun": {"status": "PROCESSING"}}}}
235
+
236
+ mock_response3 = MagicMock()
237
+ mock_response3.ok = True
238
+ mock_response3.json.return_value = {
239
+ "data": {"report": {"lastRun": {"status": "COMPLETED", "url": "https://example.com/report.csv"}}}
240
+ }
241
+
242
+ # Configure mock to return different responses on subsequent calls
243
+ mock_download_report.side_effect = [mock_response1, mock_response2, mock_response3]
244
+
245
+ # Call the function
246
+ result = get_report_url_and_status("test-report-id")
247
+
248
+ # Verify the result
249
+ self.assertEqual(result, "https://example.com/report.csv")
250
+ self.assertEqual(mock_download_report.call_count, 3)
251
+ # Should sleep twice (after first and second attempts)
252
+ self.assertEqual(mock_sleep.call_count, 2)
253
+ mock_rerun_report.assert_not_called()
254
+
255
+
256
+ class TestGetOrCreateReportId(unittest.TestCase):
257
+ """Test cases for get_or_create_report_id function."""
258
+
259
+ def setUp(self):
260
+ """Set up test fixtures."""
261
+ self.project_id = "test-project-123"
262
+ self.frameworks = ["NIST_SP_800-53_Revision_5", "NIST_CSF_v1.1"]
263
+ self.wiz_frameworks = [
264
+ {"id": "framework-1", "name": "NIST SP 800-53 Revision 5"},
265
+ {"id": "framework-2", "name": "NIST CSF v1.1"},
266
+ ]
267
+ self.target_framework = "NIST_SP_800-53_Revision_5"
268
+
269
+ @patch("regscale.integrations.commercial.wizv2.utils.main.Application")
270
+ @patch("regscale.integrations.commercial.wizv2.utils.main.is_report_expired")
271
+ def test_get_or_create_report_id_existing_valid_report(self, mock_is_expired, mock_application):
272
+ """Test returning existing report ID when report is valid (not expired)."""
273
+ # Mock Application
274
+ mock_app = MagicMock()
275
+ mock_app.config.get.return_value = 15
276
+ mock_application.return_value = mock_app
277
+
278
+ # Mock is_report_expired to return False (not expired)
279
+ mock_is_expired.return_value = False
280
+
281
+ # Mock existing reports with a valid report
282
+ existing_reports = [
283
+ {
284
+ "id": "existing-report-123",
285
+ "name": "NIST_SP_800-53_Revision_5_project_test-project-123",
286
+ "lastRun": {"runAt": "2023-07-15T14:37:55.450532Z"},
287
+ }
288
+ ]
289
+
290
+ result = get_or_create_report_id(
291
+ self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, self.target_framework
292
+ )
293
+
294
+ self.assertEqual(result, "existing-report-123")
295
+ mock_app.config.get.assert_called_once_with("wizReportAge", 15)
296
+ mock_is_expired.assert_called_once_with("2023-07-15T14:37:55.450532Z", 15)
297
+
298
+ @patch("regscale.integrations.commercial.wizv2.utils.main.Application")
299
+ @patch("regscale.integrations.commercial.wizv2.utils.main.is_report_expired")
300
+ @patch("regscale.integrations.commercial.wizv2.utils.main.create_compliance_report")
301
+ def test_get_or_create_report_id_existing_expired_report(
302
+ self, mock_create_report, mock_is_expired, mock_application
303
+ ):
304
+ """Test creating new report when existing report is expired."""
305
+ # Mock Application
306
+ mock_app = MagicMock()
307
+ mock_app.config.get.return_value = 15
308
+ mock_application.return_value = mock_app
309
+
310
+ # Mock is_report_expired to return True (expired)
311
+ mock_is_expired.return_value = True
312
+
313
+ # Mock create_compliance_report to return new report ID
314
+ mock_create_report.return_value = "new-report-456"
315
+
316
+ # Mock existing reports with an expired report
317
+ existing_reports = [
318
+ {
319
+ "id": "existing-report-123",
320
+ "name": "NIST_SP_800-53_Revision_5_project_test-project-123",
321
+ "lastRun": {"runAt": "2023-06-01T14:37:55.450532Z"}, # Old date
322
+ }
323
+ ]
324
+
325
+ result = get_or_create_report_id(
326
+ self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, self.target_framework
327
+ )
328
+
329
+ self.assertEqual(result, "new-report-456")
330
+ mock_app.config.get.assert_called_once_with("wizReportAge", 15)
331
+ mock_is_expired.assert_called_once_with("2023-06-01T14:37:55.450532Z", 15)
332
+ mock_create_report.assert_called_once_with(
333
+ wiz_project_id=self.project_id,
334
+ report_name="NIST_SP_800-53_Revision_5_project_test-project-123",
335
+ framework_id="framework-1",
336
+ )
337
+
338
+ @patch("regscale.integrations.commercial.wizv2.utils.main.Application")
339
+ @patch("regscale.integrations.commercial.wizv2.utils.main.create_compliance_report")
340
+ def test_get_or_create_report_id_no_existing_report(self, mock_create_report, mock_application):
341
+ """Test creating new report when no existing report is found."""
342
+ # Mock Application
343
+ mock_app = MagicMock()
344
+ mock_app.config.get.return_value = 15
345
+ mock_application.return_value = mock_app
346
+
347
+ # Mock create_compliance_report to return new report ID
348
+ mock_create_report.return_value = "new-report-789"
349
+
350
+ # Empty existing reports list
351
+ existing_reports = []
352
+
353
+ result = get_or_create_report_id(
354
+ self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, self.target_framework
355
+ )
356
+
357
+ self.assertEqual(result, "new-report-789")
358
+ mock_app.config.get.assert_called_once_with("wizReportAge", 15)
359
+ mock_create_report.assert_called_once_with(
360
+ wiz_project_id=self.project_id,
361
+ report_name="NIST_SP_800-53_Revision_5_project_test-project-123",
362
+ framework_id="framework-1",
363
+ )
364
+
365
+ @patch("regscale.integrations.commercial.wizv2.utils.main.Application")
366
+ @patch("regscale.integrations.commercial.wizv2.utils.main.is_report_expired")
367
+ def test_get_or_create_report_id_missing_run_at(self, mock_is_expired, mock_application):
368
+ """Test behavior when existing report has no runAt timestamp."""
369
+ # Mock Application
370
+ mock_app = MagicMock()
371
+ mock_app.config.get.return_value = 15
372
+ mock_application.return_value = mock_app
373
+
374
+ # Mock existing reports with missing runAt
375
+ existing_reports = [
376
+ {
377
+ "id": "existing-report-123",
378
+ "name": "NIST_SP_800-53_Revision_5_project_test-project-123",
379
+ "lastRun": {}, # No runAt timestamp
380
+ }
381
+ ]
382
+
383
+ result = get_or_create_report_id(
384
+ self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, self.target_framework
385
+ )
386
+
387
+ # When runAt is missing, the method returns the existing report
388
+ # because the condition `if run_at and is_report_expired(run_at, report_age_days):`
389
+ # is False when run_at is None/missing
390
+ self.assertEqual(result, "existing-report-123")
391
+ mock_app.config.get.assert_called_once_with("wizReportAge", 15)
392
+ # is_report_expired should not be called when runAt is missing
393
+ mock_is_expired.assert_not_called()
394
+
395
+ @patch("regscale.integrations.commercial.wizv2.utils.main.Application")
396
+ def test_get_or_create_report_id_framework_not_found(self, mock_application):
397
+ """Test ValueError when target framework is not in frameworks list."""
398
+ # Mock Application
399
+ mock_app = MagicMock()
400
+ mock_app.config.get.return_value = 15
401
+ mock_application.return_value = mock_app
402
+
403
+ # Use a framework not in the frameworks list
404
+ invalid_framework = "INVALID_FRAMEWORK"
405
+
406
+ existing_reports = []
407
+
408
+ with self.assertRaises(ValueError) as context:
409
+ get_or_create_report_id(
410
+ self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, invalid_framework
411
+ )
412
+
413
+ # The actual error message from list.index() is different
414
+ self.assertIn("is not in list", str(context.exception))
415
+
416
+ @patch("regscale.integrations.commercial.wizv2.utils.main.Application")
417
+ @patch("regscale.integrations.commercial.wizv2.utils.main.is_report_expired")
418
+ def test_get_or_create_report_id_custom_report_age(self, mock_is_expired, mock_application):
419
+ """Test using custom wizReportAge configuration."""
420
+ # Mock Application with custom age
421
+ mock_app = MagicMock()
422
+ mock_app.config.get.return_value = 30 # 30 days instead of default 15
423
+ mock_application.return_value = mock_app
424
+
425
+ # Mock is_report_expired to return False
426
+ mock_is_expired.return_value = False
427
+
428
+ # Mock existing reports with a valid report
429
+ existing_reports = [
430
+ {
431
+ "id": "existing-report-123",
432
+ "name": "NIST_SP_800-53_Revision_5_project_test-project-123",
433
+ "lastRun": {"runAt": "2023-07-15T14:37:55.450532Z"},
434
+ }
435
+ ]
436
+
437
+ result = get_or_create_report_id(
438
+ self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, self.target_framework
439
+ )
440
+
441
+ self.assertEqual(result, "existing-report-123")
442
+ mock_app.config.get.assert_called_once_with("wizReportAge", 15)
443
+ mock_is_expired.assert_called_once_with("2023-07-15T14:37:55.450532Z", 30)
444
+
445
+ @patch("regscale.integrations.commercial.wizv2.utils.main.Application")
446
+ @patch("regscale.integrations.commercial.wizv2.utils.main.is_report_expired")
447
+ @patch("regscale.integrations.commercial.wizv2.utils.main.create_compliance_report")
448
+ def test_get_or_create_report_id_multiple_reports_first_match(
449
+ self, mock_create_report, mock_is_expired, mock_application
450
+ ):
451
+ """Test behavior when multiple reports exist, should use first matching report."""
452
+ # Mock Application
453
+ mock_app = MagicMock()
454
+ mock_app.config.get.return_value = 15
455
+ mock_application.return_value = mock_app
456
+
457
+ # Mock is_report_expired to return False for first call
458
+ mock_is_expired.return_value = False
459
+
460
+ # Mock existing reports with multiple matching reports
461
+ existing_reports = [
462
+ {
463
+ "id": "first-report-123",
464
+ "name": "NIST_SP_800-53_Revision_5_project_test-project-123",
465
+ "lastRun": {"runAt": "2023-07-15T14:37:55.450532Z"},
466
+ },
467
+ {
468
+ "id": "second-report-456",
469
+ "name": "NIST_SP_800-53_Revision_5_project_test-project-123",
470
+ "lastRun": {"runAt": "2023-07-16T14:37:55.450532Z"},
471
+ },
472
+ ]
473
+
474
+ result = get_or_create_report_id(
475
+ self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, self.target_framework
476
+ )
477
+
478
+ # Should return the first matching report
479
+ self.assertEqual(result, "first-report-123")
480
+ mock_is_expired.assert_called_once_with("2023-07-15T14:37:55.450532Z", 15)
481
+ mock_create_report.assert_not_called()
482
+
483
+ @patch("regscale.integrations.commercial.wizv2.utils.main.Application")
484
+ @patch("regscale.integrations.commercial.wizv2.utils.main.is_report_expired")
485
+ @patch("regscale.integrations.commercial.wizv2.utils.main.create_compliance_report")
486
+ def test_get_or_create_report_id_different_report_names(
487
+ self, mock_create_report, mock_is_expired, mock_application
488
+ ):
489
+ """Test creating new report when existing reports have different names."""
490
+ # Mock Application
491
+ mock_app = MagicMock()
492
+ mock_app.config.get.return_value = 15
493
+ mock_application.return_value = mock_app
494
+
495
+ # Mock create_compliance_report to return new report ID
496
+ mock_create_report.return_value = "new-report-999"
497
+
498
+ # Mock existing reports with different names
499
+ existing_reports = [
500
+ {
501
+ "id": "other-report-123",
502
+ "name": "OTHER_FRAMEWORK_project_test-project-123",
503
+ "lastRun": {"runAt": "2023-07-15T14:37:55.450532Z"},
504
+ }
505
+ ]
506
+
507
+ result = get_or_create_report_id(
508
+ self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, self.target_framework
509
+ )
510
+
511
+ self.assertEqual(result, "new-report-999")
512
+ mock_app.config.get.assert_called_once_with("wizReportAge", 15)
513
+ # is_report_expired should not be called since no matching report name
514
+ mock_is_expired.assert_not_called()
515
+ mock_create_report.assert_called_once()
516
+
517
+
518
+ if __name__ == "__main__":
519
+ unittest.main()
@@ -0,0 +1 @@
1
+ """Tests for Wiz V2 Utils"""