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,750 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Unit tests for Wiz Policy Compliance Integration.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ from unittest.mock import Mock, patch, mock_open
10
+
11
+ import pytest
12
+
13
+ from regscale.integrations.commercial.wizv2.policy_compliance import (
14
+ WizComplianceItem,
15
+ WizPolicyComplianceIntegration,
16
+ )
17
+
18
+
19
+ class TestWizComplianceItem:
20
+ """Test the WizComplianceItem class."""
21
+
22
+ def setup_method(self):
23
+ """Set up test data."""
24
+ self.mock_raw_data = {
25
+ "id": "assessment-123",
26
+ "result": "FAIL",
27
+ "policy": {
28
+ "id": "policy-456",
29
+ "name": "Test Policy",
30
+ "description": "Test policy description",
31
+ "severity": "HIGH",
32
+ "remediationInstructions": "Fix the issue",
33
+ "securitySubCategories": [
34
+ {
35
+ "externalId": "AC-3",
36
+ "category": {"framework": {"id": "wf-id-4", "name": "NIST SP 800-53 Revision 5"}},
37
+ }
38
+ ],
39
+ },
40
+ "resource": {
41
+ "id": "resource-789",
42
+ "name": "Test Resource",
43
+ "type": "VIRTUAL_MACHINE",
44
+ "region": "us-east-1",
45
+ "subscription": {"cloudProvider": "Azure", "name": "Test Subscription", "externalId": "sub-123"},
46
+ "tags": [{"key": "Environment", "value": "Production"}, {"key": "Owner", "value": "TestTeam"}],
47
+ },
48
+ "output": {"someField": "someValue"},
49
+ }
50
+
51
+ self.mock_integration = Mock()
52
+ self.mock_integration.framework_id = "wf-id-4" # Add framework_id for filtering
53
+ self.mock_integration.get_framework_name.return_value = "NIST SP 800-53 Revision 5"
54
+
55
+ def test_wiz_compliance_item_creation(self):
56
+ """Test creating a WizComplianceItem from raw data."""
57
+ item = WizComplianceItem(self.mock_raw_data, self.mock_integration)
58
+
59
+ assert item.id == "assessment-123"
60
+ assert item.result == "FAIL"
61
+ assert item.resource_id == "resource-789"
62
+ assert item.resource_name == "Test Resource"
63
+ assert item.control_id == "AC-3"
64
+ assert item.compliance_result == "FAIL"
65
+ assert item.severity == "HIGH"
66
+ assert item.framework_id == "wf-id-4"
67
+ assert item.framework == "NIST SP 800-53 Revision 5"
68
+ assert item.is_fail
69
+ assert not item.is_pass
70
+
71
+ def test_wiz_compliance_item_pass_result(self):
72
+ """Test WizComplianceItem with PASS result."""
73
+ self.mock_raw_data["result"] = "PASS"
74
+ item = WizComplianceItem(self.mock_raw_data, self.mock_integration)
75
+
76
+ assert item.compliance_result == "PASS"
77
+ assert item.is_pass
78
+ assert not item.is_fail
79
+
80
+ def test_wiz_compliance_item_missing_control_id(self):
81
+ """Test WizComplianceItem with missing control ID."""
82
+ # Remove security subcategories
83
+ self.mock_raw_data["policy"]["securitySubCategories"] = []
84
+ item = WizComplianceItem(self.mock_raw_data, self.mock_integration)
85
+
86
+ assert item.control_id == ""
87
+ assert item.framework == ""
88
+ assert item.framework_id is None
89
+
90
+ def test_wiz_compliance_item_missing_policy(self):
91
+ """Test WizComplianceItem with missing policy data."""
92
+ self.mock_raw_data["policy"] = {}
93
+ item = WizComplianceItem(self.mock_raw_data, self.mock_integration)
94
+
95
+ assert item.control_id == ""
96
+ assert item.severity is None
97
+ assert "unknown policy" in item.description.lower()
98
+ assert item.framework == ""
99
+
100
+ def test_wiz_compliance_item_description_fallback(self):
101
+ """Test description fallback when policy description is missing."""
102
+ # Remove description but keep name
103
+ del self.mock_raw_data["policy"]["description"]
104
+ item = WizComplianceItem(self.mock_raw_data, self.mock_integration)
105
+
106
+ assert "Test Policy" in item.description
107
+
108
+ # Test with ruleDescription
109
+ self.mock_raw_data["policy"]["ruleDescription"] = "Rule description"
110
+ item = WizComplianceItem(self.mock_raw_data, self.mock_integration)
111
+ assert item.description == "Rule description"
112
+
113
+ def test_wiz_compliance_item_framework_caching(self):
114
+ """Test that framework mapping uses integration cache."""
115
+ item = WizComplianceItem(self.mock_raw_data, self.mock_integration)
116
+
117
+ # Access framework property
118
+ framework = item.framework
119
+
120
+ # Verify integration's get_framework_name was called
121
+ self.mock_integration.get_framework_name.assert_called_with("wf-id-4")
122
+ assert framework == "NIST SP 800-53 Revision 5"
123
+
124
+ def test_wiz_compliance_item_no_integration(self):
125
+ """Test WizComplianceItem without integration instance."""
126
+ item = WizComplianceItem(self.mock_raw_data, None)
127
+
128
+ # Should fallback to framework name from raw data
129
+ assert item.framework == "NIST SP 800-53 Revision 5"
130
+
131
+
132
+ class TestWizPolicyComplianceIntegration:
133
+ """Test the WizPolicyComplianceIntegration class."""
134
+
135
+ def setup_method(self):
136
+ """Set up test fixtures."""
137
+ self.integration = WizPolicyComplianceIntegration(
138
+ plan_id=123,
139
+ wiz_project_id="test-project-123",
140
+ client_id="test-client-id",
141
+ client_secret="test-client-secret",
142
+ framework_id="wf-id-4",
143
+ catalog_id=456,
144
+ )
145
+
146
+ # Mock authentication
147
+ self.integration.access_token = "mock-token"
148
+ self.integration.wiz_endpoint = "https://api.wiz.io/graphql"
149
+
150
+ def test_initialization(self):
151
+ """Test integration initialization."""
152
+ assert self.integration.plan_id == 123
153
+ assert self.integration.wiz_project_id == "test-project-123"
154
+ assert self.integration.client_id == "test-client-id"
155
+ assert self.integration.client_secret == "test-client-secret"
156
+ assert self.integration.framework_id == "wf-id-4"
157
+ assert self.integration.catalog_id == 456
158
+ assert self.integration.title == "Wiz Policy Compliance Integration"
159
+ assert self.integration.framework == "NIST800-53R5"
160
+
161
+ def test_framework_id_mapping(self):
162
+ """Test framework ID to name mapping."""
163
+ assert self.integration._map_framework_id_to_name("wf-id-4") == "NIST800-53R5"
164
+ assert self.integration._map_framework_id_to_name("wf-id-48") == "NIST800-53R4"
165
+ assert self.integration._map_framework_id_to_name("wf-id-5") == "FedRAMP"
166
+ assert self.integration._map_framework_id_to_name("unknown-id") == "unknown-id"
167
+
168
+ def test_resource_type_mapping(self):
169
+ """Test resource type to asset type mapping."""
170
+ # Create mock compliance items with different resource types
171
+ test_cases = [
172
+ ("VIRTUAL_MACHINE", "Virtual Machine"),
173
+ ("CONTAINER", "Container"),
174
+ ("DATABASE", "Database"),
175
+ ("BUCKET", "Storage"),
176
+ ("UNKNOWN_TYPE", "Cloud Resource"),
177
+ ]
178
+
179
+ for resource_type, expected_asset_type in test_cases:
180
+ mock_data = {
181
+ "id": "test-id",
182
+ "result": "PASS",
183
+ "policy": {},
184
+ "resource": {"type": resource_type},
185
+ "output": {},
186
+ }
187
+
188
+ compliance_item = WizComplianceItem(mock_data)
189
+ asset_type = self.integration._map_resource_type_to_asset_type(compliance_item)
190
+ assert asset_type == expected_asset_type
191
+
192
+ @patch("regscale.integrations.commercial.wizv2.policy_compliance.wiz_authenticate")
193
+ @patch("regscale.integrations.commercial.wizv2.policy_compliance.check_license")
194
+ def test_authenticate_wiz(self, mock_check_license, mock_wiz_authenticate):
195
+ """Test Wiz authentication."""
196
+ # Set up mocks
197
+ mock_app = Mock()
198
+ mock_app.config = {"wizUrl": "https://api.wiz.io/graphql"}
199
+ mock_check_license.return_value = mock_app
200
+ mock_wiz_authenticate.return_value = "test-token"
201
+
202
+ # Clear existing token
203
+ self.integration.access_token = ""
204
+
205
+ token = self.integration.authenticate_wiz()
206
+
207
+ assert token == "test-token"
208
+ assert self.integration.access_token == "test-token"
209
+ assert self.integration.wiz_endpoint == "https://api.wiz.io/graphql"
210
+
211
+ mock_wiz_authenticate.assert_called_once_with(client_id="test-client-id", client_secret="test-client-secret")
212
+
213
+ @patch("regscale.integrations.commercial.wizv2.policy_compliance.run_async_queries")
214
+ def test_fetch_policy_assessments_from_wiz(self, mock_run_async_queries):
215
+ """Test fetching policy assessments from Wiz API."""
216
+ # Mock API response
217
+ mock_nodes = [
218
+ {"id": "assessment-1", "result": "PASS"},
219
+ {"id": "assessment-2", "result": "FAIL"},
220
+ ]
221
+ mock_run_async_queries.return_value = [("configuration", mock_nodes, None)] # (query_type, nodes, error)
222
+
223
+ results = self.integration._fetch_policy_assessments_from_wiz()
224
+
225
+ assert len(results) == 2
226
+ assert results[0]["id"] == "assessment-1"
227
+ assert results[1]["result"] == "FAIL"
228
+
229
+ # Verify the query was called with correct parameters
230
+ mock_run_async_queries.assert_called_once()
231
+ call_args = mock_run_async_queries.call_args
232
+ assert call_args[1]["max_concurrent"] == 1
233
+
234
+ @patch("regscale.integrations.commercial.wizv2.policy_compliance.run_async_queries")
235
+ def test_fetch_policy_assessments_with_error(self, mock_run_async_queries):
236
+ """Test handling API errors when fetching assessments."""
237
+ # Mock API error response
238
+ mock_run_async_queries.return_value = [("configuration", [], "API Error message")]
239
+
240
+ with pytest.raises(SystemExit): # error_and_exit raises SystemExit
241
+ self.integration._fetch_policy_assessments_from_wiz()
242
+
243
+ def test_create_compliance_item(self):
244
+ """Test creating compliance item from raw data."""
245
+ raw_data = {
246
+ "id": "test-id",
247
+ "result": "FAIL",
248
+ "policy": {"name": "Test Policy"},
249
+ "resource": {"id": "res-123", "name": "Test Resource"},
250
+ "output": {},
251
+ }
252
+
253
+ item = self.integration.create_compliance_item(raw_data)
254
+
255
+ assert isinstance(item, WizComplianceItem)
256
+ assert item.id == "test-id"
257
+ assert item.result == "FAIL"
258
+
259
+ @patch.object(WizPolicyComplianceIntegration, "_load_regscale_assets")
260
+ @patch.object(WizPolicyComplianceIntegration, "_asset_exists_in_regscale")
261
+ def test_fetch_compliance_data(self, mock_asset_exists, mock_load_assets):
262
+ """Test fetching raw compliance data with filtering."""
263
+ # Mock that all assets exist
264
+ mock_asset_exists.return_value = True
265
+
266
+ # Set up the asset cache that's used in the filtering logic
267
+ self.integration._regscale_assets_by_wiz_id = {
268
+ "res-1": Mock(name="Resource 1", wizId="res-1"),
269
+ "res-2": Mock(name="Resource 2", wizId="res-2"),
270
+ }
271
+
272
+ # Mock _load_regscale_assets to not override our cache
273
+ mock_load_assets.return_value = None
274
+
275
+ # Mock the raw data fetch with proper framework data
276
+ mock_raw_data = [
277
+ {
278
+ "id": "assessment-1",
279
+ "result": "PASS",
280
+ "policy": {
281
+ "name": "Policy 1",
282
+ "securitySubCategories": [
283
+ {
284
+ "externalId": "AC-2",
285
+ "category": {"framework": {"id": "wf-id-4", "name": "NIST SP 800-53 Revision 5"}},
286
+ }
287
+ ],
288
+ },
289
+ "resource": {"id": "res-1", "name": "Resource 1"},
290
+ "output": {},
291
+ },
292
+ {
293
+ "id": "assessment-2",
294
+ "result": "FAIL",
295
+ "policy": {
296
+ "name": "Policy 2",
297
+ "securitySubCategories": [
298
+ {
299
+ "externalId": "AC-3",
300
+ "category": {"framework": {"id": "wf-id-4", "name": "NIST SP 800-53 Revision 5"}},
301
+ }
302
+ ],
303
+ },
304
+ "resource": {"id": "res-2", "name": "Resource 2"},
305
+ "output": {},
306
+ },
307
+ ]
308
+
309
+ with patch.object(self.integration, "_fetch_policy_assessments_from_wiz", return_value=mock_raw_data):
310
+ raw_data = self.integration.fetch_compliance_data()
311
+
312
+ assert len(raw_data) == 2 # Both items should pass filtering
313
+ assert all(isinstance(item, dict) for item in raw_data)
314
+ assert raw_data[0]["result"] == "PASS"
315
+ assert raw_data[1]["result"] == "FAIL"
316
+
317
+ @patch("builtins.open", new_callable=mock_open)
318
+ @patch("os.makedirs")
319
+ @patch("regscale.integrations.commercial.wizv2.policy_compliance.datetime")
320
+ def test_write_policy_data_to_json(self, mock_datetime, mock_makedirs, mock_file):
321
+ """Test writing policy data to JSON file."""
322
+ # Set up mock datetime
323
+ mock_datetime.now.return_value.strftime.return_value = "20230806_120000"
324
+
325
+ # Set up mock compliance data
326
+ self.integration.all_compliance_items = [
327
+ WizComplianceItem(
328
+ {
329
+ "id": "test-1",
330
+ "result": "PASS",
331
+ "policy": {"name": "Test Policy"},
332
+ "resource": {"id": "res-1", "name": "Resource 1"},
333
+ "output": {},
334
+ }
335
+ )
336
+ ]
337
+ self.integration.failed_compliance_items = []
338
+ self.integration.framework_mapping = {"wf-id-4": "NIST SP 800-53 Revision 5"}
339
+
340
+ file_path = self.integration.write_policy_data_to_json()
341
+
342
+ # Verify file path
343
+ expected_path = os.path.join("artifacts", "wiz", "policy_compliance_report_20230806_120000.json")
344
+ assert file_path == expected_path
345
+
346
+ # Verify directory creation
347
+ mock_makedirs.assert_called_once_with(os.path.join("artifacts", "wiz"), exist_ok=True)
348
+
349
+ # Verify file write
350
+ mock_file.assert_called_once_with(expected_path, "w", encoding="utf-8")
351
+
352
+ # Verify JSON content was written
353
+ handle = mock_file()
354
+ assert handle.write.called
355
+
356
+ @patch("os.path.exists")
357
+ @patch("builtins.open", new_callable=mock_open)
358
+ def test_load_framework_mapping_from_cache(self, mock_file, mock_exists):
359
+ """Test loading framework mapping from cache file."""
360
+ mock_exists.return_value = True
361
+
362
+ cache_data = {
363
+ "framework_mapping": {"wf-id-4": "NIST SP 800-53 Revision 5", "wf-id-5": "FedRAMP"},
364
+ "timestamp": "2023-08-06T12:00:00",
365
+ }
366
+
367
+ mock_file.return_value.read.return_value = json.dumps(cache_data)
368
+
369
+ mapping = self.integration.load_or_create_framework_mapping()
370
+
371
+ assert mapping == cache_data["framework_mapping"]
372
+ assert self.integration.framework_mapping == mapping
373
+ mock_file.assert_called_once()
374
+
375
+ @patch("os.path.exists")
376
+ @patch("regscale.integrations.commercial.wizv2.policy_compliance.run_async_queries")
377
+ @patch("builtins.open", new_callable=mock_open)
378
+ @patch("os.makedirs")
379
+ def test_fetch_and_cache_framework_mapping(self, mock_makedirs, mock_file, mock_run_async_queries, mock_exists):
380
+ """Test fetching and caching framework mapping."""
381
+ mock_exists.return_value = False # No cache file
382
+
383
+ # Mock framework data from API
384
+ mock_frameworks = [
385
+ {"id": "wf-id-4", "name": "NIST SP 800-53 Revision 5"},
386
+ {"id": "wf-id-5", "name": "FedRAMP"},
387
+ ]
388
+ mock_run_async_queries.return_value = [("configuration", mock_frameworks, None)]
389
+
390
+ mapping = self.integration.load_or_create_framework_mapping()
391
+
392
+ expected_mapping = {"wf-id-4": "NIST SP 800-53 Revision 5", "wf-id-5": "FedRAMP"}
393
+
394
+ assert mapping == expected_mapping
395
+ assert self.integration.framework_mapping == mapping
396
+
397
+ # Verify cache file was written
398
+ mock_file.assert_called()
399
+ mock_makedirs.assert_called()
400
+
401
+ def test_get_framework_name(self):
402
+ """Test getting framework name by ID."""
403
+ self.integration.framework_mapping = {"wf-id-4": "NIST SP 800-53 Revision 5", "wf-id-5": "FedRAMP"}
404
+
405
+ assert self.integration.get_framework_name("wf-id-4") == "NIST SP 800-53 Revision 5"
406
+ assert self.integration.get_framework_name("wf-id-5") == "FedRAMP"
407
+ assert self.integration.get_framework_name("unknown-id") == "unknown-id"
408
+
409
+ def test_create_finding_from_compliance_item_wiz_specific(self):
410
+ """Test creating a finding from Wiz compliance item with enhanced metadata."""
411
+ mock_data = {
412
+ "id": "assessment-123",
413
+ "result": "FAIL",
414
+ "policy": {
415
+ "name": "Test Policy",
416
+ "description": "Test policy description",
417
+ "severity": "HIGH",
418
+ "remediationInstructions": "Fix the issue",
419
+ },
420
+ "resource": {
421
+ "id": "resource-789",
422
+ "name": "Test Resource",
423
+ "type": "VIRTUAL_MACHINE",
424
+ "region": "us-east-1",
425
+ "subscription": {"cloudProvider": "Azure", "name": "Test Subscription"},
426
+ },
427
+ "output": {},
428
+ }
429
+
430
+ compliance_item = WizComplianceItem(mock_data, self.integration)
431
+ finding = self.integration.create_finding_from_compliance_item(compliance_item)
432
+
433
+ assert finding is not None
434
+ assert finding.title == "Policy Compliance Failure: Test Policy"
435
+ assert finding.external_id == "wiz-policy-assessment-123"
436
+ assert finding.asset_identifier == "resource-789" # Should use Wiz resource ID for wizId lookup
437
+ assert "Fix the issue" in finding.description
438
+ assert "Azure" in finding.description
439
+
440
+ def test_create_asset_from_compliance_item_wiz_specific(self):
441
+ """Test creating an asset from Wiz compliance item with enhanced metadata."""
442
+ mock_data = {
443
+ "id": "assessment-123",
444
+ "result": "FAIL",
445
+ "policy": {"name": "Test Policy"},
446
+ "resource": {
447
+ "id": "resource-789",
448
+ "name": "Test Resource",
449
+ "type": "VIRTUAL_MACHINE",
450
+ "region": "us-east-1",
451
+ "subscription": {"cloudProvider": "Azure", "name": "Test Subscription", "externalId": "sub-123"},
452
+ "tags": [{"key": "Environment", "value": "Production"}, {"key": "Owner", "value": "TestTeam"}],
453
+ },
454
+ "output": {},
455
+ }
456
+
457
+ compliance_item = WizComplianceItem(mock_data, self.integration)
458
+
459
+ # Mock compliance mapping for notes
460
+ self.integration.asset_compliance_map = {"resource-789": [compliance_item]}
461
+ self.integration.FAIL_STATUSES = ["FAIL"]
462
+
463
+ with patch.object(self.integration, "_find_existing_asset_by_resource_id", return_value=None):
464
+ asset = self.integration.create_asset_from_compliance_item(compliance_item)
465
+
466
+ assert asset is not None
467
+ assert asset.name == "Test Resource"
468
+ assert asset.identifier == "Test Resource (resource-789)"
469
+ assert asset.asset_type == "Virtual Machine"
470
+ assert "Environment:Production" in asset.description
471
+ assert "Owner:TestTeam" in asset.description
472
+ assert "Azure" in asset.notes
473
+ assert "Test Subscription" in asset.notes
474
+
475
+ def test_create_asset_notes(self):
476
+ """Test creating detailed asset notes."""
477
+ mock_data = {
478
+ "id": "assessment-123",
479
+ "result": "FAIL",
480
+ "policy": {"name": "Test Policy"},
481
+ "resource": {
482
+ "id": "resource-789",
483
+ "name": "Test Resource",
484
+ "type": "VIRTUAL_MACHINE",
485
+ "subscription": {"cloudProvider": "Azure", "name": "Test Subscription", "externalId": "sub-123"},
486
+ },
487
+ "output": {},
488
+ }
489
+
490
+ compliance_item = WizComplianceItem(mock_data, self.integration)
491
+
492
+ # Set up asset compliance map
493
+ self.integration.asset_compliance_map = {"resource-789": [compliance_item, compliance_item]} # 2 assessments
494
+ self.integration.FAIL_STATUSES = ["FAIL"]
495
+
496
+ notes = self.integration._create_asset_notes(compliance_item)
497
+
498
+ assert "Wiz Asset Details" in notes
499
+ assert "resource-789" in notes
500
+ assert "VIRTUAL_MACHINE" in notes
501
+ assert "Azure" in notes
502
+ assert "Test Subscription" in notes
503
+ assert "sub-123" in notes
504
+ assert "**Total Assessments:** 2" in notes
505
+ assert "**Failed Assessments:** 2" in notes
506
+ assert "**Compliance Rate:** 0.0%" in notes
507
+
508
+ def test_fetch_assets_no_assets_created(self):
509
+ """Test that fetch_assets does not create any assets - they come from inventory import."""
510
+ # Create compliance items
511
+ failed_data = {
512
+ "id": "assessment-fail",
513
+ "result": "FAIL",
514
+ "policy": {
515
+ "name": "Fail Policy",
516
+ "securitySubCategories": [
517
+ {
518
+ "externalId": "AC-2",
519
+ "category": {"framework": {"id": "wf-id-4", "name": "NIST SP 800-53 Revision 5"}},
520
+ }
521
+ ],
522
+ },
523
+ "resource": {"id": "resource-fail", "name": "Failed Resource", "type": "VIRTUAL_MACHINE"},
524
+ "output": {},
525
+ }
526
+
527
+ failed_item = WizComplianceItem(failed_data, self.integration)
528
+ self.integration.failed_compliance_items = [failed_item]
529
+
530
+ # Call fetch_assets - should return empty iterator
531
+ assets = list(self.integration.fetch_assets())
532
+
533
+ # Should not create any assets
534
+ assert len(assets) == 0
535
+
536
+ @patch.object(WizPolicyComplianceIntegration, "_asset_exists_in_regscale")
537
+ def test_framework_filtering_in_findings(self, mock_asset_exists):
538
+ """Test that fetch_findings filters by framework and uses asset names."""
539
+ # Mock that the framework match resource exists in RegScale (using resource ID)
540
+ mock_asset_exists.side_effect = lambda resource_id: resource_id == "resource-framework-match"
541
+
542
+ # Create compliance items in different frameworks
543
+ framework_data = {
544
+ "id": "assessment-framework-match",
545
+ "result": "FAIL",
546
+ "policy": {
547
+ "name": "Framework Match Policy",
548
+ "securitySubCategories": [
549
+ {
550
+ "externalId": "AC-2",
551
+ "category": {"framework": {"id": "wf-id-4", "name": "NIST SP 800-53 Revision 5"}},
552
+ }
553
+ ],
554
+ },
555
+ "resource": {
556
+ "id": "resource-framework-match",
557
+ "name": "Framework Match Resource",
558
+ "type": "VIRTUAL_MACHINE",
559
+ },
560
+ "output": {},
561
+ }
562
+
563
+ other_framework_data = {
564
+ "id": "assessment-other-framework",
565
+ "result": "FAIL",
566
+ "policy": {
567
+ "name": "Other Framework Policy",
568
+ "securitySubCategories": [
569
+ {
570
+ "externalId": "AC-3",
571
+ "category": {"framework": {"id": "wf-id-5", "name": "Other Framework"}},
572
+ }
573
+ ],
574
+ },
575
+ "resource": {
576
+ "id": "resource-other-framework",
577
+ "name": "Other Framework Resource",
578
+ "type": "VIRTUAL_MACHINE",
579
+ },
580
+ "output": {},
581
+ }
582
+
583
+ framework_item = WizComplianceItem(framework_data, self.integration)
584
+ other_framework_item = WizComplianceItem(other_framework_data, self.integration)
585
+
586
+ # Mock integration data - both items are "failed" but only one belongs to the target framework
587
+ self.integration.all_compliance_items = [framework_item, other_framework_item]
588
+ self.integration.failed_compliance_items = [framework_item, other_framework_item]
589
+ self.integration.FAIL_STATUSES = ["FAIL"]
590
+
591
+ # Test that only framework-specific items have valid control IDs
592
+ assert framework_item.control_id == "AC-2" # Should match framework
593
+ assert other_framework_item.control_id == "" # Should be empty due to framework filtering
594
+
595
+ # Test fetch_assets - should return empty
596
+ assets = list(self.integration.fetch_assets())
597
+ assert len(assets) == 0 # No assets created
598
+
599
+ # Test fetch_findings
600
+ findings = list(self.integration.fetch_findings())
601
+ # Should only create finding for framework-matching item that has existing asset
602
+ assert len(findings) == 1
603
+ finding = findings[0]
604
+ assert finding.asset_identifier == "resource-framework-match" # Should use Wiz resource ID
605
+
606
+ @patch.object(WizPolicyComplianceIntegration, "authenticate_wiz")
607
+ @patch.object(WizPolicyComplianceIntegration, "load_or_create_framework_mapping")
608
+ @patch.object(WizPolicyComplianceIntegration, "write_policy_data_to_json")
609
+ @patch.object(WizPolicyComplianceIntegration, "sync_compliance")
610
+ def test_sync_policy_compliance(self, mock_sync_compliance, mock_write_json, mock_load_mapping, mock_auth):
611
+ """Test the main sync policy compliance method."""
612
+ mock_write_json.return_value = "/path/to/output.json"
613
+
614
+ self.integration.sync_policy_compliance()
615
+
616
+ # Verify all steps were called in correct order
617
+ mock_auth.assert_called_once()
618
+ mock_load_mapping.assert_called_once()
619
+ # Note: process_compliance_data is called internally by sync_compliance, not directly
620
+ mock_sync_compliance.assert_called_once()
621
+ mock_write_json.assert_called_once()
622
+
623
+ def test_sync_wiz_compliance_backward_compatibility(self):
624
+ """Test backward compatibility method."""
625
+ with patch.object(self.integration, "sync_policy_compliance") as mock_sync:
626
+ self.integration.sync_wiz_compliance()
627
+ mock_sync.assert_called_once()
628
+
629
+ @patch.object(WizPolicyComplianceIntegration, "_asset_exists_in_regscale")
630
+ @patch.object(WizPolicyComplianceIntegration, "_get_control_implementations")
631
+ @patch.object(WizPolicyComplianceIntegration, "_load_existing_records_cache")
632
+ @patch.object(WizPolicyComplianceIntegration, "_process_single_control_assessment")
633
+ def test_control_assessments_only_for_existing_assets(
634
+ self, mock_process_single, mock_load_cache, mock_get_implementations, mock_asset_exists
635
+ ):
636
+ """Test that control assessments are only created for controls with existing assets."""
637
+ from regscale.integrations.compliance_integration import ControlImplementation
638
+ from regscale.models.regscale_models import SecurityControl
639
+
640
+ # Mock that only one asset exists (for AC-2 control)
641
+ mock_asset_exists.side_effect = lambda resource_id: resource_id == "resource-with-asset"
642
+
643
+ # Mock single control assessment creation to return 1 (one assessment created)
644
+ mock_process_single.return_value = 1
645
+
646
+ # Mock control implementations
647
+ mock_impl1 = ControlImplementation(id=1, controlID=100, status="Not Satisfied", title="Test Implementation 1")
648
+ mock_impl2 = ControlImplementation(id=2, controlID=200, status="Not Satisfied", title="Test Implementation 2")
649
+ mock_get_implementations.return_value = [mock_impl1, mock_impl2]
650
+
651
+ # Mock SecurityControl.get_object to return controls
652
+ def mock_get_security_control(object_id):
653
+ if object_id == 100:
654
+ control = SecurityControl()
655
+ control.id = 100
656
+ control.controlId = "AC-2"
657
+ return control
658
+ elif object_id == 200:
659
+ control = SecurityControl()
660
+ control.id = 200
661
+ control.controlId = "AC-3"
662
+ return control
663
+ return None
664
+
665
+ # Create compliance items - one with existing asset, one without
666
+ compliance_data = [
667
+ {
668
+ "id": "assessment-with-asset",
669
+ "result": "FAIL",
670
+ "policy": {
671
+ "name": "Policy with Asset",
672
+ "securitySubCategories": [
673
+ {
674
+ "externalId": "AC-2",
675
+ "category": {"framework": {"id": "wf-id-4", "name": "NIST SP 800-53 Revision 5"}},
676
+ }
677
+ ],
678
+ },
679
+ "resource": {"id": "resource-with-asset", "name": "Resource With Asset"},
680
+ "output": {},
681
+ },
682
+ {
683
+ "id": "assessment-no-asset",
684
+ "result": "FAIL",
685
+ "policy": {
686
+ "name": "Policy No Asset",
687
+ "securitySubCategories": [
688
+ {
689
+ "externalId": "AC-3",
690
+ "category": {"framework": {"id": "wf-id-4", "name": "NIST SP 800-53 Revision 5"}},
691
+ }
692
+ ],
693
+ },
694
+ "resource": {"id": "resource-no-asset", "name": "Resource No Asset"},
695
+ "output": {},
696
+ },
697
+ ]
698
+
699
+ # Process the compliance data
700
+ with patch.object(SecurityControl, "get_object", side_effect=mock_get_security_control):
701
+ with patch.object(self.integration, "_fetch_policy_assessments_from_wiz", return_value=compliance_data):
702
+ self.integration.process_compliance_data()
703
+ self.integration._process_control_assessments()
704
+
705
+ # Verify only one control assessment was processed (AC-2 with existing asset)
706
+ # AC-3 should be skipped because its asset doesn't exist
707
+ mock_process_single.assert_called_once_with(
708
+ control_id="ac-2",
709
+ implementations=[mock_impl1, mock_impl2],
710
+ processed_impl_today=set(),
711
+ )
712
+
713
+
714
+ class TestWizComplianceItemEdgeCases:
715
+ """Test edge cases for WizComplianceItem."""
716
+
717
+ def test_empty_data(self):
718
+ """Test WizComplianceItem with minimal data."""
719
+ minimal_data = {"id": "", "result": "", "policy": {}, "resource": {}, "output": {}}
720
+
721
+ item = WizComplianceItem(minimal_data)
722
+
723
+ assert item.id == ""
724
+ assert item.result == ""
725
+ assert item.resource_id == ""
726
+ assert item.resource_name == ""
727
+ assert item.control_id == ""
728
+ assert item.severity is None
729
+ assert item.framework == ""
730
+ assert not item.is_pass
731
+ assert not item.is_fail
732
+
733
+ def test_nested_missing_data(self):
734
+ """Test WizComplianceItem with nested missing data."""
735
+ data_with_missing_nested = {
736
+ "id": "test",
737
+ "result": "PASS",
738
+ "policy": {"securitySubCategories": [{"category": {"framework": {}}}]}, # Missing id and name
739
+ "resource": {"subscription": {}}, # Missing cloudProvider
740
+ "output": {},
741
+ }
742
+
743
+ item = WizComplianceItem(data_with_missing_nested)
744
+
745
+ assert item.framework_id is None
746
+ assert item.framework == ""
747
+
748
+
749
+ if __name__ == "__main__":
750
+ pytest.main([__file__])