regscale-cli 6.25.0.1__py3-none-any.whl → 6.26.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of regscale-cli might be problematic. Click here for more details.

Files changed (84) hide show
  1. regscale/_version.py +1 -1
  2. regscale/airflow/hierarchy.py +2 -2
  3. regscale/core/app/application.py +18 -3
  4. regscale/core/app/internal/login.py +0 -1
  5. regscale/core/app/utils/catalog_utils/common.py +1 -1
  6. regscale/integrations/commercial/sicura/api.py +14 -13
  7. regscale/integrations/commercial/sicura/commands.py +8 -2
  8. regscale/integrations/commercial/sicura/scanner.py +49 -39
  9. regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
  10. regscale/integrations/commercial/synqly/assets.py +17 -0
  11. regscale/integrations/commercial/wizv2/click.py +26 -26
  12. regscale/integrations/commercial/wizv2/compliance_report.py +152 -157
  13. regscale/integrations/commercial/wizv2/constants.py +20 -71
  14. regscale/integrations/commercial/wizv2/scanner.py +3 -3
  15. regscale/integrations/compliance_integration.py +67 -2
  16. regscale/integrations/control_matcher.py +358 -0
  17. regscale/integrations/due_date_handler.py +118 -6
  18. regscale/integrations/milestone_manager.py +291 -0
  19. regscale/integrations/public/__init__.py +1 -0
  20. regscale/integrations/public/cci_importer.py +37 -38
  21. regscale/integrations/public/fedramp/click.py +60 -2
  22. regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
  23. regscale/integrations/scanner_integration.py +199 -130
  24. regscale/models/integration_models/cisa_kev_data.json +199 -4
  25. regscale/models/integration_models/nexpose.py +36 -10
  26. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  27. regscale/models/locking.py +12 -8
  28. regscale/models/platform.py +1 -2
  29. regscale/models/regscale_models/control_implementation.py +46 -21
  30. regscale/models/regscale_models/issue.py +256 -94
  31. regscale/models/regscale_models/milestone.py +1 -1
  32. regscale/models/regscale_models/regscale_model.py +6 -1
  33. regscale/templates/__init__.py +0 -0
  34. regscale/utils/threading/threadhandler.py +20 -15
  35. {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/METADATA +1 -1
  36. {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/RECORD +84 -37
  37. tests/regscale/integrations/commercial/__init__.py +0 -0
  38. tests/regscale/integrations/commercial/conftest.py +28 -0
  39. tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
  40. tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
  41. tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
  42. tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
  43. tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
  44. tests/regscale/integrations/commercial/test_aws.py +3731 -0
  45. tests/regscale/integrations/commercial/test_burp.py +48 -0
  46. tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
  47. tests/regscale/integrations/commercial/test_dependabot.py +341 -0
  48. tests/regscale/integrations/commercial/test_gcp.py +1543 -0
  49. tests/regscale/integrations/commercial/test_gitlab.py +549 -0
  50. tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
  51. tests/regscale/integrations/commercial/test_jira.py +1814 -0
  52. tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
  53. tests/regscale/integrations/commercial/test_okta.py +1228 -0
  54. tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
  55. tests/regscale/integrations/commercial/test_sicura.py +350 -0
  56. tests/regscale/integrations/commercial/test_snow.py +423 -0
  57. tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
  58. tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
  59. tests/regscale/integrations/commercial/test_stig.py +33 -0
  60. tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
  61. tests/regscale/integrations/commercial/test_stigv2.py +406 -0
  62. tests/regscale/integrations/commercial/test_wiz.py +1469 -0
  63. tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
  64. tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
  65. tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
  66. tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
  67. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
  68. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1351 -0
  69. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
  70. tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
  71. tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +750 -0
  72. tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
  73. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +264 -0
  74. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +624 -0
  75. tests/regscale/integrations/public/fedramp/__init__.py +1 -0
  76. tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
  77. tests/regscale/integrations/test_control_matcher.py +1314 -0
  78. tests/regscale/integrations/test_control_matching.py +155 -0
  79. tests/regscale/integrations/test_milestone_manager.py +408 -0
  80. tests/regscale/models/test_issue.py +378 -1
  81. {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/LICENSE +0 -0
  82. {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/WHEEL +0 -0
  83. {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/entry_points.txt +0 -0
  84. {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1351 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Comprehensive unit tests for Wiz Compliance Report Integration.
5
+
6
+ This test suite covers both WizComplianceReportItem and WizComplianceReportProcessor
7
+ classes with extensive mocking and edge case testing.
8
+ """
9
+
10
+ import csv
11
+ import gzip
12
+ import os
13
+ import tempfile
14
+ import unittest
15
+ from datetime import datetime, timedelta
16
+ from unittest.mock import MagicMock, patch
17
+
18
+ import pytest
19
+
20
+ from regscale.integrations.commercial.wizv2.compliance_report import (
21
+ WizComplianceReportItem,
22
+ WizComplianceReportProcessor,
23
+ )
24
+ from regscale.integrations.compliance_integration import ComplianceIntegration
25
+
26
+
27
+ @pytest.mark.no_parallel
28
+ class TestWizComplianceReportItem(unittest.TestCase):
29
+ """Test suite for WizComplianceReportItem class."""
30
+
31
+ def setUp(self):
32
+ """Set up test fixtures."""
33
+ self.sample_csv_row = {
34
+ "Resource Name": "test-vm-001",
35
+ "Cloud Provider": "Azure",
36
+ "Cloud Provider ID": "subscriptions/12345/resourceGroups/rg-test/providers/Microsoft.Compute/virtualMachines/test-vm-001",
37
+ "Resource ID": "/subscriptions/12345/resourceGroups/rg-test/providers/Microsoft.Compute/virtualMachines/test-vm-001",
38
+ "Resource Region": "East US",
39
+ "Subscription": "subscription-123",
40
+ "Subscription Name": "Dev Subscription",
41
+ "Policy Name": "Ensure VM disk encryption is enabled",
42
+ "Policy ID": "policy-disk-encryption-001",
43
+ "Result": "Pass",
44
+ "Severity": "HIGH",
45
+ "Compliance Check Name (Wiz Subcategory)": "AC-2(1) Account Management | Automated System Account Management",
46
+ "Framework": "NIST SP 800-53 Revision 5",
47
+ "Remediation Steps": "Enable disk encryption on the VM through Azure portal",
48
+ }
49
+
50
+ self.failing_csv_row = {
51
+ **self.sample_csv_row,
52
+ "Result": "Failed",
53
+ "Resource Name": "test-vm-002",
54
+ "Cloud Provider ID": "subscriptions/12345/resourceGroups/rg-test/providers/Microsoft.Compute/virtualMachines/test-vm-002",
55
+ }
56
+
57
+ def test_init_with_valid_csv_row(self):
58
+ """Test initialization with valid CSV row data."""
59
+ item = WizComplianceReportItem(self.sample_csv_row)
60
+
61
+ self.assertEqual(item._resource_name, "test-vm-001")
62
+ self.assertEqual(item.cloud_provider, "Azure")
63
+ self.assertEqual(
64
+ item.cloud_provider_id,
65
+ "subscriptions/12345/resourceGroups/rg-test/providers/Microsoft.Compute/virtualMachines/test-vm-001",
66
+ )
67
+ self.assertEqual(item.result, "Pass")
68
+ self.assertEqual(item._severity, "HIGH")
69
+ self.assertEqual(item.policy_name, "Ensure VM disk encryption is enabled")
70
+
71
+ def test_resource_id_property_priority(self):
72
+ """Test resource_id property uses correct priority: cloud_provider_id > _resource_id > _resource_name."""
73
+ # Test with all values present
74
+ item = WizComplianceReportItem(self.sample_csv_row)
75
+ self.assertEqual(item.resource_id, self.sample_csv_row["Cloud Provider ID"])
76
+
77
+ # Test with cloud_provider_id missing
78
+ csv_row_no_cloud_id = {**self.sample_csv_row, "Cloud Provider ID": ""}
79
+ item = WizComplianceReportItem(csv_row_no_cloud_id)
80
+ self.assertEqual(item.resource_id, self.sample_csv_row["Resource ID"])
81
+
82
+ # Test with both cloud_provider_id and resource_id missing
83
+ csv_row_no_ids = {**self.sample_csv_row, "Cloud Provider ID": "", "Resource ID": ""}
84
+ item = WizComplianceReportItem(csv_row_no_ids)
85
+ self.assertEqual(item.resource_id, "test-vm-001")
86
+
87
+ # Test with all missing
88
+ csv_row_all_empty = {**self.sample_csv_row, "Cloud Provider ID": "", "Resource ID": "", "Resource Name": ""}
89
+ item = WizComplianceReportItem(csv_row_all_empty)
90
+ self.assertEqual(item.resource_id, "Unknown")
91
+
92
+ def test_resource_name_property(self):
93
+ """Test resource_name property calls get_unique_resource_name."""
94
+ item = WizComplianceReportItem(self.sample_csv_row)
95
+ with patch.object(item, "get_unique_resource_name", return_value="mocked_name") as mock_method:
96
+ result = item.resource_name
97
+ self.assertEqual(result, "mocked_name")
98
+ mock_method.assert_called_once()
99
+
100
+ def test_control_id_property(self):
101
+ """Test control_id property calls get_control_id."""
102
+ item = WizComplianceReportItem(self.sample_csv_row)
103
+ with patch.object(item, "get_control_id", return_value="AC-2(1)") as mock_method:
104
+ result = item.control_id
105
+ self.assertEqual(result, "AC-2(1)")
106
+ mock_method.assert_called_once()
107
+
108
+ def test_compliance_result_property(self):
109
+ """Test compliance_result property returns result."""
110
+ item = WizComplianceReportItem(self.sample_csv_row)
111
+ self.assertEqual(item.compliance_result, "Pass")
112
+
113
+ def test_severity_property(self):
114
+ """Test severity property returns _severity or None if empty."""
115
+ item = WizComplianceReportItem(self.sample_csv_row)
116
+ self.assertEqual(item.severity, "HIGH")
117
+
118
+ # Test with empty severity
119
+ csv_row_no_severity = {**self.sample_csv_row, "Severity": ""}
120
+ item = WizComplianceReportItem(csv_row_no_severity)
121
+ self.assertIsNone(item.severity)
122
+
123
+ def test_description_property(self):
124
+ """Test description property calls get_finding_details."""
125
+ item = WizComplianceReportItem(self.sample_csv_row)
126
+ with patch.object(item, "get_finding_details", return_value="mocked_details") as mock_method:
127
+ result = item.description
128
+ self.assertEqual(result, "mocked_details")
129
+ mock_method.assert_called_once()
130
+
131
+ def test_framework_property_default(self):
132
+ """Test framework property returns NIST800-53R5 as default."""
133
+ csv_row_no_framework = {**self.sample_csv_row, "Framework": ""}
134
+ item = WizComplianceReportItem(csv_row_no_framework)
135
+ self.assertEqual(item.framework, "NIST800-53R5")
136
+
137
+ def test_framework_property_mapping(self):
138
+ """Test framework property maps Wiz framework names to RegScale format."""
139
+ test_cases = [
140
+ ("NIST SP 800-53 Revision 5", "NIST800-53R5"),
141
+ ("NIST SP 800-53 Rev 5", "NIST800-53R5"),
142
+ ("NIST SP 800-53 R5", "NIST800-53R5"),
143
+ ("NIST 800-53 Revision 5", "NIST800-53R5"),
144
+ ("NIST 800-53 Rev 5", "NIST800-53R5"),
145
+ ("NIST 800-53 R5", "NIST800-53R5"),
146
+ ("Unknown Framework", "Unknown Framework"),
147
+ ]
148
+
149
+ for wiz_framework, expected_regscale_framework in test_cases:
150
+ csv_row = {**self.sample_csv_row, "Framework": wiz_framework}
151
+ item = WizComplianceReportItem(csv_row)
152
+ self.assertEqual(item.framework, expected_regscale_framework)
153
+
154
+ def test_get_control_id_single_control(self):
155
+ """Test get_control_id returns first control ID from compliance check name."""
156
+ item = WizComplianceReportItem(self.sample_csv_row)
157
+ result = item.get_control_id()
158
+ self.assertEqual(result, "AC-2(1)")
159
+
160
+ def test_get_control_id_empty_compliance_check_name(self):
161
+ """Test get_control_id returns empty string when compliance check name is empty."""
162
+ csv_row_no_compliance_check = {**self.sample_csv_row, "Compliance Check Name (Wiz Subcategory)": ""}
163
+ item = WizComplianceReportItem(csv_row_no_compliance_check)
164
+ result = item.get_control_id()
165
+ self.assertEqual(result, "")
166
+
167
+ def test_get_all_control_ids_single_control(self):
168
+ """Test get_all_control_ids extracts control IDs correctly."""
169
+ item = WizComplianceReportItem(self.sample_csv_row)
170
+ result = item.get_all_control_ids()
171
+ self.assertEqual(result, ["AC-2(1)"])
172
+
173
+ def test_get_all_control_ids_multiple_controls(self):
174
+ """Test get_all_control_ids extracts multiple control IDs."""
175
+ csv_row_multiple_controls = {
176
+ **self.sample_csv_row,
177
+ "Compliance Check Name (Wiz Subcategory)": "AC-2(4) Account Management | Automated Audit Actions, AC-6(9) Least Privilege | Log Use of Privileged Functions",
178
+ }
179
+ item = WizComplianceReportItem(csv_row_multiple_controls)
180
+ result = item.get_all_control_ids()
181
+ self.assertEqual(result, ["AC-2(4)", "AC-6(9)"])
182
+
183
+ def test_get_all_control_ids_base_controls_without_enhancements(self):
184
+ """Test get_all_control_ids handles base controls without enhancements."""
185
+ csv_row_base_controls = {
186
+ **self.sample_csv_row,
187
+ "Compliance Check Name (Wiz Subcategory)": "AC-3 Access Enforcement, AC-4 Information Flow Enforcement",
188
+ }
189
+ item = WizComplianceReportItem(csv_row_base_controls)
190
+ result = item.get_all_control_ids()
191
+ self.assertEqual(result, ["AC-3", "AC-4"])
192
+
193
+ def test_get_all_control_ids_mixed_controls(self):
194
+ """Test get_all_control_ids handles mixed base and enhanced controls."""
195
+ csv_row_mixed_controls = {
196
+ **self.sample_csv_row,
197
+ "Compliance Check Name (Wiz Subcategory)": "AC-3 Access Enforcement, AC-4(2) Information Flow Enforcement | Processing Domains",
198
+ }
199
+ item = WizComplianceReportItem(csv_row_mixed_controls)
200
+ result = item.get_all_control_ids()
201
+ self.assertEqual(result, ["AC-3", "AC-4(2)"])
202
+
203
+ def test_get_all_control_ids_empty_compliance_check_name(self):
204
+ """Test get_all_control_ids returns empty list when compliance check name is empty."""
205
+ csv_row_no_compliance_check = {**self.sample_csv_row, "Compliance Check Name (Wiz Subcategory)": ""}
206
+ item = WizComplianceReportItem(csv_row_no_compliance_check)
207
+ result = item.get_all_control_ids()
208
+ self.assertEqual(result, [])
209
+
210
+ def test_control_id_normalization_leading_zeros(self):
211
+ """Test that control IDs with leading zeros are properly normalized."""
212
+ # Test base control with leading zeros
213
+ csv_row = {**self.sample_csv_row, "Compliance Check Name (Wiz Subcategory)": "AC-01 Access Control Policy"}
214
+ item = WizComplianceReportItem(csv_row)
215
+ self.assertEqual(item.get_control_id(), "AC-1")
216
+ self.assertEqual(item.get_all_control_ids(), ["AC-1"])
217
+
218
+ # Test enhancement with leading zeros in base control
219
+ csv_row = {
220
+ **self.sample_csv_row,
221
+ "Compliance Check Name (Wiz Subcategory)": "AC-01(04) Access Control Policy Enhancement",
222
+ }
223
+ item = WizComplianceReportItem(csv_row)
224
+ self.assertEqual(item.get_control_id(), "AC-1(4)")
225
+ self.assertEqual(item.get_all_control_ids(), ["AC-1(4)"])
226
+
227
+ # Test multiple controls with various leading zero patterns
228
+ csv_row = {
229
+ **self.sample_csv_row,
230
+ "Compliance Check Name (Wiz Subcategory)": "AC-01(04) Access Control, AU-003(001) Audit Content, SI-04(020) Monitoring",
231
+ }
232
+ item = WizComplianceReportItem(csv_row)
233
+ self.assertEqual(item.get_all_control_ids(), ["AC-1(4)", "AU-3(1)", "SI-4(20)"])
234
+
235
+ def test_enhancement_normalization(self):
236
+ """Test that enhancement numbers are normalized to remove leading zeros."""
237
+ # Single digit enhancement with leading zeros
238
+ csv_row = {
239
+ **self.sample_csv_row,
240
+ "Compliance Check Name (Wiz Subcategory)": "AC-2(01) Account Management Enhancement",
241
+ }
242
+ item = WizComplianceReportItem(csv_row)
243
+ self.assertEqual(item.get_control_id(), "AC-2(1)")
244
+
245
+ # Double digit enhancement with leading zeros
246
+ csv_row = {
247
+ **self.sample_csv_row,
248
+ "Compliance Check Name (Wiz Subcategory)": "SI-4(020) System Monitoring Enhancement",
249
+ }
250
+ item = WizComplianceReportItem(csv_row)
251
+ self.assertEqual(item.get_control_id(), "SI-4(20)")
252
+
253
+ # Triple leading zeros
254
+ csv_row = {
255
+ **self.sample_csv_row,
256
+ "Compliance Check Name (Wiz Subcategory)": "AC-2(001) Account Management Enhancement",
257
+ }
258
+ item = WizComplianceReportItem(csv_row)
259
+ self.assertEqual(item.get_control_id(), "AC-2(1)")
260
+
261
+ def test_affected_controls_property_normalized(self):
262
+ """Test that affected_controls property returns normalized comma-separated list."""
263
+ csv_row = {
264
+ **self.sample_csv_row,
265
+ "Compliance Check Name (Wiz Subcategory)": "AC-01(04) Account Management, AC-06(009) Least Privilege",
266
+ }
267
+ item = WizComplianceReportItem(csv_row)
268
+
269
+ # affected_controls should return all normalized control IDs as comma-separated string
270
+ self.assertEqual(item.affected_controls, "AC-1(4),AC-6(9)")
271
+
272
+ def test_normalize_base_control_method(self):
273
+ """Test the _normalize_base_control method directly."""
274
+ csv_row = {**self.sample_csv_row}
275
+ item = WizComplianceReportItem(csv_row)
276
+
277
+ # Test various base control formats
278
+ self.assertEqual(item._normalize_base_control("AC-01"), "AC-1")
279
+ self.assertEqual(item._normalize_base_control("AC-1"), "AC-1")
280
+ self.assertEqual(item._normalize_base_control("AU-003"), "AU-3")
281
+ self.assertEqual(item._normalize_base_control("SI-004"), "SI-4")
282
+ self.assertEqual(item._normalize_base_control("sc-7"), "SC-7") # lowercase
283
+ self.assertEqual(item._normalize_base_control("PM-10"), "PM-10") # no leading zero
284
+
285
+ def test_format_control_id_method(self):
286
+ """Test the _format_control_id method directly."""
287
+ csv_row = {**self.sample_csv_row}
288
+ item = WizComplianceReportItem(csv_row)
289
+
290
+ # Test enhancement normalization
291
+ self.assertEqual(item._format_control_id("AC-1", "04"), "AC-1(4)")
292
+ self.assertEqual(item._format_control_id("AC-2", "001"), "AC-2(1)")
293
+ self.assertEqual(item._format_control_id("SI-4", "020"), "SI-4(20)")
294
+
295
+ # Test without enhancement
296
+ self.assertEqual(item._format_control_id("AC-1", ""), "AC-1")
297
+ self.assertEqual(item._format_control_id("AC-1", None), "AC-1")
298
+
299
+ # Test with already normalized enhancement
300
+ self.assertEqual(item._format_control_id("AC-1", "4"), "AC-1(4)")
301
+ self.assertEqual(item._format_control_id("AC-2", "12"), "AC-2(12)")
302
+
303
+ def test_get_status_case_insensitive(self):
304
+ """Test get_status handles case-insensitive result values."""
305
+ test_cases = [
306
+ ("pass", "Satisfied"),
307
+ ("Pass", "Satisfied"),
308
+ ("PASS", "Satisfied"),
309
+ ("fail", "Other Than Satisfied"),
310
+ ("Failed", "Other Than Satisfied"),
311
+ ("FAILED", "Other Than Satisfied"),
312
+ ("unknown", "Other Than Satisfied"),
313
+ ]
314
+
315
+ for result_value, expected_status in test_cases:
316
+ with self.subTest(result_value=result_value):
317
+ csv_row = {**self.sample_csv_row, "Result": result_value}
318
+ item = WizComplianceReportItem(csv_row)
319
+ result = item.get_status()
320
+ self.assertEqual(result, expected_status)
321
+
322
+ def test_get_implementation_status_case_insensitive(self):
323
+ """Test get_implementation_status handles case-insensitive result values."""
324
+ test_cases = [
325
+ ("pass", "Implemented"),
326
+ ("Pass", "Implemented"),
327
+ ("PASS", "Implemented"),
328
+ ("fail", "In Remediation"),
329
+ ("Failed", "In Remediation"),
330
+ ("FAILED", "In Remediation"),
331
+ ("unknown", "In Remediation"),
332
+ ]
333
+
334
+ for result_value, expected_status in test_cases:
335
+ with self.subTest(result_value=result_value):
336
+ csv_row = {**self.sample_csv_row, "Result": result_value}
337
+ item = WizComplianceReportItem(csv_row)
338
+ result = item.get_implementation_status()
339
+ self.assertEqual(result, expected_status)
340
+
341
+ def test_get_severity_mapping(self):
342
+ """Test get_severity maps Wiz severity to RegScale severity."""
343
+ test_cases = [
344
+ ("CRITICAL", "High"),
345
+ ("HIGH", "High"),
346
+ ("MEDIUM", "Moderate"),
347
+ ("LOW", "Low"),
348
+ ("INFORMATIONAL", "Low"),
349
+ ("unknown", "Low"),
350
+ ("", "Low"),
351
+ ]
352
+
353
+ for severity_value, expected_severity in test_cases:
354
+ with self.subTest(severity_value=severity_value):
355
+ csv_row = {**self.sample_csv_row, "Severity": severity_value}
356
+ item = WizComplianceReportItem(csv_row)
357
+ result = item.get_severity()
358
+ self.assertEqual(result, expected_severity)
359
+
360
+ def test_get_unique_resource_name_full_details(self):
361
+ """Test get_unique_resource_name with all details present."""
362
+ item = WizComplianceReportItem(self.sample_csv_row)
363
+ result = item.get_unique_resource_name()
364
+ # The method should not add duplicate suffixes when resource_id is same as resource_name
365
+ self.assertEqual(result, "test-vm-001 (East US)")
366
+
367
+ def test_get_unique_resource_name_no_resource_name(self):
368
+ """Test get_unique_resource_name when resource name is empty."""
369
+ csv_row_no_name = {**self.sample_csv_row, "Resource Name": ""}
370
+ item = WizComplianceReportItem(csv_row_no_name)
371
+ result = item.get_unique_resource_name()
372
+ self.assertTrue(result.startswith("Unknown Resource"))
373
+
374
+ def test_get_unique_resource_name_no_region(self):
375
+ """Test get_unique_resource_name without region."""
376
+ csv_row_no_region = {**self.sample_csv_row, "Resource Region": ""}
377
+ item = WizComplianceReportItem(csv_row_no_region)
378
+ result = item.get_unique_resource_name()
379
+ # Should not add duplicate suffix when resource_id contains resource_name
380
+ self.assertEqual(result, "test-vm-001")
381
+
382
+ def test_get_unique_resource_name_azure_resource_id_truncation(self):
383
+ """Test get_unique_resource_name truncates long Azure resource IDs."""
384
+ csv_row_long_id = {
385
+ **self.sample_csv_row,
386
+ "Resource ID": "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/very-long-resource-group-name/providers/Microsoft.Compute/virtualMachines/very-long-vm-name-that-should-be-truncated",
387
+ }
388
+ item = WizComplianceReportItem(csv_row_long_id)
389
+ result = item.get_unique_resource_name()
390
+ # Should truncate the resource ID to 12 characters
391
+ self.assertIn("[very-long-vm]", result)
392
+
393
+ def test_get_unique_resource_name_avoids_duplicate_suffix(self):
394
+ """Test get_unique_resource_name avoids adding suffix if already present."""
395
+ csv_row_duplicate_suffix = {
396
+ **self.sample_csv_row,
397
+ "Resource Name": "test-vm-001",
398
+ "Resource ID": "test-vm-001", # Same as resource name
399
+ }
400
+ item = WizComplianceReportItem(csv_row_duplicate_suffix)
401
+ result = item.get_unique_resource_name()
402
+ # Should not duplicate the resource name
403
+ self.assertEqual(result.count("test-vm-001"), 1)
404
+
405
+ def test_get_unique_issue_identifier(self):
406
+ """Test get_unique_issue_identifier creates unique identifier."""
407
+ item = WizComplianceReportItem(self.sample_csv_row)
408
+ result = item.get_unique_issue_identifier()
409
+ expected_parts = [
410
+ self.sample_csv_row["Resource ID"],
411
+ self.sample_csv_row["Policy ID"],
412
+ "AC-2(1)", # Expected control ID
413
+ ]
414
+ expected = "|".join(expected_parts)
415
+ self.assertEqual(result, expected)
416
+
417
+ def test_get_unique_issue_identifier_fallback_values(self):
418
+ """Test get_unique_issue_identifier uses fallback values when primary ones are missing."""
419
+ csv_row_fallbacks = {
420
+ **self.sample_csv_row,
421
+ "Resource ID": "",
422
+ "Policy ID": "",
423
+ }
424
+ item = WizComplianceReportItem(csv_row_fallbacks)
425
+ result = item.get_unique_issue_identifier()
426
+ # Should use cloud_provider_id for resource and policy_name for policy
427
+ expected_parts = [self.sample_csv_row["Cloud Provider ID"], self.sample_csv_row["Policy Name"], "AC-2(1)"]
428
+ expected = "|".join(expected_parts)
429
+ self.assertEqual(result, expected)
430
+
431
+ def test_get_title(self):
432
+ """Test get_title returns control ID and policy name."""
433
+ item = WizComplianceReportItem(self.sample_csv_row)
434
+ result = item.get_title()
435
+ self.assertEqual(result, "AC-2(1) - Ensure VM disk encryption is enabled")
436
+
437
+ def test_get_description(self):
438
+ """Test get_description returns assessment description."""
439
+ item = WizComplianceReportItem(self.sample_csv_row)
440
+ result = item.get_description()
441
+ expected = "Wiz compliance assessment for test-vm-001 (East US) - Ensure VM disk encryption is enabled"
442
+ self.assertEqual(result, expected)
443
+
444
+ def test_get_finding_details(self):
445
+ """Test get_finding_details returns formatted finding details."""
446
+ item = WizComplianceReportItem(self.sample_csv_row)
447
+ result = item.get_finding_details()
448
+
449
+ expected_lines = [
450
+ "Resource: test-vm-001 (East US)",
451
+ "Cloud Provider: Azure",
452
+ "Subscription: Dev Subscription",
453
+ "Result: Pass",
454
+ "Remediation: Enable disk encryption on the VM through Azure portal",
455
+ ]
456
+
457
+ for line in expected_lines:
458
+ self.assertIn(line, result)
459
+
460
+ def test_get_finding_details_no_subscription(self):
461
+ """Test get_finding_details without subscription name."""
462
+ csv_row_no_sub = {**self.sample_csv_row, "Subscription Name": ""}
463
+ item = WizComplianceReportItem(csv_row_no_sub)
464
+ result = item.get_finding_details()
465
+
466
+ # Should not include subscription line
467
+ self.assertNotIn("Subscription:", result)
468
+
469
+ def test_get_asset_identifier(self):
470
+ """Test get_asset_identifier returns correct priority identifier."""
471
+ item = WizComplianceReportItem(self.sample_csv_row)
472
+ result = item.get_asset_identifier()
473
+ self.assertEqual(result, self.sample_csv_row["Cloud Provider ID"])
474
+
475
+ def test_get_asset_identifier_fallbacks(self):
476
+ """Test get_asset_identifier uses fallback values."""
477
+ csv_row_fallbacks = {**self.sample_csv_row, "Cloud Provider ID": "", "Resource ID": "", "Resource Name": ""}
478
+ item = WizComplianceReportItem(csv_row_fallbacks)
479
+ result = item.get_asset_identifier()
480
+ self.assertEqual(result, "Unknown")
481
+
482
+
483
+ @pytest.mark.no_parallel
484
+ class TestWizComplianceReportProcessor(unittest.TestCase):
485
+ """Test suite for WizComplianceReportProcessor class."""
486
+
487
+ def setUp(self):
488
+ """Set up test fixtures."""
489
+ self.plan_id = 123
490
+ self.wiz_project_id = "test-project-123"
491
+ self.client_id = "test-client-id"
492
+ self.client_secret = "test-client-secret"
493
+
494
+ # Sample CSV data matching expected results
495
+ self.sample_csv_data = [
496
+ {
497
+ "Resource Name": "vm-001",
498
+ "Cloud Provider": "Azure",
499
+ "Cloud Provider ID": "vm-001-id",
500
+ "Resource ID": "vm-001-id",
501
+ "Resource Region": "East US",
502
+ "Subscription": "subscription-1",
503
+ "Subscription Name": "Dev Subscription",
504
+ "Policy Name": "Policy 1",
505
+ "Policy ID": "policy-001",
506
+ "Result": "Pass",
507
+ "Severity": "HIGH",
508
+ "Compliance Check Name (Wiz Subcategory)": "AC-2(1) Account Management",
509
+ "Framework": "NIST SP 800-53 Revision 5",
510
+ "Remediation Steps": "Fix this issue",
511
+ },
512
+ {
513
+ "Resource Name": "vm-002",
514
+ "Cloud Provider": "Azure",
515
+ "Cloud Provider ID": "vm-002-id",
516
+ "Resource ID": "vm-002-id",
517
+ "Resource Region": "West US",
518
+ "Subscription": "subscription-1",
519
+ "Subscription Name": "Dev Subscription",
520
+ "Policy Name": "Policy 2",
521
+ "Policy ID": "policy-002",
522
+ "Result": "Failed",
523
+ "Severity": "MEDIUM",
524
+ "Compliance Check Name (Wiz Subcategory)": "AC-3 Access Enforcement",
525
+ "Framework": "NIST SP 800-53 Revision 5",
526
+ "Remediation Steps": "Fix this issue",
527
+ },
528
+ ]
529
+
530
+ @patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager")
531
+ @patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate")
532
+ def test_init_successful_authentication(self, mock_auth, mock_report_manager):
533
+ """Test successful initialization with authentication."""
534
+ mock_auth.return_value = "test-token"
535
+ mock_report_manager_instance = MagicMock()
536
+ mock_report_manager.return_value = mock_report_manager_instance
537
+
538
+ processor = WizComplianceReportProcessor(
539
+ plan_id=self.plan_id,
540
+ wiz_project_id=self.wiz_project_id,
541
+ client_id=self.client_id,
542
+ client_secret=self.client_secret,
543
+ )
544
+
545
+ mock_auth.assert_called_once_with(self.client_id, self.client_secret)
546
+ mock_report_manager.assert_called_once()
547
+ self.assertEqual(processor.plan_id, self.plan_id)
548
+ self.assertEqual(processor.wiz_project_id, self.wiz_project_id)
549
+ self.assertEqual(processor.title, "Wiz Compliance")
550
+
551
+ @patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager")
552
+ @patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate")
553
+ @patch("regscale.integrations.commercial.wizv2.compliance_report.error_and_exit")
554
+ def test_init_failed_authentication(self, mock_error_exit, mock_auth, mock_report_manager):
555
+ """Test initialization with failed authentication."""
556
+ mock_auth.return_value = None
557
+ mock_error_exit.side_effect = SystemExit(1)
558
+
559
+ with pytest.raises(SystemExit):
560
+ WizComplianceReportProcessor(
561
+ plan_id=self.plan_id,
562
+ wiz_project_id=self.wiz_project_id,
563
+ client_id=self.client_id,
564
+ client_secret=self.client_secret,
565
+ )
566
+
567
+ mock_error_exit.assert_called_once_with("Failed to authenticate with Wiz")
568
+
569
+ def test_parse_csv_report_regular_file(self):
570
+ """Test parse_csv_report with regular CSV file."""
571
+ # Create temporary CSV file
572
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as temp_file:
573
+ writer = csv.DictWriter(temp_file, fieldnames=self.sample_csv_data[0].keys())
574
+ writer.writeheader()
575
+ writer.writerows(self.sample_csv_data)
576
+ temp_file_path = temp_file.name
577
+
578
+ try:
579
+ # Create processor with mocked dependencies
580
+ with patch(
581
+ "regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate", return_value="token"
582
+ ):
583
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager"):
584
+ processor = WizComplianceReportProcessor(
585
+ plan_id=self.plan_id,
586
+ wiz_project_id=self.wiz_project_id,
587
+ client_id=self.client_id,
588
+ client_secret=self.client_secret,
589
+ )
590
+
591
+ items = processor.parse_csv_report(temp_file_path)
592
+
593
+ self.assertEqual(len(items), 2)
594
+ self.assertIsInstance(items[0], WizComplianceReportItem)
595
+ self.assertIsInstance(items[1], WizComplianceReportItem)
596
+ self.assertEqual(items[0]._resource_name, "vm-001")
597
+ self.assertEqual(items[1]._resource_name, "vm-002")
598
+
599
+ finally:
600
+ os.unlink(temp_file_path)
601
+
602
+ def test_parse_csv_report_gzipped_file(self):
603
+ """Test parse_csv_report with gzipped CSV file."""
604
+ # Create temporary gzipped CSV file
605
+ with tempfile.NamedTemporaryFile(suffix=".csv.gz", delete=False) as temp_file:
606
+ temp_file_path = temp_file.name
607
+
608
+ with gzip.open(temp_file_path, "wt", encoding="utf-8") as gz_file:
609
+ writer = csv.DictWriter(gz_file, fieldnames=self.sample_csv_data[0].keys())
610
+ writer.writeheader()
611
+ writer.writerows(self.sample_csv_data)
612
+
613
+ try:
614
+ # Create processor with mocked dependencies
615
+ with patch(
616
+ "regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate", return_value="token"
617
+ ):
618
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager"):
619
+ processor = WizComplianceReportProcessor(
620
+ plan_id=self.plan_id,
621
+ wiz_project_id=self.wiz_project_id,
622
+ client_id=self.client_id,
623
+ client_secret=self.client_secret,
624
+ )
625
+
626
+ items = processor.parse_csv_report(temp_file_path)
627
+
628
+ self.assertEqual(len(items), 2)
629
+ self.assertIsInstance(items[0], WizComplianceReportItem)
630
+
631
+ finally:
632
+ os.unlink(temp_file_path)
633
+
634
+ def test_parse_csv_report_file_not_found(self):
635
+ """Test parse_csv_report with non-existent file."""
636
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate", return_value="token"):
637
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager"):
638
+ processor = WizComplianceReportProcessor(
639
+ plan_id=self.plan_id,
640
+ wiz_project_id=self.wiz_project_id,
641
+ client_id=self.client_id,
642
+ client_secret=self.client_secret,
643
+ )
644
+
645
+ items = processor.parse_csv_report("non_existent_file.csv")
646
+ self.assertEqual(len(items), 0)
647
+
648
+ @patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager")
649
+ @patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate")
650
+ def test_fetch_compliance_data_with_existing_report(self, mock_auth, mock_report_manager):
651
+ """Test fetch_compliance_data with existing report file."""
652
+ mock_auth.return_value = "test-token"
653
+
654
+ # Create temporary CSV file
655
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as temp_file:
656
+ writer = csv.DictWriter(temp_file, fieldnames=self.sample_csv_data[0].keys())
657
+ writer.writeheader()
658
+ writer.writerows(self.sample_csv_data)
659
+ temp_file_path = temp_file.name
660
+
661
+ try:
662
+ processor = WizComplianceReportProcessor(
663
+ plan_id=self.plan_id,
664
+ wiz_project_id=self.wiz_project_id,
665
+ client_id=self.client_id,
666
+ client_secret=self.client_secret,
667
+ report_file_path=temp_file_path,
668
+ )
669
+
670
+ raw_data = processor.fetch_compliance_data()
671
+
672
+ self.assertEqual(len(raw_data), 2)
673
+ self.assertEqual(raw_data[0]["Resource Name"], "vm-001")
674
+ self.assertEqual(raw_data[1]["Resource Name"], "vm-002")
675
+
676
+ finally:
677
+ os.unlink(temp_file_path)
678
+
679
+ @patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager")
680
+ @patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate")
681
+ def test_fetch_compliance_data_report_creation(self, mock_auth, mock_report_manager):
682
+ """Test fetch_compliance_data creates new report when none exists."""
683
+ mock_auth.return_value = "test-token"
684
+
685
+ # Create temporary CSV file for the mock report
686
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as temp_file:
687
+ writer = csv.DictWriter(temp_file, fieldnames=self.sample_csv_data[0].keys())
688
+ writer.writeheader()
689
+ writer.writerows(self.sample_csv_data)
690
+ temp_file_path = temp_file.name
691
+
692
+ try:
693
+ processor = WizComplianceReportProcessor(
694
+ plan_id=self.plan_id,
695
+ wiz_project_id=self.wiz_project_id,
696
+ client_id=self.client_id,
697
+ client_secret=self.client_secret,
698
+ )
699
+
700
+ # Mock the report creation process
701
+ with patch.object(processor, "_get_or_create_report", return_value=temp_file_path):
702
+ raw_data = processor.fetch_compliance_data()
703
+
704
+ self.assertEqual(len(raw_data), 2)
705
+
706
+ finally:
707
+ os.unlink(temp_file_path)
708
+
709
+ def test_fetch_compliance_data_file_read_error(self):
710
+ """Test fetch_compliance_data handles file read errors."""
711
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate", return_value="token"):
712
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager"):
713
+ processor = WizComplianceReportProcessor(
714
+ plan_id=self.plan_id,
715
+ wiz_project_id=self.wiz_project_id,
716
+ client_id=self.client_id,
717
+ client_secret=self.client_secret,
718
+ )
719
+
720
+ # Mock file operations to raise an exception
721
+ with patch("builtins.open", side_effect=IOError("File read error")):
722
+ with patch.object(processor, "_get_or_create_report", return_value="test_file.csv"):
723
+ raw_data = processor.fetch_compliance_data()
724
+ self.assertEqual(len(raw_data), 0)
725
+
726
+ def test_create_compliance_item(self):
727
+ """Test create_compliance_item creates WizComplianceReportItem."""
728
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate", return_value="token"):
729
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager"):
730
+ processor = WizComplianceReportProcessor(
731
+ plan_id=self.plan_id,
732
+ wiz_project_id=self.wiz_project_id,
733
+ client_id=self.client_id,
734
+ client_secret=self.client_secret,
735
+ )
736
+
737
+ item = processor.create_compliance_item(self.sample_csv_data[0])
738
+ self.assertIsInstance(item, WizComplianceReportItem)
739
+ self.assertEqual(item._resource_name, "vm-001")
740
+
741
+ @patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager")
742
+ @patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate")
743
+ def test_process_compliance_data_bypass_filtering(self, mock_auth, mock_report_manager):
744
+ """Test process_compliance_data with bypass_control_filtering=True."""
745
+ mock_auth.return_value = "test-token"
746
+
747
+ processor = WizComplianceReportProcessor(
748
+ plan_id=self.plan_id,
749
+ wiz_project_id=self.wiz_project_id,
750
+ client_id=self.client_id,
751
+ client_secret=self.client_secret,
752
+ bypass_control_filtering=True,
753
+ )
754
+
755
+ with patch.object(processor, "_process_compliance_data_without_filtering") as mock_without_filtering:
756
+ processor.process_compliance_data()
757
+ mock_without_filtering.assert_called_once()
758
+
759
+ @patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager")
760
+ @patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate")
761
+ def test_process_compliance_data_normal_filtering(self, mock_auth, mock_report_manager):
762
+ """Test process_compliance_data with normal filtering."""
763
+ mock_auth.return_value = "test-token"
764
+
765
+ processor = WizComplianceReportProcessor(
766
+ plan_id=self.plan_id,
767
+ wiz_project_id=self.wiz_project_id,
768
+ client_id=self.client_id,
769
+ client_secret=self.client_secret,
770
+ bypass_control_filtering=False,
771
+ )
772
+
773
+ # Mock the parent's process_compliance_data method
774
+ with patch.object(ComplianceIntegration, "process_compliance_data") as mock_parent:
775
+ processor.process_compliance_data()
776
+ mock_parent.assert_called_once()
777
+
778
+ def test_process_compliance_data_without_filtering_categorization(self):
779
+ """Test _process_compliance_data_without_filtering categorizes controls correctly."""
780
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate", return_value="token"):
781
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager"):
782
+ processor = WizComplianceReportProcessor(
783
+ plan_id=self.plan_id,
784
+ wiz_project_id=self.wiz_project_id,
785
+ client_id=self.client_id,
786
+ client_secret=self.client_secret,
787
+ )
788
+
789
+ # Mock fetch_compliance_data to return our test data
790
+ with patch.object(processor, "fetch_compliance_data", return_value=self.sample_csv_data):
791
+ with patch.object(processor, "_categorize_controls_fail_first") as mock_categorize:
792
+ processor._process_compliance_data_without_filtering()
793
+
794
+ # Should have processed 2 items
795
+ self.assertEqual(len(processor.all_compliance_items), 2)
796
+ # Should have 1 failed item (Result="Failed")
797
+ self.assertEqual(len(processor.failed_compliance_items), 1)
798
+ # Should call fail-first categorization
799
+ mock_categorize.assert_called_once()
800
+
801
+ def test_process_compliance_sync(self):
802
+ """Test process_compliance_sync calls sync_compliance."""
803
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate", return_value="token"):
804
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager"):
805
+ processor = WizComplianceReportProcessor(
806
+ plan_id=self.plan_id,
807
+ wiz_project_id=self.wiz_project_id,
808
+ client_id=self.client_id,
809
+ client_secret=self.client_secret,
810
+ )
811
+
812
+ with patch.object(processor, "sync_compliance") as mock_sync:
813
+ processor.process_compliance_sync()
814
+ mock_sync.assert_called_once()
815
+
816
+ def test_get_or_create_report_existing_recent_report(self):
817
+ """Test _get_or_create_report uses existing recent report."""
818
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate", return_value="token"):
819
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager"):
820
+ processor = WizComplianceReportProcessor(
821
+ plan_id=self.plan_id,
822
+ wiz_project_id=self.wiz_project_id,
823
+ client_id=self.client_id,
824
+ client_secret=self.client_secret,
825
+ )
826
+
827
+ with patch.object(processor, "_find_recent_report", return_value="existing_report.csv"):
828
+ result = processor._get_or_create_report()
829
+ self.assertEqual(result, "existing_report.csv")
830
+
831
+ def test_get_or_create_report_creates_new_report(self):
832
+ """Test _get_or_create_report creates new report when none exists."""
833
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate", return_value="token"):
834
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager"):
835
+ processor = WizComplianceReportProcessor(
836
+ plan_id=self.plan_id,
837
+ wiz_project_id=self.wiz_project_id,
838
+ client_id=self.client_id,
839
+ client_secret=self.client_secret,
840
+ )
841
+
842
+ with patch.object(processor, "_find_recent_report", return_value=None):
843
+ with patch.object(processor, "_create_and_download_report", return_value="new_report.csv"):
844
+ result = processor._get_or_create_report()
845
+ self.assertEqual(result, "new_report.csv")
846
+
847
+ def test_find_recent_report_no_artifacts_dir(self):
848
+ """Test _find_recent_report returns None when artifacts directory doesn't exist."""
849
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate", return_value="token"):
850
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager"):
851
+ processor = WizComplianceReportProcessor(
852
+ plan_id=self.plan_id,
853
+ wiz_project_id=self.wiz_project_id,
854
+ client_id=self.client_id,
855
+ client_secret=self.client_secret,
856
+ )
857
+
858
+ with patch("os.path.exists", return_value=False):
859
+ result = processor._find_recent_report()
860
+ self.assertIsNone(result)
861
+
862
+ def test_find_recent_report_finds_recent_file(self):
863
+ """Test _find_recent_report finds and returns recent file."""
864
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate", return_value="token"):
865
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager"):
866
+ processor = WizComplianceReportProcessor(
867
+ plan_id=self.plan_id,
868
+ wiz_project_id=self.wiz_project_id,
869
+ client_id=self.client_id,
870
+ client_secret=self.client_secret,
871
+ )
872
+
873
+ recent_time = datetime.now().timestamp()
874
+ expected_filename = f"compliance_report_{self.wiz_project_id}_test.csv"
875
+
876
+ with patch("os.path.exists", return_value=True):
877
+ with patch("os.listdir", return_value=[expected_filename]):
878
+ with patch("os.path.getmtime", return_value=recent_time):
879
+ result = processor._find_recent_report()
880
+ expected_path = f"artifacts/wiz/{expected_filename}"
881
+ self.assertEqual(result, expected_path)
882
+
883
+ def test_find_recent_report_ignores_old_files(self):
884
+ """Test _find_recent_report ignores files older than max_age_hours."""
885
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate", return_value="token"):
886
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager"):
887
+ processor = WizComplianceReportProcessor(
888
+ plan_id=self.plan_id,
889
+ wiz_project_id=self.wiz_project_id,
890
+ client_id=self.client_id,
891
+ client_secret=self.client_secret,
892
+ )
893
+
894
+ # File older than 24 hours
895
+ old_time = (datetime.now() - timedelta(hours=25)).timestamp()
896
+ expected_filename = f"compliance_report_{self.wiz_project_id}_test.csv"
897
+
898
+ with patch("os.path.exists", return_value=True):
899
+ with patch("os.listdir", return_value=[expected_filename]):
900
+ with patch("os.path.getmtime", return_value=old_time):
901
+ result = processor._find_recent_report(max_age_hours=24)
902
+ self.assertIsNone(result)
903
+
904
+ @patch("regscale.integrations.commercial.wizv2.compliance_report.ReportFileCleanup")
905
+ @patch("os.makedirs")
906
+ def test_create_and_download_report_success(self, mock_makedirs, mock_cleanup):
907
+ """Test _create_and_download_report successful report creation."""
908
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate", return_value="token"):
909
+ mock_report_manager = MagicMock()
910
+ with patch(
911
+ "regscale.integrations.commercial.wizv2.compliance_report.WizReportManager",
912
+ return_value=mock_report_manager,
913
+ ):
914
+ processor = WizComplianceReportProcessor(
915
+ plan_id=self.plan_id,
916
+ wiz_project_id=self.wiz_project_id,
917
+ client_id=self.client_id,
918
+ client_secret=self.client_secret,
919
+ )
920
+
921
+ # Mock successful report creation flow
922
+ mock_report_manager.create_compliance_report.return_value = "report-123"
923
+ mock_report_manager.wait_for_report_completion.return_value = "http://download.url" # NOSONAR
924
+ mock_report_manager.download_report.return_value = True
925
+
926
+ result = processor._create_and_download_report()
927
+
928
+ self.assertIsNotNone(result)
929
+ self.assertTrue(result.startswith("artifacts/wiz/compliance_report_"))
930
+ self.assertTrue(result.endswith(".csv"))
931
+ mock_makedirs.assert_called_once()
932
+ mock_cleanup.cleanup_old_files.assert_called_once()
933
+
934
+ def test_create_and_download_report_creation_failure(self):
935
+ """Test _create_and_download_report handles report creation failure."""
936
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate", return_value="token"):
937
+ mock_report_manager = MagicMock()
938
+ with patch(
939
+ "regscale.integrations.commercial.wizv2.compliance_report.WizReportManager",
940
+ return_value=mock_report_manager,
941
+ ):
942
+ processor = WizComplianceReportProcessor(
943
+ plan_id=self.plan_id,
944
+ wiz_project_id=self.wiz_project_id,
945
+ client_id=self.client_id,
946
+ client_secret=self.client_secret,
947
+ )
948
+
949
+ # Mock failed report creation
950
+ mock_report_manager.create_compliance_report.return_value = None
951
+
952
+ result = processor._create_and_download_report()
953
+ self.assertIsNone(result)
954
+
955
+ def test_create_and_download_report_download_failure(self):
956
+ """Test _create_and_download_report handles download failure."""
957
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate", return_value="token"):
958
+ mock_report_manager = MagicMock()
959
+ with patch(
960
+ "regscale.integrations.commercial.wizv2.compliance_report.WizReportManager",
961
+ return_value=mock_report_manager,
962
+ ):
963
+ processor = WizComplianceReportProcessor(
964
+ plan_id=self.plan_id,
965
+ wiz_project_id=self.wiz_project_id,
966
+ client_id=self.client_id,
967
+ client_secret=self.client_secret,
968
+ )
969
+
970
+ # Mock successful creation but failed download
971
+ mock_report_manager.create_compliance_report.return_value = "report-123"
972
+ mock_report_manager.wait_for_report_completion.return_value = "http://download.url" # NOSONAR
973
+ mock_report_manager.download_report.return_value = False
974
+
975
+ with patch("os.makedirs"):
976
+ result = processor._create_and_download_report()
977
+ self.assertIsNone(result)
978
+
979
+
980
+ @pytest.mark.no_parallel
981
+ class TestWizComplianceStatusMatching(unittest.TestCase):
982
+ """Test suite for status matching and case-insensitive logic."""
983
+
984
+ def setUp(self):
985
+ """Set up test fixtures."""
986
+ self.sample_csv_row = {
987
+ "Resource Name": "test-vm",
988
+ "Cloud Provider": "Azure",
989
+ "Cloud Provider ID": "vm-id",
990
+ "Resource ID": "vm-id",
991
+ "Resource Region": "East US",
992
+ "Subscription": "sub-1",
993
+ "Subscription Name": "Test Sub",
994
+ "Policy Name": "Test Policy",
995
+ "Policy ID": "policy-001",
996
+ "Result": "Pass",
997
+ "Severity": "HIGH",
998
+ "Compliance Check Name (Wiz Subcategory)": "AC-2(1) Account Management",
999
+ "Framework": "NIST SP 800-53 Revision 5",
1000
+ "Remediation Steps": "Fix this",
1001
+ }
1002
+
1003
+ def test_pass_status_matching_case_insensitive(self):
1004
+ """Test that pass statuses are matched case-insensitively."""
1005
+ # Current implementation supports: Pass, PASS, pass but not Passed, PASSED, passed
1006
+ pass_values = ["Pass", "PASS", "pass"]
1007
+ fail_values = ["Passed", "PASSED", "passed"] # These don't work with current implementation
1008
+
1009
+ # Test values that currently work
1010
+ for result_value in pass_values:
1011
+ with self.subTest(result_value=result_value):
1012
+ csv_row = {**self.sample_csv_row, "Result": result_value}
1013
+ item = WizComplianceReportItem(csv_row)
1014
+
1015
+ self.assertEqual(item.get_status(), "Satisfied")
1016
+ self.assertEqual(item.get_implementation_status(), "Implemented")
1017
+
1018
+ # Test values that currently don't work (ones ending with 'ed')
1019
+ for result_value in fail_values:
1020
+ with self.subTest(result_value=result_value):
1021
+ csv_row = {**self.sample_csv_row, "Result": result_value}
1022
+ item = WizComplianceReportItem(csv_row)
1023
+
1024
+ # These return "Other Than Satisfied" because they're not in PASS_STATUSES
1025
+ self.assertEqual(item.get_status(), "Other Than Satisfied")
1026
+ self.assertEqual(item.get_implementation_status(), "In Remediation")
1027
+
1028
+ def test_fail_status_matching_case_insensitive(self):
1029
+ """Test that fail statuses are matched case-insensitively."""
1030
+ fail_values = ["Fail", "FAIL", "fail", "Failed", "FAILED", "failed"]
1031
+
1032
+ for result_value in fail_values:
1033
+ with self.subTest(result_value=result_value):
1034
+ csv_row = {**self.sample_csv_row, "Result": result_value}
1035
+ item = WizComplianceReportItem(csv_row)
1036
+
1037
+ self.assertEqual(item.get_status(), "Other Than Satisfied")
1038
+ self.assertEqual(item.get_implementation_status(), "In Remediation")
1039
+
1040
+ def test_status_categorization_in_processor(self):
1041
+ """Test that WizComplianceReportProcessor correctly categorizes statuses."""
1042
+ # Test data with mixed case statuses
1043
+ test_data = [
1044
+ {**self.sample_csv_row, "Result": "Pass", "Compliance Check Name (Wiz Subcategory)": "AC-2(1)"},
1045
+ {**self.sample_csv_row, "Result": "PASS", "Compliance Check Name (Wiz Subcategory)": "AC-3"},
1046
+ {**self.sample_csv_row, "Result": "pass", "Compliance Check Name (Wiz Subcategory)": "AC-4"},
1047
+ {**self.sample_csv_row, "Result": "Failed", "Compliance Check Name (Wiz Subcategory)": "SI-2"},
1048
+ {**self.sample_csv_row, "Result": "FAILED", "Compliance Check Name (Wiz Subcategory)": "SI-3"},
1049
+ {**self.sample_csv_row, "Result": "fail", "Compliance Check Name (Wiz Subcategory)": "SI-4"},
1050
+ ]
1051
+
1052
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate", return_value="token"):
1053
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager"):
1054
+ processor = WizComplianceReportProcessor(
1055
+ plan_id=123,
1056
+ wiz_project_id="test-project",
1057
+ client_id="client",
1058
+ client_secret="secret",
1059
+ bypass_control_filtering=False, # Use threshold-based logic
1060
+ )
1061
+
1062
+ # Mock fetch_compliance_data to return test data
1063
+ with patch.object(processor, "fetch_compliance_data", return_value=test_data):
1064
+ processor._process_compliance_data_without_filtering()
1065
+
1066
+ # Should correctly categorize 6 items total: 3 passing, 3 failing
1067
+ self.assertEqual(len(processor.all_compliance_items), 6)
1068
+ self.assertEqual(len(processor.failed_compliance_items), 3)
1069
+
1070
+ # Verify that the failed items have the correct results (case-insensitive matching)
1071
+ failed_results = [item.compliance_result for item in processor.failed_compliance_items]
1072
+ expected_failed_results = ["Failed", "FAILED", "fail"]
1073
+ self.assertEqual(sorted(failed_results), sorted(expected_failed_results))
1074
+
1075
+
1076
+ @pytest.mark.no_parallel
1077
+ class TestWizComplianceControlCategorization(unittest.TestCase):
1078
+ """Test suite for control categorization and aggregation logic."""
1079
+
1080
+ def test_control_categorization_all_pass(self):
1081
+ """Test control categorization when all items pass."""
1082
+ test_data = [
1083
+ {
1084
+ "Result": "Pass",
1085
+ "Compliance Check Name (Wiz Subcategory)": "AC-2(1)",
1086
+ "Resource Name": "vm1",
1087
+ "Cloud Provider ID": "id1",
1088
+ },
1089
+ {
1090
+ "Result": "PASS",
1091
+ "Compliance Check Name (Wiz Subcategory)": "AC-2(1)",
1092
+ "Resource Name": "vm2",
1093
+ "Cloud Provider ID": "id2",
1094
+ },
1095
+ {
1096
+ "Result": "pass",
1097
+ "Compliance Check Name (Wiz Subcategory)": "AC-2(1)",
1098
+ "Resource Name": "vm3",
1099
+ "Cloud Provider ID": "id3",
1100
+ },
1101
+ ]
1102
+
1103
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate", return_value="token"):
1104
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager"):
1105
+ processor = WizComplianceReportProcessor(
1106
+ plan_id=123,
1107
+ wiz_project_id="test-project",
1108
+ client_id="client",
1109
+ client_secret="secret",
1110
+ bypass_control_filtering=False, # Use threshold-based logic
1111
+ )
1112
+
1113
+ # Fill in required fields for test data
1114
+ full_test_data = []
1115
+ for item in test_data:
1116
+ full_item = {
1117
+ "Resource Name": item["Resource Name"],
1118
+ "Cloud Provider": "Azure",
1119
+ "Cloud Provider ID": item["Cloud Provider ID"],
1120
+ "Resource ID": item["Cloud Provider ID"],
1121
+ "Resource Region": "East US",
1122
+ "Subscription": "sub-1",
1123
+ "Subscription Name": "Test Sub",
1124
+ "Policy Name": "Test Policy",
1125
+ "Policy ID": "policy-001",
1126
+ "Result": item["Result"],
1127
+ "Severity": "HIGH",
1128
+ "Compliance Check Name (Wiz Subcategory)": item["Compliance Check Name (Wiz Subcategory)"],
1129
+ "Framework": "NIST SP 800-53 Revision 5",
1130
+ "Remediation Steps": "Fix this",
1131
+ }
1132
+ full_test_data.append(full_item)
1133
+
1134
+ with patch.object(processor, "fetch_compliance_data", return_value=full_test_data):
1135
+ processor.process_compliance_data() # This will use threshold-based logic since bypass_control_filtering=False
1136
+
1137
+ # Should have AC-2(1) as passing control since all items pass
1138
+ self.assertEqual(len(processor.passing_controls), 1)
1139
+ self.assertEqual(len(processor.failing_controls), 0)
1140
+ self.assertIn("ac-2(1)", processor.passing_controls)
1141
+
1142
+ def test_control_categorization_all_fail(self):
1143
+ """Test control categorization when all items fail."""
1144
+ test_data = [
1145
+ {
1146
+ "Result": "Failed",
1147
+ "Compliance Check Name (Wiz Subcategory)": "AC-3",
1148
+ "Resource Name": "vm1",
1149
+ "Cloud Provider ID": "id1",
1150
+ },
1151
+ {
1152
+ "Result": "FAILED",
1153
+ "Compliance Check Name (Wiz Subcategory)": "AC-3",
1154
+ "Resource Name": "vm2",
1155
+ "Cloud Provider ID": "id2",
1156
+ },
1157
+ {
1158
+ "Result": "fail",
1159
+ "Compliance Check Name (Wiz Subcategory)": "AC-3",
1160
+ "Resource Name": "vm3",
1161
+ "Cloud Provider ID": "id3",
1162
+ },
1163
+ ]
1164
+
1165
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate", return_value="token"):
1166
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager"):
1167
+ processor = WizComplianceReportProcessor(
1168
+ plan_id=123,
1169
+ wiz_project_id="test-project",
1170
+ client_id="client",
1171
+ client_secret="secret",
1172
+ bypass_control_filtering=False, # Use threshold-based logic
1173
+ )
1174
+
1175
+ # Fill in required fields for test data
1176
+ full_test_data = []
1177
+ for item in test_data:
1178
+ full_item = {
1179
+ "Resource Name": item["Resource Name"],
1180
+ "Cloud Provider": "Azure",
1181
+ "Cloud Provider ID": item["Cloud Provider ID"],
1182
+ "Resource ID": item["Cloud Provider ID"],
1183
+ "Resource Region": "East US",
1184
+ "Subscription": "sub-1",
1185
+ "Subscription Name": "Test Sub",
1186
+ "Policy Name": "Test Policy",
1187
+ "Policy ID": "policy-001",
1188
+ "Result": item["Result"],
1189
+ "Severity": "HIGH",
1190
+ "Compliance Check Name (Wiz Subcategory)": item["Compliance Check Name (Wiz Subcategory)"],
1191
+ "Framework": "NIST SP 800-53 Revision 5",
1192
+ "Remediation Steps": "Fix this",
1193
+ }
1194
+ full_test_data.append(full_item)
1195
+
1196
+ with patch.object(processor, "fetch_compliance_data", return_value=full_test_data):
1197
+ processor.process_compliance_data() # This will use threshold-based logic since bypass_control_filtering=False
1198
+
1199
+ # Should have AC-3 as failing control since all items fail
1200
+ self.assertEqual(len(processor.passing_controls), 0)
1201
+ self.assertEqual(len(processor.failing_controls), 1)
1202
+ self.assertIn("ac-3", processor.failing_controls)
1203
+
1204
+ def test_control_categorization_mixed_results_high_failure_rate(self):
1205
+ """Test control categorization with mixed results above failure threshold."""
1206
+ # 5 items: 2 pass, 3 fail = 60% failure rate (above default 20% threshold)
1207
+ test_data = [
1208
+ {"Result": "Pass", "Resource Name": "vm1", "Cloud Provider ID": "id1"},
1209
+ {"Result": "Pass", "Resource Name": "vm2", "Cloud Provider ID": "id2"},
1210
+ {"Result": "Failed", "Resource Name": "vm3", "Cloud Provider ID": "id3"},
1211
+ {"Result": "Failed", "Resource Name": "vm4", "Cloud Provider ID": "id4"},
1212
+ {"Result": "Failed", "Resource Name": "vm5", "Cloud Provider ID": "id5"},
1213
+ ]
1214
+
1215
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate", return_value="token"):
1216
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager"):
1217
+ processor = WizComplianceReportProcessor(
1218
+ plan_id=123,
1219
+ wiz_project_id="test-project",
1220
+ client_id="client",
1221
+ client_secret="secret",
1222
+ bypass_control_filtering=False, # Use threshold-based logic
1223
+ )
1224
+
1225
+ # Fill in required fields - all same control
1226
+ full_test_data = []
1227
+ for item in test_data:
1228
+ full_item = {
1229
+ "Resource Name": item["Resource Name"],
1230
+ "Cloud Provider": "Azure",
1231
+ "Cloud Provider ID": item["Cloud Provider ID"],
1232
+ "Resource ID": item["Cloud Provider ID"],
1233
+ "Resource Region": "East US",
1234
+ "Subscription": "sub-1",
1235
+ "Subscription Name": "Test Sub",
1236
+ "Policy Name": "Test Policy",
1237
+ "Policy ID": "policy-001",
1238
+ "Result": item["Result"],
1239
+ "Severity": "HIGH",
1240
+ "Compliance Check Name (Wiz Subcategory)": "AC-4 Information Flow Enforcement",
1241
+ "Framework": "NIST SP 800-53 Revision 5",
1242
+ "Remediation Steps": "Fix this",
1243
+ }
1244
+ full_test_data.append(full_item)
1245
+
1246
+ with patch.object(processor, "fetch_compliance_data", return_value=full_test_data):
1247
+ processor.process_compliance_data() # This will use threshold-based logic since bypass_control_filtering=False
1248
+
1249
+ # Should categorize as failing due to high failure rate (60% > 20% threshold)
1250
+ self.assertEqual(len(processor.passing_controls), 0)
1251
+ self.assertEqual(len(processor.failing_controls), 1)
1252
+ self.assertIn("ac-4", processor.failing_controls)
1253
+
1254
+ def test_control_categorization_mixed_results_fail_first(self):
1255
+ """Test control categorization with mixed results using fail-first logic."""
1256
+ # 10 items: 9 pass, 1 fail - with fail-first logic, any failure makes the control fail
1257
+ test_data = []
1258
+ for i in range(9):
1259
+ test_data.append({"Result": "Pass", "Resource Name": f"vm{i + 1}", "Cloud Provider ID": f"id{i + 1}"})
1260
+ test_data.append({"Result": "Failed", "Resource Name": "vm10", "Cloud Provider ID": "id10"})
1261
+
1262
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate", return_value="token"):
1263
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager"):
1264
+ processor = WizComplianceReportProcessor(
1265
+ plan_id=123,
1266
+ wiz_project_id="test-project",
1267
+ client_id="client",
1268
+ client_secret="secret",
1269
+ bypass_control_filtering=False,
1270
+ )
1271
+
1272
+ # Fill in required fields - all same control
1273
+ full_test_data = []
1274
+ for item in test_data:
1275
+ full_item = {
1276
+ "Resource Name": item["Resource Name"],
1277
+ "Cloud Provider": "Azure",
1278
+ "Cloud Provider ID": item["Cloud Provider ID"],
1279
+ "Resource ID": item["Cloud Provider ID"],
1280
+ "Resource Region": "East US",
1281
+ "Subscription": "sub-1",
1282
+ "Subscription Name": "Test Sub",
1283
+ "Policy Name": "Test Policy",
1284
+ "Policy ID": "policy-001",
1285
+ "Result": item["Result"],
1286
+ "Severity": "HIGH",
1287
+ "Compliance Check Name (Wiz Subcategory)": "SI-2 Flaw Remediation",
1288
+ "Framework": "NIST SP 800-53 Revision 5",
1289
+ "Remediation Steps": "Fix this",
1290
+ }
1291
+ full_test_data.append(full_item)
1292
+
1293
+ with patch.object(processor, "fetch_compliance_data", return_value=full_test_data):
1294
+ processor.process_compliance_data()
1295
+
1296
+ # With fail-first logic, any failure makes the control fail (1 failure out of 10)
1297
+ self.assertEqual(len(processor.passing_controls), 0)
1298
+ self.assertEqual(len(processor.failing_controls), 1)
1299
+ self.assertIn("si-2", processor.failing_controls)
1300
+
1301
+ def test_control_categorization_fail_first_logic(self):
1302
+ """Test that Wiz compliance uses fail-first logic regardless of bypass_control_filtering setting."""
1303
+ # 10 items: 7 pass, 3 fail - with fail-first logic, this should be a failing control
1304
+ test_data = []
1305
+ for i in range(7):
1306
+ test_data.append({"Result": "Pass", "Resource Name": f"vm{i + 1}", "Cloud Provider ID": f"id{i + 1}"})
1307
+ for i in range(3):
1308
+ test_data.append({"Result": "Failed", "Resource Name": f"vm{i + 8}", "Cloud Provider ID": f"id{i + 8}"})
1309
+
1310
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.wiz_authenticate", return_value="token"):
1311
+ with patch("regscale.integrations.commercial.wizv2.compliance_report.WizReportManager"):
1312
+ processor = WizComplianceReportProcessor(
1313
+ plan_id=123,
1314
+ wiz_project_id="test-project",
1315
+ client_id="client",
1316
+ client_secret="secret",
1317
+ bypass_control_filtering=False,
1318
+ )
1319
+
1320
+ # Fill in required fields - all same control
1321
+ full_test_data = []
1322
+ for item in test_data:
1323
+ full_item = {
1324
+ "Resource Name": item["Resource Name"],
1325
+ "Cloud Provider": "Azure",
1326
+ "Cloud Provider ID": item["Cloud Provider ID"],
1327
+ "Resource ID": item["Cloud Provider ID"],
1328
+ "Resource Region": "East US",
1329
+ "Subscription": "sub-1",
1330
+ "Subscription Name": "Test Sub",
1331
+ "Policy Name": "Test Policy",
1332
+ "Policy ID": "policy-001",
1333
+ "Result": item["Result"],
1334
+ "Severity": "HIGH",
1335
+ "Compliance Check Name (Wiz Subcategory)": "CM-3 Configuration Change Control",
1336
+ "Framework": "NIST SP 800-53 Revision 5",
1337
+ "Remediation Steps": "Fix this",
1338
+ }
1339
+ full_test_data.append(full_item)
1340
+
1341
+ with patch.object(processor, "fetch_compliance_data", return_value=full_test_data):
1342
+ processor.process_compliance_data()
1343
+
1344
+ # With fail-first logic, any failures make the control fail
1345
+ self.assertEqual(len(processor.passing_controls), 0)
1346
+ self.assertEqual(len(processor.failing_controls), 1)
1347
+ self.assertIn("cm-3", processor.failing_controls)
1348
+
1349
+
1350
+ if __name__ == "__main__":
1351
+ unittest.main()