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,1523 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Comprehensive unit tests for Wiz V2 utility functions in utils/main.py"""
4
+
5
+ import csv
6
+ import codecs
7
+ import datetime
8
+ import json
9
+ import logging
10
+ import time
11
+ import unittest
12
+ from io import StringIO
13
+ from typing import Dict, List
14
+ from unittest.mock import MagicMock, Mock, patch, mock_open, call
15
+ from zipfile import ZipFile
16
+
17
+ import pytest
18
+ import requests
19
+ from pydantic import ValidationError
20
+
21
+ from regscale.core.app.utils.app_utils import error_and_exit
22
+ from regscale.integrations.commercial.wizv2.utils.main import (
23
+ is_report_expired,
24
+ get_notes_from_wiz_props,
25
+ handle_management_type,
26
+ create_asset_type,
27
+ map_category,
28
+ convert_first_seen_to_days,
29
+ fetch_report_by_id,
30
+ download_file,
31
+ fetch_sbom_report,
32
+ fetch_report_id,
33
+ get_framework_names,
34
+ check_reports_for_frameworks,
35
+ create_report_if_needed,
36
+ fetch_and_process_report_data,
37
+ get_or_create_report_id,
38
+ fetch_report_data,
39
+ process_single_report,
40
+ fetch_framework_report,
41
+ fetch_frameworks,
42
+ query_reports,
43
+ send_request,
44
+ create_compliance_report,
45
+ get_report_url_and_status,
46
+ download_report,
47
+ rerun_expired_report,
48
+ check_compliance,
49
+ create_assessment_from_compliance_report,
50
+ create_report_assessment,
51
+ update_implementation_status,
52
+ get_wiz_compliance_settings,
53
+ report_result_to_implementation_status,
54
+ create_vulnerabilities_from_wiz_findings,
55
+ create_single_vulnerability_from_wiz_data,
56
+ _get_category_from_cpe,
57
+ _get_category_from_hardware_types,
58
+ _get_category_from_asset_type,
59
+ _handle_report_response,
60
+ _handle_rate_limit_error,
61
+ _add_controls_to_controls_to_report_dict,
62
+ _clean_passing_list,
63
+ _create_aggregated_assessment_report,
64
+ _try_get_status_from_settings,
65
+ _match_label_to_result,
66
+ _get_default_status_mapping,
67
+ )
68
+ from regscale.models import regscale_models
69
+ from regscale.models.integration_models.wizv2 import ComplianceReport, ComplianceCheckStatus
70
+
71
+ logger = logging.getLogger("regscale")
72
+ PATH = "regscale.integrations.commercial.wizv2.utils.main"
73
+
74
+
75
+ # ==================== Date and Reporting Tests ====================
76
+ class TestDateAndReporting(unittest.TestCase):
77
+ """Test date handling and report validation functions"""
78
+
79
+ def test_is_report_expired_not_expired(self):
80
+ """Test report that is not expired"""
81
+ # Report from 5 days ago
82
+ five_days_ago = (datetime.datetime.now() - datetime.timedelta(days=5)).isoformat()
83
+ result = is_report_expired(five_days_ago, max_age_days=10)
84
+ self.assertFalse(result)
85
+
86
+ def test_is_report_expired_expired(self):
87
+ """Test report that is expired"""
88
+ # Report from 20 days ago
89
+ twenty_days_ago = (datetime.datetime.now() - datetime.timedelta(days=20)).isoformat()
90
+ result = is_report_expired(twenty_days_ago, max_age_days=15)
91
+ self.assertTrue(result)
92
+
93
+ def test_is_report_expired_exact_boundary(self):
94
+ """Test report at exact boundary (should be expired)"""
95
+ # Report from exactly 15 days ago
96
+ fifteen_days_ago = (datetime.datetime.now() - datetime.timedelta(days=15)).isoformat()
97
+ result = is_report_expired(fifteen_days_ago, max_age_days=15)
98
+ self.assertTrue(result)
99
+
100
+ def test_is_report_expired_invalid_date(self):
101
+ """Test with invalid date format"""
102
+ result = is_report_expired("invalid-date", max_age_days=15)
103
+ self.assertTrue(result)
104
+
105
+ def test_is_report_expired_none_date(self):
106
+ """Test with None date"""
107
+ result = is_report_expired(None, max_age_days=15)
108
+ self.assertTrue(result)
109
+
110
+ def test_is_report_expired_empty_string(self):
111
+ """Test with empty string"""
112
+ result = is_report_expired("", max_age_days=15)
113
+ self.assertTrue(result)
114
+
115
+ def test_convert_first_seen_to_days_valid_date(self):
116
+ """Test converting a valid first seen date to days"""
117
+ # Date from 10 days ago
118
+ ten_days_ago = (datetime.datetime.now() - datetime.timedelta(days=10)).isoformat()
119
+ result = convert_first_seen_to_days(ten_days_ago)
120
+ self.assertEqual(result, 10)
121
+
122
+ def test_convert_first_seen_to_days_today(self):
123
+ """Test converting today's date to days"""
124
+ today = datetime.datetime.now().isoformat()
125
+ result = convert_first_seen_to_days(today)
126
+ self.assertEqual(result, 0)
127
+
128
+ def test_convert_first_seen_to_days_invalid_date(self):
129
+ """Test with invalid date format"""
130
+ result = convert_first_seen_to_days("invalid-date")
131
+ self.assertEqual(result, 0)
132
+
133
+ def test_convert_first_seen_to_days_none(self):
134
+ """Test with None"""
135
+ result = convert_first_seen_to_days(None)
136
+ self.assertEqual(result, 0)
137
+
138
+
139
+ # ==================== Property and Entity Tests ====================
140
+ class TestPropertiesAndEntities(unittest.TestCase):
141
+ """Test property extraction and entity handling functions"""
142
+
143
+ def test_get_notes_from_wiz_props_all_properties(self):
144
+ """Test getting notes with all properties present"""
145
+ wiz_properties = {
146
+ "cloudPlatform": "AWS",
147
+ "providerUniqueId": "i-1234567890",
148
+ "cloudProviderURL": "https://console.aws.amazon.com/ec2/v2/home",
149
+ "_vertexID": "vertex-123",
150
+ "severity_name": "High",
151
+ "severity_description": "Critical vulnerability detected",
152
+ }
153
+ external_id = "ext-123"
154
+
155
+ result = get_notes_from_wiz_props(wiz_properties, external_id)
156
+
157
+ self.assertIn("External ID: ext-123", result)
158
+ self.assertIn("Cloud Platform: AWS", result)
159
+ self.assertIn("Provider Unique ID: i-1234567890", result)
160
+ self.assertIn("cloudProviderURL:", result)
161
+ self.assertIn('target="_blank"', result)
162
+ self.assertIn("Vertex ID: vertex-123", result)
163
+ self.assertIn("Severity Name: High", result)
164
+ self.assertIn("Severity Description: Critical vulnerability detected", result)
165
+ self.assertIn("<br>", result)
166
+
167
+ def test_get_notes_from_wiz_props_minimal_properties(self):
168
+ """Test getting notes with minimal properties"""
169
+ wiz_properties = {}
170
+ external_id = "ext-456"
171
+
172
+ result = get_notes_from_wiz_props(wiz_properties, external_id)
173
+
174
+ self.assertIn("External ID: ext-456", result)
175
+ self.assertNotIn("Cloud Platform:", result)
176
+ self.assertNotIn("Provider Unique ID:", result)
177
+
178
+ def test_get_notes_from_wiz_props_with_url(self):
179
+ """Test URL formatting in notes"""
180
+ wiz_properties = {"cloudProviderURL": "https://example.com/resource"}
181
+ external_id = "ext-789"
182
+
183
+ result = get_notes_from_wiz_props(wiz_properties, external_id)
184
+
185
+ self.assertIn('<a href="https://example.com/resource" target="_blank">', result)
186
+
187
+ def test_handle_management_type_managed(self):
188
+ """Test management type for managed resources"""
189
+ wiz_properties = {"isManaged": True}
190
+ result = handle_management_type(wiz_properties)
191
+ self.assertEqual(result, "External/Third Party Managed")
192
+
193
+ def test_handle_management_type_internally_managed(self):
194
+ """Test management type for internally managed resources"""
195
+ wiz_properties = {"isManaged": False}
196
+ result = handle_management_type(wiz_properties)
197
+ self.assertEqual(result, "Internally Managed")
198
+
199
+ def test_handle_management_type_missing_key(self):
200
+ """Test management type when isManaged key is missing"""
201
+ wiz_properties = {}
202
+ result = handle_management_type(wiz_properties)
203
+ self.assertEqual(result, "Internally Managed")
204
+
205
+
206
+ # ==================== Asset Category Mapping Tests ====================
207
+ class TestAssetCategoryMapping(unittest.TestCase):
208
+ """Test asset category mapping functions"""
209
+
210
+ @patch(f"{PATH}._get_category_from_cpe")
211
+ def test_map_category_from_cpe(self, mock_cpe):
212
+ """Test mapping category from CPE"""
213
+ mock_cpe.return_value = regscale_models.AssetCategory.Hardware
214
+ node = {"type": "VM", "graphEntity": {}}
215
+
216
+ result = map_category(node)
217
+
218
+ self.assertEqual(result, regscale_models.AssetCategory.Hardware)
219
+ mock_cpe.assert_called_once()
220
+
221
+ @patch(f"{PATH}._get_category_from_cpe")
222
+ @patch(f"{PATH}._get_category_from_hardware_types")
223
+ def test_map_category_from_hardware_types(self, mock_hardware, mock_cpe):
224
+ """Test mapping category from hardware types"""
225
+ mock_cpe.return_value = None
226
+ mock_hardware.return_value = regscale_models.AssetCategory.Hardware
227
+
228
+ node = {"type": "VM", "graphEntity": {}}
229
+
230
+ result = map_category(node)
231
+
232
+ self.assertEqual(result, regscale_models.AssetCategory.Hardware)
233
+ mock_hardware.assert_called_once()
234
+
235
+ @patch(f"{PATH}._get_category_from_cpe")
236
+ @patch(f"{PATH}._get_category_from_hardware_types")
237
+ @patch(f"{PATH}._get_category_from_asset_type")
238
+ def test_map_category_from_asset_type(self, mock_asset_type, mock_hardware, mock_cpe):
239
+ """Test mapping category from asset type"""
240
+ mock_cpe.return_value = None
241
+ mock_hardware.return_value = None
242
+ mock_asset_type.return_value = regscale_models.AssetCategory.Software
243
+
244
+ node = {"type": "Application", "graphEntity": {}}
245
+
246
+ result = map_category(node)
247
+
248
+ self.assertEqual(result, regscale_models.AssetCategory.Software)
249
+ mock_asset_type.assert_called_once()
250
+
251
+ @patch(f"{PATH}._get_category_from_cpe")
252
+ @patch(f"{PATH}._get_category_from_hardware_types")
253
+ @patch(f"{PATH}._get_category_from_asset_type")
254
+ def test_map_category_default_to_software(self, mock_asset_type, mock_hardware, mock_cpe):
255
+ """Test default category is Software"""
256
+ mock_cpe.return_value = None
257
+ mock_hardware.return_value = None
258
+ mock_asset_type.return_value = None
259
+
260
+ node = {"type": "Unknown", "graphEntity": {}}
261
+
262
+ result = map_category(node)
263
+
264
+ self.assertEqual(result, regscale_models.AssetCategory.Software)
265
+
266
+ @patch(f"{PATH}.extract_product_name_and_version")
267
+ def test_get_category_from_cpe_valid(self, mock_extract):
268
+ """Test getting category from valid CPE"""
269
+ mock_extract.return_value = {"part": "h"}
270
+ node = {"graphEntity": {"properties": {"cpe": "cpe:2.3:h:vendor:product:*"}}}
271
+
272
+ result = _get_category_from_cpe(node)
273
+
274
+ self.assertEqual(result, regscale_models.AssetCategory.Hardware)
275
+
276
+ @patch(f"{PATH}.extract_product_name_and_version")
277
+ def test_get_category_from_cpe_no_cpe(self, mock_extract):
278
+ """Test getting category when no CPE present"""
279
+ node = {"graphEntity": {"properties": {}}}
280
+
281
+ result = _get_category_from_cpe(node)
282
+
283
+ self.assertIsNone(result)
284
+
285
+ @patch(f"{PATH}.WizVariables")
286
+ def test_get_category_from_hardware_types_matching_type(self, mock_vars):
287
+ """Test getting hardware category from matching type"""
288
+ mock_vars.useWizHardwareAssetTypes = True
289
+ mock_vars.wizHardwareAssetTypes = ["VIRTUAL_MACHINE", "CONTAINER"]
290
+
291
+ node = {"type": "VIRTUAL_MACHINE", "graphEntity": {}}
292
+
293
+ result = _get_category_from_hardware_types(node, "VIRTUAL_MACHINE")
294
+
295
+ self.assertEqual(result, regscale_models.AssetCategory.Hardware)
296
+
297
+ @patch(f"{PATH}.WizVariables")
298
+ def test_get_category_from_hardware_types_feature_disabled(self, mock_vars):
299
+ """Test when useWizHardwareAssetTypes is disabled"""
300
+ mock_vars.useWizHardwareAssetTypes = False
301
+
302
+ node = {"type": "VIRTUAL_MACHINE", "graphEntity": {}}
303
+
304
+ result = _get_category_from_hardware_types(node, "VIRTUAL_MACHINE")
305
+
306
+ self.assertIsNone(result)
307
+
308
+ def test_get_category_from_asset_type_valid_attribute(self):
309
+ """Test getting category from valid asset type attribute"""
310
+ node = {"type": "Software"}
311
+
312
+ result = _get_category_from_asset_type("Software", node)
313
+
314
+ self.assertEqual(result, regscale_models.AssetCategory.Software)
315
+
316
+ def test_get_category_from_asset_type_invalid_attribute(self):
317
+ """Test getting category from invalid asset type"""
318
+ node = {"type": "Unknown"}
319
+
320
+ result = _get_category_from_asset_type("UnknownType", node)
321
+
322
+ self.assertIsNone(result)
323
+
324
+
325
+ # ==================== Asset Type Creation Tests ====================
326
+ class TestAssetTypeCreation(unittest.TestCase):
327
+ """Test asset type creation and formatting"""
328
+
329
+ @patch(f"{PATH}.regscale_models.Metadata.get_metadata_by_module_field")
330
+ @patch(f"{PATH}.regscale_models.Metadata")
331
+ def test_create_asset_type_new_type(self, mock_metadata_class, mock_get_metadata):
332
+ """Test creating a new asset type"""
333
+ mock_get_metadata.return_value = []
334
+ mock_metadata_instance = MagicMock()
335
+ mock_metadata_class.return_value = mock_metadata_instance
336
+
337
+ result = create_asset_type("VIRTUAL_MACHINE")
338
+
339
+ self.assertEqual(result, "Virtual Machine")
340
+ mock_metadata_instance.create.assert_called_once()
341
+
342
+ @patch(f"{PATH}.regscale_models.Metadata.get_metadata_by_module_field")
343
+ def test_create_asset_type_existing_type(self, mock_get_metadata):
344
+ """Test with existing asset type"""
345
+ mock_metadata = MagicMock()
346
+ mock_metadata.value = "Virtual Machine"
347
+ mock_get_metadata.return_value = [mock_metadata]
348
+
349
+ result = create_asset_type("virtual_machine")
350
+
351
+ self.assertEqual(result, "Virtual Machine")
352
+
353
+ @patch(f"{PATH}.regscale_models.Metadata.get_metadata_by_module_field")
354
+ @patch(f"{PATH}.regscale_models.Metadata")
355
+ def test_create_asset_type_formatting(self, mock_metadata_class, mock_get_metadata):
356
+ """Test asset type string formatting"""
357
+ mock_get_metadata.return_value = []
358
+ mock_metadata_instance = MagicMock()
359
+ mock_metadata_class.return_value = mock_metadata_instance
360
+
361
+ result = create_asset_type("test_asset_type")
362
+
363
+ self.assertEqual(result, "Test Asset Type")
364
+
365
+
366
+ # ==================== Framework and Report Tests ====================
367
+ class TestFrameworkAndReports(unittest.TestCase):
368
+ """Test framework and report handling functions"""
369
+
370
+ def test_get_framework_names_single(self):
371
+ """Test getting framework names with single framework"""
372
+ wiz_frameworks = [{"name": "NIST SP 800-53 Revision 5"}]
373
+
374
+ result = get_framework_names(wiz_frameworks)
375
+
376
+ self.assertEqual(result, ["NIST_SP_800-53_Revision_5"])
377
+
378
+ def test_get_framework_names_multiple(self):
379
+ """Test getting framework names with multiple frameworks"""
380
+ wiz_frameworks = [{"name": "NIST SP 800-53 Revision 5"}, {"name": "NIST CSF v1.1"}, {"name": "ISO 27001"}]
381
+
382
+ result = get_framework_names(wiz_frameworks)
383
+
384
+ self.assertEqual(result, ["NIST_SP_800-53_Revision_5", "NIST_CSF_v1.1", "ISO_27001"])
385
+
386
+ def test_get_framework_names_empty(self):
387
+ """Test getting framework names with empty list"""
388
+ result = get_framework_names([])
389
+ self.assertEqual(result, [])
390
+
391
+ def test_check_reports_for_frameworks_found(self):
392
+ """Test checking reports when framework is found"""
393
+ reports = [
394
+ {"name": "NIST_SP_800-53_Revision_5_project_123"},
395
+ {"name": "ISO_27001_project_456"},
396
+ ]
397
+ frames = ["NIST_SP_800-53_Revision_5"]
398
+
399
+ result = check_reports_for_frameworks(reports, frames)
400
+
401
+ self.assertTrue(result)
402
+
403
+ def test_check_reports_for_frameworks_not_found(self):
404
+ """Test checking reports when framework is not found"""
405
+ reports = [
406
+ {"name": "ISO_27001_project_456"},
407
+ ]
408
+ frames = ["NIST_SP_800-53_Revision_5"]
409
+
410
+ result = check_reports_for_frameworks(reports, frames)
411
+
412
+ self.assertFalse(result)
413
+
414
+ def test_check_reports_for_frameworks_empty_reports(self):
415
+ """Test checking empty reports"""
416
+ result = check_reports_for_frameworks([], ["NIST_SP_800-53_Revision_5"])
417
+ self.assertFalse(result)
418
+
419
+ @patch(f"{PATH}.create_compliance_report")
420
+ def test_create_report_if_needed_creates_new(self, mock_create_report):
421
+ """Test creating a new report when needed"""
422
+ mock_create_report.return_value = "new-report-123"
423
+
424
+ wiz_project_id = "project-456"
425
+ frames = ["NIST_SP_800-53_Revision_5"]
426
+ wiz_frameworks = [{"id": "framework-1", "name": "NIST SP 800-53 Revision 5"}]
427
+ reports = []
428
+ snake_framework = "NIST_SP_800-53_Revision_5"
429
+
430
+ result = create_report_if_needed(wiz_project_id, frames, wiz_frameworks, reports, snake_framework)
431
+
432
+ self.assertEqual(result, ["new-report-123"])
433
+ mock_create_report.assert_called_once()
434
+
435
+ def test_create_report_if_needed_existing_reports(self):
436
+ """Test when reports already exist"""
437
+ wiz_project_id = "project-456"
438
+ frames = ["NIST_SP_800-53_Revision_5"]
439
+ wiz_frameworks = [{"id": "framework-1", "name": "NIST SP 800-53 Revision 5"}]
440
+ reports = [
441
+ {"id": "existing-report-1", "name": "NIST_SP_800-53_Revision_5_project_123"},
442
+ {"id": "existing-report-2", "name": "other_framework_project_123"},
443
+ ]
444
+ snake_framework = "NIST_SP_800-53_Revision_5"
445
+
446
+ result = create_report_if_needed(wiz_project_id, frames, wiz_frameworks, reports, snake_framework)
447
+
448
+ self.assertEqual(result, ["existing-report-1"])
449
+
450
+
451
+ # ==================== Report Download and Processing Tests ====================
452
+ class TestReportDownloadAndProcessing(unittest.TestCase):
453
+ """Test report download and processing functions"""
454
+
455
+ @patch(f"{PATH}.get_report_url_and_status")
456
+ @patch(f"{PATH}.requests.get")
457
+ @patch(f"{PATH}.csv.DictReader")
458
+ def test_fetch_and_process_report_data(self, mock_dict_reader, mock_requests_get, mock_get_url):
459
+ """Test fetching and processing report data"""
460
+ mock_get_url.return_value = "https://example.com/report.csv"
461
+
462
+ # Mock response
463
+ mock_response = MagicMock()
464
+ mock_response.iter_lines.return_value = [
465
+ b"col1,col2",
466
+ b"val1,val2",
467
+ b"val3,val4",
468
+ ]
469
+ mock_requests_get.return_value.__enter__.return_value = mock_response
470
+
471
+ # Mock CSV reader
472
+ mock_dict_reader.return_value = [
473
+ {"col1": "val1", "col2": "val2"},
474
+ {"col1": "val3", "col2": "val4"},
475
+ ]
476
+
477
+ wiz_report_ids = ["report-1"]
478
+ result = fetch_and_process_report_data(wiz_report_ids)
479
+
480
+ self.assertEqual(len(result), 2)
481
+ mock_get_url.assert_called_once_with("report-1")
482
+
483
+ @patch(f"{PATH}.check_file_path")
484
+ @patch(f"{PATH}.requests.get")
485
+ @patch("builtins.open", new_callable=mock_open)
486
+ def test_download_file_success(self, mock_file, mock_requests_get, mock_check_path):
487
+ """Test successful file download"""
488
+ mock_response = MagicMock()
489
+ mock_response.raise_for_status.return_value = None
490
+ mock_response.iter_content.return_value = [b"chunk1", b"chunk2"]
491
+ mock_requests_get.return_value.__enter__.return_value = mock_response
492
+
493
+ download_file("https://example.com/file.csv", "artifacts/test.csv")
494
+
495
+ mock_check_path.assert_called_once_with("artifacts")
496
+ mock_requests_get.assert_called_once()
497
+ mock_file.assert_called_once()
498
+
499
+ @patch(f"{PATH}.check_file_path")
500
+ @patch(f"{PATH}.requests.get")
501
+ def test_download_file_http_error(self, mock_requests_get, mock_check_path):
502
+ """Test file download with HTTP error"""
503
+ mock_response = MagicMock()
504
+ mock_response.raise_for_status.side_effect = requests.HTTPError("404 Not Found")
505
+ mock_requests_get.return_value.__enter__.return_value = mock_response
506
+
507
+ with self.assertRaises(requests.HTTPError):
508
+ download_file("https://example.com/file.csv", "artifacts/test.csv")
509
+
510
+ @patch(f"{PATH}.WizVariables")
511
+ @patch(f"{PATH}.PaginatedGraphQLClient")
512
+ @patch(f"{PATH}.error_and_exit")
513
+ def test_fetch_report_by_id_no_token(self, mock_error_exit, mock_client, mock_vars):
514
+ """Test fetch_report_by_id with missing token"""
515
+ mock_vars.wizAccessToken = None
516
+ mock_error_exit.side_effect = SystemExit(1)
517
+
518
+ with pytest.raises(SystemExit):
519
+ fetch_report_by_id("report-123", 456)
520
+
521
+ mock_error_exit.assert_called_once()
522
+
523
+ @patch(f"{PATH}.WizVariables")
524
+ @patch(f"{PATH}.PaginatedGraphQLClient")
525
+ @patch(f"{PATH}.download_file")
526
+ @patch(f"{PATH}.Api")
527
+ @patch(f"{PATH}.File")
528
+ def test_fetch_report_by_id_success(self, mock_file, mock_api, mock_download, mock_client, mock_vars):
529
+ """Test successful report fetch"""
530
+ mock_vars.wizAccessToken = "test-token"
531
+ mock_vars.wizUrl = "https://api.wiz.io/graphql"
532
+
533
+ mock_client_instance = MagicMock()
534
+ mock_client.return_value = mock_client_instance
535
+ mock_client_instance.fetch_results.return_value = {
536
+ "report": {"lastRun": {"url": "https://example.com/report.csv"}}
537
+ }
538
+
539
+ mock_api_instance = MagicMock()
540
+ mock_api.return_value = mock_api_instance
541
+
542
+ fetch_report_by_id("report-123", 456)
543
+
544
+ mock_download.assert_called_once()
545
+ mock_file.upload_file_to_regscale.assert_called_once()
546
+
547
+ @patch(f"{PATH}.WizVariables")
548
+ @patch(f"{PATH}.PaginatedGraphQLClient")
549
+ def test_fetch_report_by_id_with_errors(self, mock_client, mock_vars):
550
+ """Test fetch_report_by_id with API errors"""
551
+ mock_vars.wizAccessToken = "test-token"
552
+ mock_vars.wizUrl = "https://api.wiz.io/graphql"
553
+
554
+ mock_client_instance = MagicMock()
555
+ mock_client.return_value = mock_client_instance
556
+ mock_client_instance.fetch_results.return_value = {"errors": [{"message": "API Error"}]}
557
+
558
+ # Should not raise, just log error
559
+ fetch_report_by_id("report-123", 456)
560
+
561
+
562
+ # ==================== SBOM Report Tests ====================
563
+ class TestSBOMReports(unittest.TestCase):
564
+ """Test SBOM report fetching and processing"""
565
+
566
+ @patch(f"{PATH}.WizVariables")
567
+ @patch(f"{PATH}.error_and_exit")
568
+ def test_fetch_sbom_report_no_token(self, mock_error_exit, mock_vars):
569
+ """Test fetch_sbom_report with missing token"""
570
+ mock_vars.wizAccessToken = None
571
+ mock_error_exit.side_effect = SystemExit(1)
572
+
573
+ with pytest.raises(SystemExit):
574
+ fetch_sbom_report("report-123", "456")
575
+
576
+ mock_error_exit.assert_called_once()
577
+
578
+ @patch(f"{PATH}.WizVariables")
579
+ @patch(f"{PATH}.PaginatedGraphQLClient")
580
+ @patch(f"{PATH}.download_file")
581
+ @patch(f"{PATH}.ZipFile")
582
+ @patch(f"{PATH}.Sbom")
583
+ def test_fetch_sbom_report_success(self, mock_sbom, mock_zipfile, mock_download, mock_client, mock_vars):
584
+ """Test successful SBOM report fetch"""
585
+ mock_vars.wizAccessToken = "test-token"
586
+ mock_vars.wizUrl = "https://api.wiz.io/graphql"
587
+
588
+ # Mock client response
589
+ mock_client_instance = MagicMock()
590
+ mock_client.return_value = mock_client_instance
591
+ mock_client_instance.fetch_results.return_value = {
592
+ "report": {"lastRun": {"url": "https://example.com/sbom.zip"}}
593
+ }
594
+
595
+ # Mock zip file
596
+ mock_zip_instance = MagicMock()
597
+ mock_zipfile.return_value.__enter__.return_value = mock_zip_instance
598
+ mock_zip_instance.namelist.return_value = ["sbom.json"]
599
+
600
+ # Mock JSON file inside zip
601
+ mock_json_file = MagicMock()
602
+ mock_json_file.__enter__.return_value = mock_json_file
603
+ sbom_data = {"bomFormat": "CycloneDX", "specVersion": "1.5", "components": []}
604
+ mock_json_file.read.return_value = json.dumps(sbom_data).encode()
605
+ mock_zip_instance.open.return_value = mock_json_file
606
+
607
+ # Mock Sbom model
608
+ mock_sbom_instance = MagicMock()
609
+ mock_sbom.return_value = mock_sbom_instance
610
+
611
+ fetch_sbom_report("report-123", "456")
612
+
613
+ mock_download.assert_called_once()
614
+ mock_sbom.assert_called_once()
615
+ mock_sbom_instance.create_or_update.assert_called_once()
616
+
617
+
618
+ # ==================== GraphQL Request Tests ====================
619
+ class TestGraphQLRequests(unittest.TestCase):
620
+ """Test GraphQL request functions"""
621
+
622
+ @patch(f"{PATH}.WizVariables")
623
+ @patch(f"{PATH}.Api")
624
+ def test_send_request_success(self, mock_api, mock_vars):
625
+ """Test successful send_request"""
626
+ mock_vars.wizUrl = "https://api.wiz.io/graphql"
627
+ mock_vars.wizAccessToken = "test-token"
628
+
629
+ mock_api_instance = MagicMock()
630
+ mock_api.return_value = mock_api_instance
631
+
632
+ mock_response = MagicMock()
633
+ mock_api_instance.post.return_value = mock_response
634
+
635
+ query = "query { test }"
636
+ variables = {"var1": "value1"}
637
+
638
+ result = send_request(query, variables)
639
+
640
+ self.assertEqual(result, mock_response)
641
+ mock_api_instance.post.assert_called_once()
642
+
643
+ @patch(f"{PATH}.WizVariables")
644
+ @patch(f"{PATH}.Api")
645
+ def test_send_request_no_token(self, mock_api, mock_vars):
646
+ """Test send_request with missing token"""
647
+ mock_vars.wizAccessToken = None
648
+
649
+ query = "query { test }"
650
+ variables = {"var1": "value1"}
651
+
652
+ with self.assertRaises(ValueError) as context:
653
+ send_request(query, variables)
654
+
655
+ self.assertIn("access token is missing", str(context.exception))
656
+
657
+ @patch(f"{PATH}.send_request")
658
+ def test_fetch_frameworks_success(self, mock_send_request):
659
+ """Test successful fetch_frameworks"""
660
+ mock_response = MagicMock()
661
+ mock_response.ok = True
662
+ mock_response.json.return_value = {
663
+ "data": {
664
+ "securityFrameworks": {
665
+ "nodes": [
666
+ {"id": "framework-1", "name": "NIST SP 800-53 Revision 5"},
667
+ {"id": "framework-2", "name": "ISO 27001"},
668
+ ]
669
+ }
670
+ }
671
+ }
672
+ mock_send_request.return_value = mock_response
673
+
674
+ result = fetch_frameworks()
675
+
676
+ self.assertEqual(len(result), 2)
677
+ self.assertEqual(result[0]["name"], "NIST SP 800-53 Revision 5")
678
+
679
+ @patch(f"{PATH}.send_request")
680
+ @patch(f"{PATH}.error_and_exit")
681
+ def test_fetch_frameworks_error(self, mock_error_exit, mock_send_request):
682
+ """Test fetch_frameworks with API error"""
683
+ mock_response = MagicMock()
684
+ mock_response.ok = False
685
+ mock_response.status_code = 500
686
+ mock_response.text = "Internal Server Error"
687
+ mock_send_request.return_value = mock_response
688
+ mock_error_exit.side_effect = SystemExit(1)
689
+
690
+ with pytest.raises(SystemExit):
691
+ fetch_frameworks()
692
+
693
+ mock_error_exit.assert_called_once()
694
+
695
+ @patch(f"{PATH}.send_request")
696
+ def test_query_reports_success(self, mock_send_request):
697
+ """Test successful query_reports"""
698
+ mock_response = MagicMock()
699
+ mock_response.json.return_value = {
700
+ "data": {
701
+ "reports": {
702
+ "nodes": [
703
+ {"id": "report-1", "name": "Test Report 1"},
704
+ {"id": "report-2", "name": "Test Report 2"},
705
+ ]
706
+ }
707
+ }
708
+ }
709
+ mock_send_request.return_value = mock_response
710
+
711
+ result = query_reports("project-123")
712
+
713
+ self.assertEqual(len(result), 2)
714
+ self.assertEqual(result[0]["id"], "report-1")
715
+
716
+ @patch(f"{PATH}.send_request")
717
+ @patch(f"{PATH}.error_and_exit")
718
+ def test_query_reports_with_errors(self, mock_error_exit, mock_send_request):
719
+ """Test query_reports with API errors"""
720
+ mock_response = MagicMock()
721
+ mock_response.json.return_value = {"errors": [{"message": "API Error"}]}
722
+ mock_send_request.return_value = mock_response
723
+ mock_error_exit.side_effect = SystemExit(1)
724
+
725
+ with pytest.raises(SystemExit):
726
+ query_reports("project-123")
727
+
728
+ mock_error_exit.assert_called_once()
729
+
730
+ @patch(f"{PATH}.send_request")
731
+ @patch(f"{PATH}.error_and_exit")
732
+ def test_query_reports_json_decode_error(self, mock_error_exit, mock_send_request):
733
+ """Test query_reports with JSON decode error"""
734
+ mock_response = MagicMock()
735
+ mock_response.json.side_effect = requests.JSONDecodeError("msg", "doc", 0)
736
+ mock_response.status_code = 500
737
+ mock_response.reason = "Internal Server Error"
738
+ mock_send_request.return_value = mock_response
739
+ mock_error_exit.side_effect = SystemExit(1)
740
+
741
+ with pytest.raises(SystemExit):
742
+ query_reports("project-123")
743
+
744
+ mock_error_exit.assert_called_once()
745
+
746
+
747
+ # ==================== Report Creation and Download Tests ====================
748
+ class TestReportCreationAndDownload(unittest.TestCase):
749
+ """Test report creation and download functions"""
750
+
751
+ @patch(f"{PATH}.fetch_report_id")
752
+ def test_create_compliance_report(self, mock_fetch_report_id):
753
+ """Test creating compliance report"""
754
+ mock_fetch_report_id.return_value = "new-report-789"
755
+
756
+ result = create_compliance_report(
757
+ report_name="Test_Report", wiz_project_id="project-123", framework_id="framework-456"
758
+ )
759
+
760
+ self.assertEqual(result, "new-report-789")
761
+ mock_fetch_report_id.assert_called_once()
762
+
763
+ @patch(f"{PATH}.send_request")
764
+ def test_download_report(self, mock_send_request):
765
+ """Test download_report"""
766
+ mock_response = MagicMock()
767
+ mock_send_request.return_value = mock_response
768
+
769
+ variables = {"reportId": "report-123"}
770
+ result = download_report(variables)
771
+
772
+ self.assertEqual(result, mock_response)
773
+ mock_send_request.assert_called_once()
774
+
775
+ @patch(f"{PATH}.send_request")
776
+ def test_rerun_expired_report(self, mock_send_request):
777
+ """Test rerun_expired_report"""
778
+ mock_response = MagicMock()
779
+ mock_send_request.return_value = mock_response
780
+
781
+ variables = {"reportId": "report-123"}
782
+ result = rerun_expired_report(variables)
783
+
784
+ self.assertEqual(result, mock_response)
785
+ mock_send_request.assert_called_once()
786
+
787
+ @patch(f"{PATH}.time.sleep")
788
+ @patch(f"{PATH}.download_report")
789
+ def test_get_report_url_and_status_completed(self, mock_download_report, mock_sleep):
790
+ """Test get_report_url_and_status with completed status"""
791
+ mock_response = MagicMock()
792
+ mock_response.ok = True
793
+ mock_response.json.return_value = {
794
+ "data": {"report": {"lastRun": {"status": "COMPLETED", "url": "https://example.com/report.csv"}}}
795
+ }
796
+ mock_download_report.return_value = mock_response
797
+
798
+ result = get_report_url_and_status("report-123")
799
+
800
+ self.assertEqual(result, "https://example.com/report.csv")
801
+ mock_download_report.assert_called_once()
802
+
803
+ @patch(f"{PATH}.time.sleep")
804
+ @patch(f"{PATH}.download_report")
805
+ def test_get_report_url_and_status_failed_response(self, mock_download_report, mock_sleep):
806
+ """Test get_report_url_and_status with failed response"""
807
+ mock_response = MagicMock()
808
+ mock_response.ok = False
809
+ mock_download_report.return_value = mock_response
810
+
811
+ with self.assertRaises(requests.RequestException) as context:
812
+ get_report_url_and_status("report-123")
813
+
814
+ self.assertIn("Failed to download report", str(context.exception))
815
+
816
+ def test_handle_rate_limit_error_with_rate_limit(self):
817
+ """Test handling rate limit error"""
818
+ errors = [{"message": "Rate limit exceeded", "extensions": {"retryAfter": 0.001}}]
819
+
820
+ with patch(f"{PATH}.time.sleep") as mock_sleep:
821
+ result = _handle_rate_limit_error(errors)
822
+
823
+ self.assertTrue(result)
824
+ mock_sleep.assert_called_once_with(0.001)
825
+
826
+ def test_handle_rate_limit_error_without_rate_limit(self):
827
+ """Test handling non-rate-limit error"""
828
+ errors = [{"message": "Some other error"}]
829
+
830
+ result = _handle_rate_limit_error(errors)
831
+
832
+ self.assertFalse(result)
833
+
834
+ def test_handle_report_response_completed(self):
835
+ """Test handling completed report response"""
836
+ response_json = {"data": {"report": {"lastRun": {"status": "COMPLETED", "url": "https://example.com/report"}}}}
837
+
838
+ result = _handle_report_response(response_json, "report-123")
839
+
840
+ self.assertEqual(result, "https://example.com/report")
841
+
842
+ @patch(f"{PATH}.rerun_expired_report")
843
+ @patch(f"{PATH}.get_report_url_and_status")
844
+ def test_handle_report_response_expired(self, mock_get_url, mock_rerun):
845
+ """Test handling expired report response"""
846
+ response_json = {"data": {"report": {"lastRun": {"status": "EXPIRED"}}}}
847
+ mock_get_url.return_value = "https://example.com/new-report"
848
+
849
+ result = _handle_report_response(response_json, "report-123")
850
+
851
+ self.assertEqual(result, "https://example.com/new-report")
852
+ mock_rerun.assert_called_once()
853
+
854
+ def test_handle_report_response_with_errors(self):
855
+ """Test handling response with errors"""
856
+ response_json = {"errors": [{"message": "API Error"}]}
857
+
858
+ result = _handle_report_response(response_json, "report-123")
859
+
860
+ self.assertIsNone(result)
861
+
862
+
863
+ # ==================== Compliance Check Tests ====================
864
+ class TestComplianceChecks(unittest.TestCase):
865
+ """Test compliance checking and assessment creation"""
866
+
867
+ def test_check_compliance_passing_control(self):
868
+ """Test checking compliance for passing control"""
869
+ cr_dict = {
870
+ "Framework": "NIST SP 800-53 Revision 5",
871
+ "Compliance Check Name (Wiz Subcategory)": "ac-1 access control policy",
872
+ "Result": "Pass",
873
+ "Resource Name": "test-resource",
874
+ "Resource ID": "res-123",
875
+ "Cloud Provider": "AWS",
876
+ "Cloud Provider ID": "cloud-123",
877
+ "Object Type": "VM",
878
+ "Native Type": "Virtual Machine",
879
+ "Subscription": "sub-123",
880
+ "Policy ID": "policy-123",
881
+ "Policy Short Name": "AC-1",
882
+ "Severity": "Medium",
883
+ "Assessed At": "2023-07-15T14:37:55.450532Z",
884
+ }
885
+ cr = ComplianceReport(**cr_dict)
886
+
887
+ controls = [{"controlId": "AC-1", "id": 1}]
888
+ passing = {}
889
+ failing = {}
890
+ controls_to_reports = {}
891
+
892
+ check_compliance(cr, controls, passing, failing, controls_to_reports)
893
+
894
+ self.assertIn("ac-1", passing)
895
+ self.assertIn("ac-1", controls_to_reports)
896
+ self.assertEqual(len(controls_to_reports["ac-1"]), 1)
897
+
898
+ def test_check_compliance_failing_control(self):
899
+ """Test checking compliance for failing control"""
900
+ cr_dict = {
901
+ "Framework": "NIST SP 800-53 Revision 5",
902
+ "Compliance Check Name (Wiz Subcategory)": "ac-2 account management",
903
+ "Result": "Fail",
904
+ "Resource Name": "test-resource",
905
+ "Resource ID": "res-456",
906
+ "Cloud Provider": "Azure",
907
+ "Cloud Provider ID": "cloud-456",
908
+ "Object Type": "VM",
909
+ "Native Type": "Virtual Machine",
910
+ "Subscription": "sub-456",
911
+ "Policy ID": "policy-456",
912
+ "Policy Short Name": "AC-2",
913
+ "Severity": "High",
914
+ "Assessed At": "2023-07-15T14:37:55.450532Z",
915
+ }
916
+ cr = ComplianceReport(**cr_dict)
917
+
918
+ controls = [{"controlId": "AC-2", "id": 2}]
919
+ passing = {}
920
+ failing = {}
921
+ controls_to_reports = {}
922
+
923
+ check_compliance(cr, controls, passing, failing, controls_to_reports)
924
+
925
+ self.assertIn("ac-2", failing)
926
+ self.assertNotIn("ac-2", passing)
927
+
928
+ def test_add_controls_to_controls_to_report_dict_new(self):
929
+ """Test adding control to report dict for first time"""
930
+ control = {"controlId": "AC-1", "id": 1}
931
+ controls_to_reports = {}
932
+ cr = MagicMock()
933
+
934
+ _add_controls_to_controls_to_report_dict(control, controls_to_reports, cr)
935
+
936
+ self.assertIn("ac-1", controls_to_reports)
937
+ self.assertEqual(len(controls_to_reports["ac-1"]), 1)
938
+
939
+ def test_add_controls_to_controls_to_report_dict_existing(self):
940
+ """Test adding control to report dict when already exists"""
941
+ control = {"controlId": "AC-1", "id": 1}
942
+ cr1 = MagicMock()
943
+ controls_to_reports = {"ac-1": [cr1]}
944
+ cr2 = MagicMock()
945
+
946
+ _add_controls_to_controls_to_report_dict(control, controls_to_reports, cr2)
947
+
948
+ self.assertEqual(len(controls_to_reports["ac-1"]), 2)
949
+
950
+ def test_clean_passing_list(self):
951
+ """Test cleaning passing list removes failing controls"""
952
+ passing = {"ac-1": {"controlId": "AC-1"}, "ac-2": {"controlId": "AC-2"}}
953
+ failing = {"ac-2": {"controlId": "AC-2"}}
954
+
955
+ _clean_passing_list(passing, failing)
956
+
957
+ self.assertIn("ac-1", passing)
958
+ self.assertNotIn("ac-2", passing)
959
+
960
+
961
+ # ==================== Assessment Creation Tests ====================
962
+ class TestAssessmentCreation(unittest.TestCase):
963
+ """Test assessment creation from compliance reports"""
964
+
965
+ def test_create_aggregated_assessment_report_with_failures(self):
966
+ """Test creating aggregated assessment report with failures"""
967
+ asset_details = [
968
+ {
969
+ "resource_name": "resource1",
970
+ "resource_id": "id1",
971
+ "cloud_provider": "AWS",
972
+ "subscription": "sub1",
973
+ "result": "Fail",
974
+ "policy_short_name": "AC-1",
975
+ "compliance_check": "access control",
976
+ "severity": "High",
977
+ "assessed_at": "2023-07-15",
978
+ },
979
+ {
980
+ "resource_name": "resource2",
981
+ "resource_id": "id2",
982
+ "cloud_provider": "Azure",
983
+ "subscription": "sub2",
984
+ "result": "Pass",
985
+ "policy_short_name": "AC-2",
986
+ "compliance_check": "account management",
987
+ "severity": "Medium",
988
+ "assessed_at": "2023-07-15",
989
+ },
990
+ ]
991
+
992
+ result = _create_aggregated_assessment_report(
993
+ control_id="AC-1",
994
+ overall_result="Fail",
995
+ pass_count=1,
996
+ fail_count=1,
997
+ asset_details=asset_details,
998
+ total_assets=2,
999
+ )
1000
+
1001
+ self.assertIn("AC-1", result)
1002
+ self.assertIn("Fail", result)
1003
+ self.assertIn("resource1", result)
1004
+ self.assertIn("resource2", result)
1005
+ self.assertIn("Total Assets Assessed:", result)
1006
+
1007
+ def test_create_aggregated_assessment_report_all_pass(self):
1008
+ """Test creating aggregated assessment report with all passing"""
1009
+ asset_details = [
1010
+ {
1011
+ "resource_name": "resource1",
1012
+ "resource_id": "id1",
1013
+ "cloud_provider": "AWS",
1014
+ "subscription": "sub1",
1015
+ "result": "Pass",
1016
+ "policy_short_name": "AC-1",
1017
+ "compliance_check": "access control",
1018
+ "severity": "Low",
1019
+ "assessed_at": "2023-07-15",
1020
+ }
1021
+ ]
1022
+
1023
+ result = _create_aggregated_assessment_report(
1024
+ control_id="AC-1",
1025
+ overall_result="Pass",
1026
+ pass_count=1,
1027
+ fail_count=0,
1028
+ asset_details=asset_details,
1029
+ total_assets=1,
1030
+ )
1031
+
1032
+ self.assertIn("AC-1", result)
1033
+ self.assertIn("Pass", result)
1034
+ self.assertIn("#2e7d32", result) # Green color for pass
1035
+
1036
+ @patch(f"{PATH}.Assessment")
1037
+ @patch(f"{PATH}.update_implementation_status")
1038
+ def test_create_report_assessment_with_failures(self, mock_update_status, mock_assessment):
1039
+ """Test creating report assessment with failures"""
1040
+ implementation = MagicMock()
1041
+ implementation.id = 123
1042
+ implementation.createdById = 456
1043
+
1044
+ reports = [
1045
+ MagicMock(
1046
+ result="Fail",
1047
+ resource_name="res1",
1048
+ resource_id="id1",
1049
+ cloud_provider="AWS",
1050
+ subscription="sub1",
1051
+ policy_short_name="AC-1",
1052
+ compliance_check="access control",
1053
+ severity="High",
1054
+ assessed_at="2023-07-15",
1055
+ )
1056
+ ]
1057
+
1058
+ mock_assessment_instance = MagicMock()
1059
+ mock_assessment_instance.id = 789
1060
+ mock_assessment.return_value = mock_assessment_instance
1061
+ mock_assessment_instance.create.return_value = mock_assessment_instance
1062
+
1063
+ create_report_assessment([implementation], reports, "AC-1", update_control_status=True)
1064
+
1065
+ mock_assessment.assert_called_once()
1066
+ mock_assessment_instance.create.assert_called_once()
1067
+ mock_update_status.assert_called_once()
1068
+
1069
+ @patch(f"{PATH}.Assessment")
1070
+ def test_create_report_assessment_no_implementation(self, mock_assessment):
1071
+ """Test creating report assessment with no implementation"""
1072
+ create_report_assessment([], [], "AC-1")
1073
+
1074
+ mock_assessment.assert_not_called()
1075
+
1076
+ @patch(f"{PATH}.Assessment")
1077
+ @patch(f"{PATH}.update_implementation_status")
1078
+ def test_create_report_assessment_update_status_disabled(self, mock_update_status, mock_assessment):
1079
+ """Test creating report assessment with status update disabled"""
1080
+ implementation = MagicMock()
1081
+ implementation.id = 123
1082
+ implementation.createdById = 456
1083
+
1084
+ reports = [
1085
+ MagicMock(
1086
+ result="Pass",
1087
+ resource_name="res1",
1088
+ resource_id="id1",
1089
+ cloud_provider="AWS",
1090
+ subscription="sub1",
1091
+ policy_short_name="AC-1",
1092
+ compliance_check="access control",
1093
+ severity="Low",
1094
+ assessed_at="2023-07-15",
1095
+ )
1096
+ ]
1097
+
1098
+ mock_assessment_instance = MagicMock()
1099
+ mock_assessment.return_value = mock_assessment_instance
1100
+ mock_assessment_instance.create.return_value = mock_assessment_instance
1101
+
1102
+ create_report_assessment([implementation], reports, "AC-1", update_control_status=False)
1103
+
1104
+ mock_update_status.assert_not_called()
1105
+
1106
+
1107
+ # ==================== Status Mapping Tests ====================
1108
+ class TestStatusMapping(unittest.TestCase):
1109
+ """Test compliance status to implementation status mapping"""
1110
+
1111
+ def test_get_default_status_mapping_pass(self):
1112
+ """Test default status mapping for Pass"""
1113
+ result = _get_default_status_mapping(ComplianceCheckStatus.PASS.value)
1114
+ self.assertEqual(result, "Implemented")
1115
+
1116
+ def test_get_default_status_mapping_fail(self):
1117
+ """Test default status mapping for Fail"""
1118
+ result = _get_default_status_mapping(ComplianceCheckStatus.FAIL.value)
1119
+ self.assertEqual(result, "In Remediation")
1120
+
1121
+ def test_get_default_status_mapping_other(self):
1122
+ """Test default status mapping for other status"""
1123
+ result = _get_default_status_mapping("Unknown")
1124
+ self.assertEqual(result, "Not Implemented")
1125
+
1126
+ def test_match_label_to_result_pass(self):
1127
+ """Test matching label to Pass result"""
1128
+ result = _match_label_to_result("Implemented", ComplianceCheckStatus.PASS.value.lower())
1129
+ self.assertEqual(result, "Implemented")
1130
+
1131
+ result = _match_label_to_result("Complete", ComplianceCheckStatus.PASS.value.lower())
1132
+ self.assertEqual(result, "Complete")
1133
+
1134
+ def test_match_label_to_result_fail(self):
1135
+ """Test matching label to Fail result"""
1136
+ result = _match_label_to_result("InRemediation", ComplianceCheckStatus.FAIL.value.lower())
1137
+ self.assertEqual(result, "InRemediation")
1138
+
1139
+ result = _match_label_to_result("Failed", ComplianceCheckStatus.FAIL.value.lower())
1140
+ self.assertEqual(result, "Failed")
1141
+
1142
+ def test_match_label_to_result_no_match(self):
1143
+ """Test matching label with no match"""
1144
+ result = _match_label_to_result("SomeOtherLabel", ComplianceCheckStatus.PASS.value.lower())
1145
+ self.assertIsNone(result)
1146
+
1147
+ @patch(f"{PATH}.get_wiz_compliance_settings")
1148
+ def test_report_result_to_implementation_status_with_settings(self, mock_get_settings):
1149
+ """Test converting report result with compliance settings"""
1150
+ mock_settings = MagicMock()
1151
+ mock_settings.get_field_labels.return_value = ["Implemented", "InRemediation", "NotImplemented"]
1152
+ mock_get_settings.return_value = mock_settings
1153
+
1154
+ result = report_result_to_implementation_status("Pass")
1155
+
1156
+ self.assertEqual(result, "Implemented")
1157
+
1158
+ @patch(f"{PATH}.get_wiz_compliance_settings")
1159
+ def test_report_result_to_implementation_status_no_settings(self, mock_get_settings):
1160
+ """Test converting report result without compliance settings"""
1161
+ mock_get_settings.return_value = None
1162
+
1163
+ result = report_result_to_implementation_status("Pass")
1164
+
1165
+ self.assertEqual(result, "Implemented")
1166
+
1167
+ @patch(f"{PATH}.ComplianceSettings")
1168
+ def test_get_wiz_compliance_settings_found(self, mock_compliance_settings):
1169
+ """Test getting Wiz compliance settings when found"""
1170
+ mock_setting = MagicMock()
1171
+ mock_setting.title = "Wiz Compliance Setting"
1172
+ mock_compliance_settings.get_by_current_tenant.return_value = [mock_setting]
1173
+
1174
+ result = get_wiz_compliance_settings()
1175
+
1176
+ self.assertEqual(result, mock_setting)
1177
+
1178
+ @patch(f"{PATH}.ComplianceSettings")
1179
+ def test_get_wiz_compliance_settings_not_found(self, mock_compliance_settings):
1180
+ """Test getting Wiz compliance settings when not found"""
1181
+ mock_other_setting = MagicMock()
1182
+ mock_other_setting.title = "Other Setting"
1183
+ mock_compliance_settings.get_by_current_tenant.return_value = [mock_other_setting]
1184
+
1185
+ result = get_wiz_compliance_settings()
1186
+
1187
+ self.assertIsNone(result)
1188
+
1189
+ @patch(f"{PATH}.ComplianceSettings")
1190
+ def test_get_wiz_compliance_settings_exception(self, mock_compliance_settings):
1191
+ """Test getting Wiz compliance settings with exception"""
1192
+ mock_compliance_settings.get_by_current_tenant.side_effect = Exception("API Error")
1193
+
1194
+ result = get_wiz_compliance_settings()
1195
+
1196
+ self.assertIsNone(result)
1197
+
1198
+ @patch(f"{PATH}.ImplementationObjective")
1199
+ def test_update_implementation_status_with_objectives(self, mock_objective):
1200
+ """Test updating implementation status with objectives"""
1201
+ implementation = MagicMock()
1202
+ implementation.id = 123
1203
+ implementation.get_module_slug.return_value = "controls"
1204
+
1205
+ objective = MagicMock()
1206
+ objective.id = 456
1207
+ mock_objective.get_all_by_parent.return_value = [objective]
1208
+
1209
+ update_implementation_status(implementation, "Pass")
1210
+
1211
+ objective.save.assert_called_once()
1212
+ implementation.save.assert_called_once()
1213
+
1214
+ @patch(f"{PATH}.ImplementationObjective")
1215
+ def test_update_implementation_status_no_objectives(self, mock_objective):
1216
+ """Test updating implementation status without objectives"""
1217
+ implementation = MagicMock()
1218
+ implementation.id = 123
1219
+ implementation.get_module_slug.return_value = "controls"
1220
+
1221
+ mock_objective.get_all_by_parent.return_value = []
1222
+
1223
+ update_implementation_status(implementation, "Pass")
1224
+
1225
+ self.assertEqual(implementation.objectives, [])
1226
+ implementation.save.assert_called_once()
1227
+
1228
+
1229
+ # ==================== Vulnerability Creation Tests ====================
1230
+ class TestVulnerabilityCreation(unittest.TestCase):
1231
+ """Test vulnerability creation from Wiz findings"""
1232
+
1233
+ @patch(f"{PATH}.WizVariables")
1234
+ @patch("regscale.integrations.commercial.wizv2.scanner.WizVulnerabilityIntegration")
1235
+ def test_create_vulnerabilities_from_wiz_findings_success(self, mock_wiz_integration_class, mock_vars):
1236
+ """Test successful vulnerability creation"""
1237
+ mock_vars.wizAccessToken = "test-token"
1238
+
1239
+ mock_integration = MagicMock()
1240
+ mock_wiz_integration_class.return_value = mock_integration
1241
+ mock_wiz_integration_class.sync_findings.return_value = 10
1242
+
1243
+ result = create_vulnerabilities_from_wiz_findings(
1244
+ wiz_project_id="project-123", regscale_plan_id=456, client_id="client-id", client_secret="client-secret"
1245
+ )
1246
+
1247
+ self.assertEqual(result, 10)
1248
+ mock_integration.authenticate.assert_called_once_with(client_id="client-id", client_secret="client-secret")
1249
+
1250
+ @patch(f"{PATH}.WizVariables")
1251
+ @patch("regscale.integrations.commercial.wizv2.scanner.WizVulnerabilityIntegration")
1252
+ def test_create_vulnerabilities_from_wiz_findings_with_filter(self, mock_wiz_integration_class, mock_vars):
1253
+ """Test vulnerability creation with filter override"""
1254
+ mock_vars.wizAccessToken = "test-token"
1255
+
1256
+ mock_integration = MagicMock()
1257
+ mock_wiz_integration_class.return_value = mock_integration
1258
+ mock_wiz_integration_class.sync_findings.return_value = 5
1259
+
1260
+ filter_override = '{"severity": "HIGH"}'
1261
+ result = create_vulnerabilities_from_wiz_findings(
1262
+ wiz_project_id="project-123", regscale_plan_id=456, filter_by_override=filter_override
1263
+ )
1264
+
1265
+ self.assertEqual(result, 5)
1266
+
1267
+ @patch(f"{PATH}.WizVariables")
1268
+ @patch("regscale.integrations.commercial.wizv2.scanner.WizVulnerabilityIntegration")
1269
+ def test_create_vulnerabilities_from_wiz_findings_error(self, mock_wiz_integration_class, mock_vars):
1270
+ """Test vulnerability creation with error"""
1271
+ mock_vars.wizAccessToken = "test-token"
1272
+
1273
+ mock_wiz_integration_class.side_effect = Exception("Integration Error")
1274
+
1275
+ with self.assertRaises(Exception):
1276
+ create_vulnerabilities_from_wiz_findings(wiz_project_id="project-123", regscale_plan_id=456)
1277
+
1278
+ @patch("regscale.integrations.commercial.wizv2.scanner.WizVulnerabilityIntegration")
1279
+ def test_create_single_vulnerability_from_wiz_data_success(self, mock_wiz_integration_class):
1280
+ """Test creating single vulnerability successfully"""
1281
+ mock_integration = MagicMock()
1282
+ mock_wiz_integration_class.return_value = mock_integration
1283
+
1284
+ # Mock scan history with RegScaleModel-like behavior
1285
+ mock_scan_history_instance = MagicMock()
1286
+ mock_scan_history_instance.id = 456
1287
+
1288
+ # Create a mock ScanHistory class that returns the instance from get_by_id
1289
+ with patch(f"{PATH}.regscale_models.ScanHistory") as mock_scan_history_class:
1290
+ mock_scan_history_class.get_by_id.return_value = mock_scan_history_instance
1291
+
1292
+ mock_finding = MagicMock()
1293
+ mock_integration.parse_finding.return_value = mock_finding
1294
+
1295
+ mock_asset = MagicMock()
1296
+ mock_integration.get_asset_by_identifier.return_value = mock_asset
1297
+
1298
+ mock_integration.handle_vulnerability.return_value = 789
1299
+
1300
+ # Mock Vulnerability class with get_by_id
1301
+ mock_vuln = MagicMock()
1302
+ mock_vuln.id = 789
1303
+ with patch(f"{PATH}.regscale_models.Vulnerability") as mock_vuln_class:
1304
+ mock_vuln_class.get_by_id.return_value = mock_vuln
1305
+
1306
+ wiz_finding_data = {"id": "finding-123", "severity": "HIGH"}
1307
+ result = create_single_vulnerability_from_wiz_data(
1308
+ wiz_finding_data=wiz_finding_data, asset_id="asset-456", regscale_plan_id=123, scan_history_id=456
1309
+ )
1310
+
1311
+ self.assertIsNotNone(result)
1312
+ # The mock returns a MagicMock with id = 789
1313
+ self.assertEqual(result, mock_vuln)
1314
+
1315
+ @patch("regscale.integrations.commercial.wizv2.scanner.WizVulnerabilityIntegration")
1316
+ def test_create_single_vulnerability_from_wiz_data_no_scan_history(self, mock_wiz_integration_class):
1317
+ """Test creating single vulnerability without scan history"""
1318
+ mock_integration = MagicMock()
1319
+ mock_wiz_integration_class.return_value = mock_integration
1320
+
1321
+ mock_scan_history = MagicMock()
1322
+ mock_integration.create_scan_history.return_value = mock_scan_history
1323
+
1324
+ mock_finding = MagicMock()
1325
+ mock_integration.parse_finding.return_value = mock_finding
1326
+
1327
+ mock_asset = MagicMock()
1328
+ mock_integration.get_asset_by_identifier.return_value = mock_asset
1329
+
1330
+ mock_integration.handle_vulnerability.return_value = 789
1331
+
1332
+ mock_vuln = MagicMock()
1333
+ mock_vuln.id = 789
1334
+ with patch(f"{PATH}.regscale_models.Vulnerability") as mock_vuln_class:
1335
+ mock_vuln_class.get_by_id.return_value = mock_vuln
1336
+
1337
+ wiz_finding_data = {"id": "finding-123", "severity": "HIGH"}
1338
+ result = create_single_vulnerability_from_wiz_data(
1339
+ wiz_finding_data=wiz_finding_data, asset_id="asset-456", regscale_plan_id=123
1340
+ )
1341
+
1342
+ self.assertIsNotNone(result)
1343
+ mock_integration.create_scan_history.assert_called_once()
1344
+
1345
+ @patch("regscale.integrations.commercial.wizv2.scanner.WizVulnerabilityIntegration")
1346
+ def test_create_single_vulnerability_from_wiz_data_parse_failure(self, mock_wiz_integration_class):
1347
+ """Test creating single vulnerability with parse failure"""
1348
+ mock_integration = MagicMock()
1349
+ mock_wiz_integration_class.return_value = mock_integration
1350
+
1351
+ mock_scan_history = MagicMock()
1352
+ mock_integration.create_scan_history.return_value = mock_scan_history
1353
+
1354
+ mock_integration.parse_finding.return_value = None
1355
+
1356
+ wiz_finding_data = {"id": "finding-123"}
1357
+ result = create_single_vulnerability_from_wiz_data(
1358
+ wiz_finding_data=wiz_finding_data, asset_id="asset-456", regscale_plan_id=123
1359
+ )
1360
+
1361
+ self.assertIsNone(result)
1362
+
1363
+
1364
+ # ==================== Deprecated and Legacy Function Tests ====================
1365
+ class TestDeprecatedFunctions(unittest.TestCase):
1366
+ """Test deprecated and legacy functions"""
1367
+
1368
+ @patch(f"{PATH}.send_request")
1369
+ def test_fetch_report_id_success(self, mock_send_request):
1370
+ """Test deprecated fetch_report_id success"""
1371
+ mock_response = MagicMock()
1372
+ mock_response.json.return_value = {"data": {"createReport": {"report": {"id": "report-123"}}}}
1373
+ mock_send_request.return_value = mock_response
1374
+
1375
+ result = fetch_report_id(query="query { test }", variables={}, url="https://api.wiz.io/graphql")
1376
+
1377
+ self.assertEqual(result, "report-123")
1378
+
1379
+ @patch(f"{PATH}.send_request")
1380
+ @patch(f"{PATH}.error_and_exit")
1381
+ def test_fetch_report_id_with_error(self, mock_error_exit, mock_send_request):
1382
+ """Test deprecated fetch_report_id with error"""
1383
+ mock_response = MagicMock()
1384
+ mock_response.json.return_value = {"error": "API Error"}
1385
+ mock_send_request.return_value = mock_response
1386
+ mock_error_exit.side_effect = SystemExit(1)
1387
+
1388
+ with pytest.raises(SystemExit):
1389
+ fetch_report_id(query="query { test }", variables={}, url="https://api.wiz.io/graphql")
1390
+
1391
+ mock_error_exit.assert_called_once()
1392
+
1393
+ @patch(f"{PATH}.send_request")
1394
+ def test_fetch_report_id_request_exception(self, mock_send_request):
1395
+ """Test deprecated fetch_report_id with request exception"""
1396
+ mock_send_request.side_effect = requests.RequestException("Connection Error")
1397
+
1398
+ result = fetch_report_id(query="query { test }", variables={}, url="https://api.wiz.io/graphql")
1399
+
1400
+ self.assertEqual(result, "")
1401
+
1402
+
1403
+ # ==================== Integration Tests ====================
1404
+ class TestReportProcessingIntegration(unittest.TestCase):
1405
+ """Integration tests for report processing workflow"""
1406
+
1407
+ @patch(f"{PATH}.get_or_create_report_id")
1408
+ @patch(f"{PATH}.fetch_report_data")
1409
+ def test_process_single_report_workflow(self, mock_fetch_data, mock_get_report_id):
1410
+ """Test complete single report processing workflow"""
1411
+ mock_get_report_id.return_value = "report-123"
1412
+ mock_fetch_data.return_value = [{"col1": "val1"}, {"col1": "val2"}]
1413
+
1414
+ project_id = "project-456"
1415
+ frameworks = ["NIST_SP_800-53_Revision_5"]
1416
+ wiz_frameworks = [{"id": "framework-1", "name": "NIST SP 800-53 Revision 5"}]
1417
+ existing_reports = []
1418
+ target_framework = "NIST_SP_800-53_Revision_5"
1419
+
1420
+ result = process_single_report(project_id, frameworks, wiz_frameworks, existing_reports, target_framework)
1421
+
1422
+ self.assertEqual(len(result), 2)
1423
+ mock_get_report_id.assert_called_once()
1424
+ mock_fetch_data.assert_called_once_with("report-123")
1425
+
1426
+ @patch(f"{PATH}.get_report_url_and_status")
1427
+ @patch(f"{PATH}.requests.get")
1428
+ @patch(f"{PATH}.error_and_exit")
1429
+ def test_fetch_report_data_success(self, mock_error_exit, mock_requests_get, mock_get_url):
1430
+ """Test successful report data fetching"""
1431
+ mock_get_url.return_value = "https://example.com/report.csv"
1432
+
1433
+ mock_response = MagicMock()
1434
+ mock_response.raise_for_status.return_value = None
1435
+ mock_response.iter_lines.return_value = iter([b"col1,col2", b"val1,val2", b"val3,val4"])
1436
+ mock_requests_get.return_value.__enter__.return_value = mock_response
1437
+
1438
+ result = fetch_report_data("report-123")
1439
+
1440
+ self.assertIsNotNone(result)
1441
+ mock_get_url.assert_called_once_with("report-123")
1442
+
1443
+ @patch(f"{PATH}.get_report_url_and_status")
1444
+ @patch(f"{PATH}.requests.get")
1445
+ @patch(f"{PATH}.error_and_exit")
1446
+ def test_fetch_report_data_request_error(self, mock_error_exit, mock_requests_get, mock_get_url):
1447
+ """Test report data fetching with request error"""
1448
+ mock_get_url.return_value = "https://example.com/report.csv"
1449
+ mock_requests_get.side_effect = requests.RequestException("Connection Error")
1450
+ mock_error_exit.side_effect = SystemExit(1)
1451
+
1452
+ with pytest.raises(SystemExit):
1453
+ fetch_report_data("report-123")
1454
+
1455
+ mock_error_exit.assert_called_once()
1456
+
1457
+ @patch(f"{PATH}.fetch_frameworks")
1458
+ @patch(f"{PATH}.get_framework_names")
1459
+ @patch(f"{PATH}.query_reports")
1460
+ @patch(f"{PATH}.process_single_report")
1461
+ def test_fetch_framework_report_workflow(
1462
+ self, mock_process_report, mock_query_reports, mock_get_names, mock_fetch_frameworks
1463
+ ):
1464
+ """Test complete framework report fetching workflow"""
1465
+ mock_fetch_frameworks.return_value = [{"id": "framework-1", "name": "NIST SP 800-53 Revision 5"}]
1466
+ mock_get_names.return_value = ["NIST_SP_800-53_Revision_5"]
1467
+ mock_query_reports.return_value = []
1468
+ mock_process_report.return_value = [{"data": "report_data"}]
1469
+
1470
+ result = fetch_framework_report("project-123", "NIST_SP_800-53_Revision_5")
1471
+
1472
+ self.assertEqual(len(result), 1)
1473
+ mock_fetch_frameworks.assert_called_once()
1474
+ mock_query_reports.assert_called_once_with("project-123")
1475
+
1476
+
1477
+ # ==================== Edge Cases and Error Handling Tests ====================
1478
+ class TestEdgeCasesAndErrorHandling(unittest.TestCase):
1479
+ """Test edge cases and error handling"""
1480
+
1481
+ def test_get_notes_from_wiz_props_empty_values(self):
1482
+ """Test getting notes with empty string values"""
1483
+ wiz_properties = {"cloudPlatform": "", "providerUniqueId": ""}
1484
+ external_id = ""
1485
+
1486
+ result = get_notes_from_wiz_props(wiz_properties, external_id)
1487
+
1488
+ # With all empty values, result should be empty string
1489
+ # The function only includes values if they are truthy
1490
+ self.assertEqual(result, "")
1491
+
1492
+ @patch(f"{PATH}.regscale_models.Metadata.get_metadata_by_module_field")
1493
+ @patch(f"{PATH}.regscale_models.Metadata")
1494
+ def test_create_asset_type_with_special_characters(self, mock_metadata_class, mock_get_metadata):
1495
+ """Test creating asset type with special characters"""
1496
+ mock_get_metadata.return_value = []
1497
+ mock_metadata_instance = MagicMock()
1498
+ mock_metadata_class.return_value = mock_metadata_instance
1499
+
1500
+ result = create_asset_type("test-asset_TYPE_123")
1501
+
1502
+ # Should handle title case and underscore replacement
1503
+ self.assertIn("Test-Asset", result)
1504
+ self.assertIn("Type", result)
1505
+
1506
+ @patch(f"{PATH}.time.sleep")
1507
+ @patch(f"{PATH}.download_report")
1508
+ @patch(f"{PATH}.MAX_RETRIES", 2)
1509
+ def test_get_report_url_and_status_max_retries_exceeded(self, mock_download_report, mock_sleep):
1510
+ """Test exceeding max retries for report download"""
1511
+ mock_response = MagicMock()
1512
+ mock_response.ok = True
1513
+ mock_response.json.return_value = {"data": {"report": {"lastRun": {"status": "PROCESSING"}}}}
1514
+ mock_download_report.return_value = mock_response
1515
+
1516
+ with self.assertRaises(requests.RequestException) as context:
1517
+ get_report_url_and_status("report-123")
1518
+
1519
+ self.assertIn("exceeding the maximum number of retries", str(context.exception))
1520
+
1521
+
1522
+ if __name__ == "__main__":
1523
+ unittest.main()