regscale-cli 6.26.0.0__py3-none-any.whl → 6.27.0.1__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 (96) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +1 -1
  3. regscale/core/app/internal/evidence.py +419 -2
  4. regscale/dev/code_gen.py +24 -20
  5. regscale/integrations/commercial/__init__.py +0 -1
  6. regscale/integrations/commercial/jira.py +367 -126
  7. regscale/integrations/commercial/qualys/__init__.py +7 -8
  8. regscale/integrations/commercial/qualys/scanner.py +8 -3
  9. regscale/integrations/commercial/synqly/assets.py +17 -0
  10. regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
  11. regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
  12. regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
  13. regscale/integrations/commercial/tenablev2/commands.py +142 -1
  14. regscale/integrations/commercial/tenablev2/scanner.py +0 -1
  15. regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
  16. regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
  17. regscale/integrations/commercial/wizv2/click.py +44 -59
  18. regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
  19. regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
  20. regscale/integrations/commercial/wizv2/compliance_report.py +10 -9
  21. regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
  22. regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
  23. regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
  24. regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
  25. regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
  26. regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
  27. regscale/integrations/commercial/wizv2/issue.py +1 -1
  28. regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
  29. regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
  30. regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
  31. regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
  32. regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
  33. regscale/integrations/commercial/wizv2/reports.py +1 -1
  34. regscale/integrations/commercial/wizv2/sbom.py +1 -1
  35. regscale/integrations/commercial/wizv2/scanner.py +40 -100
  36. regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
  37. regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
  38. regscale/integrations/commercial/wizv2/variables.py +89 -3
  39. regscale/integrations/compliance_integration.py +0 -46
  40. regscale/integrations/control_matcher.py +22 -3
  41. regscale/integrations/due_date_handler.py +14 -8
  42. regscale/integrations/public/fedramp/docx_parser.py +10 -1
  43. regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
  44. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  45. regscale/integrations/scanner_integration.py +127 -57
  46. regscale/models/integration_models/cisa_kev_data.json +132 -9
  47. regscale/models/integration_models/qualys.py +3 -4
  48. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  49. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
  50. regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
  51. regscale/models/regscale_models/control_implementation.py +1 -1
  52. regscale/models/regscale_models/issue.py +0 -1
  53. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/METADATA +1 -17
  54. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/RECORD +94 -61
  55. tests/regscale/integrations/commercial/test_jira.py +481 -91
  56. tests/regscale/integrations/commercial/test_wiz.py +96 -200
  57. tests/regscale/integrations/commercial/wizv2/__init__.py +1 -1
  58. tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
  59. tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
  60. tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
  61. tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
  62. tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
  63. tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
  64. tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
  65. tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
  66. tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
  67. tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
  68. tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
  69. tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
  70. tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
  71. tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
  72. tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
  73. tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
  74. tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
  75. tests/regscale/integrations/commercial/wizv2/test_issue.py +1 -1
  76. tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
  77. tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
  78. tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
  79. tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
  80. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +1 -1
  81. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +72 -29
  82. tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
  83. tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
  84. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +946 -78
  85. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +97 -202
  86. tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
  87. tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
  88. tests/regscale/integrations/public/test_fedramp.py +301 -0
  89. tests/regscale/integrations/test_control_matcher.py +83 -0
  90. regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
  91. tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +0 -750
  92. /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
  93. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/LICENSE +0 -0
  94. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/WHEEL +0 -0
  95. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/entry_points.txt +0 -0
  96. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1 @@
1
+ """Tests for Wiz processors module."""
@@ -0,0 +1,671 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Comprehensive unit tests for Wiz finding processors module."""
4
+
5
+ import logging
6
+ from unittest.mock import MagicMock, patch, PropertyMock
7
+ import pytest
8
+
9
+ from regscale.integrations.commercial.wizv2.processors.finding import (
10
+ WizComplianceItem,
11
+ FindingConsolidator,
12
+ FindingToIssueProcessor,
13
+ )
14
+ from regscale.integrations.scanner_integration import IntegrationFinding
15
+ from regscale.models import regscale_models
16
+
17
+ logger = logging.getLogger("regscale")
18
+
19
+ PATH = "regscale.integrations.commercial.wizv2.processors.finding"
20
+
21
+
22
+ # =============================
23
+ # WizComplianceItem Tests
24
+ # =============================
25
+
26
+
27
+ class TestWizComplianceItem:
28
+ """Test WizComplianceItem wrapper class."""
29
+
30
+ def test_initialization(self):
31
+ """Test creating WizComplianceItem wrapper."""
32
+ mock_item = MagicMock()
33
+ mock_item.resource_id = "res-123"
34
+ mock_item.control_id = "AC-2(1)"
35
+ mock_item.is_fail = True
36
+
37
+ wrapper = WizComplianceItem(mock_item)
38
+
39
+ assert wrapper._item == mock_item
40
+
41
+ def test_resource_id_property(self):
42
+ """Test resource_id property returns correct value."""
43
+ mock_item = MagicMock()
44
+ mock_item.resource_id = "resource-456"
45
+
46
+ wrapper = WizComplianceItem(mock_item)
47
+
48
+ assert wrapper.resource_id == "resource-456"
49
+
50
+ def test_resource_id_property_missing_attribute(self):
51
+ """Test resource_id property when attribute is missing."""
52
+ mock_item = MagicMock(spec=[])
53
+
54
+ wrapper = WizComplianceItem(mock_item)
55
+
56
+ assert wrapper.resource_id == ""
57
+
58
+ def test_control_id_property(self):
59
+ """Test control_id property returns correct value."""
60
+ mock_item = MagicMock()
61
+ mock_item.control_id = "SC-7"
62
+
63
+ wrapper = WizComplianceItem(mock_item)
64
+
65
+ assert wrapper.control_id == "SC-7"
66
+
67
+ def test_control_id_property_missing_attribute(self):
68
+ """Test control_id property when attribute is missing."""
69
+ mock_item = MagicMock(spec=[])
70
+
71
+ wrapper = WizComplianceItem(mock_item)
72
+
73
+ assert wrapper.control_id == ""
74
+
75
+ def test_is_fail_property(self):
76
+ """Test is_fail property returns correct value."""
77
+ mock_item = MagicMock()
78
+ mock_item.is_fail = True
79
+
80
+ wrapper = WizComplianceItem(mock_item)
81
+
82
+ assert wrapper.is_fail is True
83
+
84
+ def test_is_fail_property_missing_attribute(self):
85
+ """Test is_fail property when attribute is missing."""
86
+ mock_item = MagicMock(spec=[])
87
+
88
+ wrapper = WizComplianceItem(mock_item)
89
+
90
+ assert wrapper.is_fail is False
91
+
92
+ def test_get_all_control_ids_with_method(self):
93
+ """Test get_all_control_ids when wrapped item has the method."""
94
+ mock_item = MagicMock()
95
+ mock_item.control_id = "AC-2"
96
+ mock_item._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2", "AC-2(1)", "AC-2(2)"])
97
+
98
+ wrapper = WizComplianceItem(mock_item)
99
+ result = wrapper.get_all_control_ids()
100
+
101
+ assert result == ["AC-2", "AC-2(1)", "AC-2(2)"]
102
+ mock_item._get_all_control_ids_for_compliance_item.assert_called_once_with(mock_item)
103
+
104
+ def test_get_all_control_ids_fallback_single_control(self):
105
+ """Test get_all_control_ids falls back to control_id."""
106
+ mock_item = MagicMock()
107
+ mock_item.control_id = "SC-7"
108
+ del mock_item._get_all_control_ids_for_compliance_item
109
+
110
+ wrapper = WizComplianceItem(mock_item)
111
+ result = wrapper.get_all_control_ids()
112
+
113
+ assert result == ["SC-7"]
114
+
115
+ def test_get_all_control_ids_fallback_no_control(self):
116
+ """Test get_all_control_ids when no control_id exists."""
117
+ mock_item = MagicMock(spec=[])
118
+
119
+ wrapper = WizComplianceItem(mock_item)
120
+ result = wrapper.get_all_control_ids()
121
+
122
+ assert result == []
123
+
124
+
125
+ # =============================
126
+ # FindingConsolidator Tests
127
+ # =============================
128
+
129
+
130
+ class TestFindingConsolidator:
131
+ """Test FindingConsolidator class."""
132
+
133
+ def setup_method(self):
134
+ """Set up test fixtures."""
135
+ self.mock_integration = MagicMock()
136
+ self.consolidator = FindingConsolidator(self.mock_integration)
137
+
138
+ def test_initialization(self):
139
+ """Test FindingConsolidator initialization."""
140
+ assert self.consolidator.integration == self.mock_integration
141
+ assert self.consolidator.asset_consolidator is not None
142
+
143
+ def test_create_consolidated_findings_empty_list(self):
144
+ """Test creating findings with empty list."""
145
+ result = list(self.consolidator.create_consolidated_findings([]))
146
+
147
+ assert result == []
148
+
149
+ def test_create_consolidated_findings_no_control_groups(self):
150
+ """Test creating findings when no control groups are created."""
151
+ mock_item = MagicMock()
152
+ mock_item.resource_id = ""
153
+ mock_item.control_id = ""
154
+ mock_item._get_all_control_ids_for_compliance_item = MagicMock(return_value=[])
155
+
156
+ result = list(self.consolidator.create_consolidated_findings([mock_item]))
157
+
158
+ assert result == []
159
+
160
+ @patch(f"{PATH}.FindingConsolidator._create_consolidated_finding_for_control")
161
+ def test_create_consolidated_findings_success(self, mock_create_finding):
162
+ """Test creating consolidated findings successfully."""
163
+ # Create mock compliance items
164
+ item1 = MagicMock()
165
+ item1.resource_id = "res-1"
166
+ item1.control_id = "AC-2"
167
+ item1._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2"])
168
+
169
+ item2 = MagicMock()
170
+ item2.resource_id = "res-2"
171
+ item2.control_id = "AC-2"
172
+ item2._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2"])
173
+
174
+ # Mock finding creation
175
+ mock_finding = MagicMock(spec=IntegrationFinding)
176
+ mock_create_finding.return_value = mock_finding
177
+
178
+ result = list(self.consolidator.create_consolidated_findings([item1, item2]))
179
+
180
+ assert len(result) == 1
181
+ assert result[0] == mock_finding
182
+ mock_create_finding.assert_called_once()
183
+
184
+ @patch(f"{PATH}.FindingConsolidator._create_consolidated_finding_for_control")
185
+ def test_create_consolidated_findings_multiple_controls(self, mock_create_finding):
186
+ """Test creating findings for multiple controls."""
187
+ item1 = MagicMock()
188
+ item1.resource_id = "res-1"
189
+ item1._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2", "SC-7"])
190
+
191
+ mock_finding1 = MagicMock(spec=IntegrationFinding)
192
+ mock_finding2 = MagicMock(spec=IntegrationFinding)
193
+ mock_create_finding.side_effect = [mock_finding1, mock_finding2]
194
+
195
+ result = list(self.consolidator.create_consolidated_findings([item1]))
196
+
197
+ assert len(result) == 2
198
+ assert mock_create_finding.call_count == 2
199
+
200
+ def test_group_by_control_empty_list(self):
201
+ """Test grouping empty compliance items."""
202
+ result = self.consolidator._group_by_control([])
203
+
204
+ assert result == {}
205
+
206
+ def test_group_by_control_single_control_single_resource(self):
207
+ """Test grouping single control with single resource."""
208
+ mock_item = MagicMock()
209
+ mock_item.resource_id = "res-123"
210
+ mock_item._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2"])
211
+
212
+ result = self.consolidator._group_by_control([mock_item])
213
+
214
+ assert "AC-2" in result
215
+ assert "res-123" in result["AC-2"]
216
+ assert result["AC-2"]["res-123"] == mock_item
217
+
218
+ def test_group_by_control_multiple_resources_same_control(self):
219
+ """Test grouping multiple resources for same control."""
220
+ item1 = MagicMock()
221
+ item1.resource_id = "res-1"
222
+ item1._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2"])
223
+
224
+ item2 = MagicMock()
225
+ item2.resource_id = "res-2"
226
+ item2._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2"])
227
+
228
+ result = self.consolidator._group_by_control([item1, item2])
229
+
230
+ assert len(result["AC-2"]) == 2
231
+ assert "res-1" in result["AC-2"]
232
+ assert "res-2" in result["AC-2"]
233
+
234
+ def test_group_by_control_case_normalization(self):
235
+ """Test that control IDs are normalized to uppercase."""
236
+ item1 = MagicMock()
237
+ item1.resource_id = "res-1"
238
+ item1._get_all_control_ids_for_compliance_item = MagicMock(return_value=["ac-2"])
239
+
240
+ item2 = MagicMock()
241
+ item2.resource_id = "res-2"
242
+ item2._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2"])
243
+
244
+ result = self.consolidator._group_by_control([item1, item2])
245
+
246
+ assert "AC-2" in result
247
+ assert len(result["AC-2"]) == 2
248
+
249
+ def test_group_by_control_resource_id_normalization(self):
250
+ """Test that resource IDs are normalized to lowercase."""
251
+ item1 = MagicMock()
252
+ item1.resource_id = "RES-1"
253
+ item1._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2"])
254
+
255
+ item2 = MagicMock()
256
+ item2.resource_id = "res-1"
257
+ item2._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2"])
258
+
259
+ result = self.consolidator._group_by_control([item1, item2])
260
+
261
+ # Should only have one resource (first occurrence wins)
262
+ assert len(result["AC-2"]) == 1
263
+ assert "res-1" in result["AC-2"]
264
+
265
+ def test_group_by_control_multiple_controls_per_item(self):
266
+ """Test grouping item that maps to multiple controls."""
267
+ mock_item = MagicMock()
268
+ mock_item.resource_id = "res-1"
269
+ mock_item._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2", "SC-7", "AU-2"])
270
+
271
+ result = self.consolidator._group_by_control([mock_item])
272
+
273
+ assert len(result) == 3
274
+ assert "AC-2" in result
275
+ assert "SC-7" in result
276
+ assert "AU-2" in result
277
+ assert result["AC-2"]["res-1"] == mock_item
278
+ assert result["SC-7"]["res-1"] == mock_item
279
+ assert result["AU-2"]["res-1"] == mock_item
280
+
281
+ def test_group_by_control_skip_empty_resource_id(self):
282
+ """Test that items without resource_id are skipped."""
283
+ mock_item = MagicMock()
284
+ mock_item.resource_id = ""
285
+ mock_item._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2"])
286
+
287
+ result = self.consolidator._group_by_control([mock_item])
288
+
289
+ assert result == {}
290
+
291
+ def test_group_by_control_skip_empty_control_ids(self):
292
+ """Test that items without control IDs are skipped."""
293
+ mock_item = MagicMock()
294
+ mock_item.resource_id = "res-1"
295
+ mock_item._get_all_control_ids_for_compliance_item = MagicMock(return_value=[])
296
+
297
+ result = self.consolidator._group_by_control([mock_item])
298
+
299
+ assert result == {}
300
+
301
+ @patch(f"{PATH}.FindingConsolidator._update_finding_with_assets")
302
+ @patch(f"{PATH}.FindingConsolidator._create_base_finding")
303
+ @patch(f"{PATH}.FindingConsolidator._build_asset_mappings")
304
+ def test_create_consolidated_finding_for_control_success(
305
+ self, mock_build_mappings, mock_create_base, mock_update_finding
306
+ ):
307
+ """Test creating consolidated finding successfully."""
308
+ resources = {"res-1": MagicMock(), "res-2": MagicMock()}
309
+ asset_mappings = {"res-1": {"name": "Asset 1", "wiz_id": "res-1"}}
310
+
311
+ mock_build_mappings.return_value = asset_mappings
312
+ mock_finding = MagicMock(spec=IntegrationFinding)
313
+ mock_create_base.return_value = mock_finding
314
+
315
+ result = self.consolidator._create_consolidated_finding_for_control("AC-2", resources)
316
+
317
+ assert result == mock_finding
318
+ mock_build_mappings.assert_called_once()
319
+ mock_create_base.assert_called_once()
320
+ mock_update_finding.assert_called_once_with(mock_finding, asset_mappings)
321
+
322
+ @patch(f"{PATH}.FindingConsolidator._build_asset_mappings")
323
+ def test_create_consolidated_finding_for_control_no_assets(self, mock_build_mappings):
324
+ """Test creating finding when no assets exist in RegScale."""
325
+ resources = {"res-1": MagicMock()}
326
+ mock_build_mappings.return_value = {}
327
+
328
+ result = self.consolidator._create_consolidated_finding_for_control("AC-2", resources)
329
+
330
+ assert result is None
331
+
332
+ @patch(f"{PATH}.FindingConsolidator._create_base_finding")
333
+ @patch(f"{PATH}.FindingConsolidator._build_asset_mappings")
334
+ def test_create_consolidated_finding_for_control_base_finding_fails(self, mock_build_mappings, mock_create_base):
335
+ """Test handling when base finding creation fails."""
336
+ resources = {"res-1": MagicMock()}
337
+ asset_mappings = {"res-1": {"name": "Asset 1", "wiz_id": "res-1"}}
338
+
339
+ mock_build_mappings.return_value = asset_mappings
340
+ mock_create_base.return_value = None
341
+
342
+ result = self.consolidator._create_consolidated_finding_for_control("AC-2", resources)
343
+
344
+ assert result is None
345
+
346
+ def test_build_asset_mappings_all_assets_exist(self):
347
+ """Test building asset mappings when all assets exist."""
348
+ self.mock_integration._asset_exists_in_regscale.return_value = True
349
+
350
+ mock_asset1 = MagicMock()
351
+ mock_asset1.name = "Asset 1"
352
+ mock_asset2 = MagicMock()
353
+ mock_asset2.name = "Asset 2"
354
+
355
+ self.mock_integration.get_asset_by_identifier.side_effect = [mock_asset1, mock_asset2]
356
+
357
+ result = self.consolidator._build_asset_mappings(["res-1", "res-2"])
358
+
359
+ assert len(result) == 2
360
+ assert result["res-1"]["name"] == "Asset 1"
361
+ assert result["res-1"]["wiz_id"] == "res-1"
362
+ assert result["res-2"]["name"] == "Asset 2"
363
+ assert result["res-2"]["wiz_id"] == "res-2"
364
+
365
+ def test_build_asset_mappings_some_assets_missing(self):
366
+ """Test building asset mappings when some assets don't exist."""
367
+ self.mock_integration._asset_exists_in_regscale.side_effect = [True, False, True]
368
+
369
+ mock_asset1 = MagicMock()
370
+ mock_asset1.name = "Asset 1"
371
+ mock_asset3 = MagicMock()
372
+ mock_asset3.name = "Asset 3"
373
+
374
+ self.mock_integration.get_asset_by_identifier.side_effect = [mock_asset1, mock_asset3]
375
+
376
+ result = self.consolidator._build_asset_mappings(["res-1", "res-2", "res-3"])
377
+
378
+ assert len(result) == 2
379
+ assert "res-1" in result
380
+ assert "res-2" not in result
381
+ assert "res-3" in result
382
+
383
+ def test_build_asset_mappings_asset_has_no_name(self):
384
+ """Test building asset mappings when asset has no name."""
385
+ self.mock_integration._asset_exists_in_regscale.return_value = True
386
+
387
+ mock_asset = MagicMock(spec=[])
388
+ self.mock_integration.get_asset_by_identifier.return_value = mock_asset
389
+
390
+ result = self.consolidator._build_asset_mappings(["res-1"])
391
+
392
+ # Should fall back to resource ID
393
+ assert result["res-1"]["name"] == "res-1"
394
+ assert result["res-1"]["wiz_id"] == "res-1"
395
+
396
+ def test_build_asset_mappings_get_asset_returns_none(self):
397
+ """Test building asset mappings when get_asset returns None."""
398
+ self.mock_integration._asset_exists_in_regscale.return_value = True
399
+ self.mock_integration.get_asset_by_identifier.return_value = None
400
+
401
+ result = self.consolidator._build_asset_mappings(["res-1"])
402
+
403
+ # Should fall back to resource ID
404
+ assert result["res-1"]["name"] == "res-1"
405
+ assert result["res-1"]["wiz_id"] == "res-1"
406
+
407
+ def test_create_base_finding_with_specific_control_method(self):
408
+ """Test creating base finding using specific control method."""
409
+ mock_item = MagicMock()
410
+ mock_finding = MagicMock(spec=IntegrationFinding)
411
+
412
+ self.mock_integration._create_finding_for_specific_control = MagicMock(return_value=mock_finding)
413
+
414
+ result = self.consolidator._create_base_finding(mock_item, "AC-2")
415
+
416
+ assert result == mock_finding
417
+ self.mock_integration._create_finding_for_specific_control.assert_called_once_with(mock_item, "AC-2")
418
+
419
+ def test_create_base_finding_fallback_to_generic_method(self):
420
+ """Test creating base finding using generic method."""
421
+ mock_item = MagicMock()
422
+ mock_finding = MagicMock(spec=IntegrationFinding)
423
+
424
+ del self.mock_integration._create_finding_for_specific_control
425
+ self.mock_integration.create_finding_from_compliance_item = MagicMock(return_value=mock_finding)
426
+
427
+ result = self.consolidator._create_base_finding(mock_item, "AC-2")
428
+
429
+ assert result == mock_finding
430
+ self.mock_integration.create_finding_from_compliance_item.assert_called_once_with(mock_item)
431
+
432
+ def test_create_base_finding_exception_handling(self):
433
+ """Test exception handling during base finding creation."""
434
+ mock_item = MagicMock()
435
+
436
+ self.mock_integration._create_finding_for_specific_control = MagicMock(side_effect=Exception("Creation failed"))
437
+
438
+ result = self.consolidator._create_base_finding(mock_item, "AC-2")
439
+
440
+ assert result is None
441
+
442
+ def test_update_finding_with_assets(self):
443
+ """Test updating finding with consolidated asset information."""
444
+ mock_finding = MagicMock(spec=IntegrationFinding)
445
+ mock_finding.description = "Control failure"
446
+
447
+ asset_mappings = {"res-1": {"name": "Asset 1"}, "res-2": {"name": "Asset 2"}}
448
+
449
+ self.consolidator._update_finding_with_assets(mock_finding, asset_mappings)
450
+
451
+ # Verify asset_identifier was set (mocked consolidator would have been called)
452
+ assert mock_finding.asset_identifier is not None
453
+ # Description should be updated with multiple assets
454
+ assert "2 assets" in mock_finding.description
455
+
456
+
457
+ # =============================
458
+ # FindingToIssueProcessor Tests
459
+ # =============================
460
+
461
+
462
+ class TestFindingToIssueProcessor:
463
+ """Test FindingToIssueProcessor class."""
464
+
465
+ def setup_method(self):
466
+ """Set up test fixtures."""
467
+ self.mock_integration = MagicMock()
468
+ self.processor = FindingToIssueProcessor(self.mock_integration)
469
+
470
+ def test_initialization(self):
471
+ """Test FindingToIssueProcessor initialization."""
472
+ assert self.processor.integration == self.mock_integration
473
+
474
+ def test_process_findings_to_issues_empty_list(self):
475
+ """Test processing empty findings list."""
476
+ created, skipped = self.processor.process_findings_to_issues([])
477
+
478
+ assert created == 0
479
+ assert skipped == 0
480
+
481
+ @patch(f"{PATH}.FindingToIssueProcessor._process_single_finding")
482
+ def test_process_findings_to_issues_all_successful(self, mock_process_single):
483
+ """Test processing findings where all succeed."""
484
+ mock_process_single.return_value = True
485
+
486
+ findings = [MagicMock(spec=IntegrationFinding) for _ in range(3)]
487
+ created, skipped = self.processor.process_findings_to_issues(findings) # type: ignore[arg-type]
488
+
489
+ assert created == 3
490
+ assert skipped == 0
491
+ assert mock_process_single.call_count == 3
492
+
493
+ @patch(f"{PATH}.FindingToIssueProcessor._process_single_finding")
494
+ def test_process_findings_to_issues_some_skipped(self, mock_process_single):
495
+ """Test processing findings where some are skipped."""
496
+ mock_process_single.side_effect = [True, False, True, False]
497
+
498
+ findings = [MagicMock(spec=IntegrationFinding) for _ in range(4)]
499
+ created, skipped = self.processor.process_findings_to_issues(findings) # type: ignore[arg-type]
500
+
501
+ assert created == 2
502
+ assert skipped == 2
503
+
504
+ @patch(f"{PATH}.FindingToIssueProcessor._process_single_finding")
505
+ def test_process_findings_to_issues_exception_handling(self, mock_process_single):
506
+ """Test exception handling during finding processing."""
507
+ mock_process_single.side_effect = [True, Exception("Processing error"), True]
508
+
509
+ findings = [MagicMock(spec=IntegrationFinding) for _ in range(3)]
510
+ created, skipped = self.processor.process_findings_to_issues(findings) # type: ignore[arg-type]
511
+
512
+ assert created == 2
513
+ assert skipped == 1
514
+
515
+ @patch(f"{PATH}.FindingToIssueProcessor._verify_assets_exist")
516
+ def test_process_single_finding_assets_not_found(self, mock_verify):
517
+ """Test processing single finding when assets don't exist."""
518
+ mock_verify.return_value = False
519
+ mock_finding = MagicMock(spec=IntegrationFinding)
520
+ mock_finding.external_id = "finding-123"
521
+
522
+ result = self.processor._process_single_finding(mock_finding)
523
+
524
+ assert result is False
525
+ mock_verify.assert_called_once_with(mock_finding)
526
+
527
+ @patch(f"{PATH}.FindingToIssueProcessor._verify_assets_exist")
528
+ def test_process_single_finding_success(self, mock_verify):
529
+ """Test successfully processing single finding."""
530
+ mock_verify.return_value = True
531
+ mock_finding = MagicMock(spec=IntegrationFinding)
532
+
533
+ self.mock_integration.get_issue_title.return_value = "Issue Title"
534
+ mock_issue = MagicMock(spec=regscale_models.Issue)
535
+ self.mock_integration.create_or_update_issue_from_finding.return_value = mock_issue
536
+
537
+ result = self.processor._process_single_finding(mock_finding)
538
+
539
+ assert result is True
540
+ self.mock_integration.get_issue_title.assert_called_once_with(mock_finding)
541
+ self.mock_integration.create_or_update_issue_from_finding.assert_called_once()
542
+
543
+ @patch(f"{PATH}.FindingToIssueProcessor._verify_assets_exist")
544
+ def test_process_single_finding_issue_creation_fails(self, mock_verify):
545
+ """Test processing when issue creation returns None."""
546
+ mock_verify.return_value = True
547
+ mock_finding = MagicMock(spec=IntegrationFinding)
548
+
549
+ self.mock_integration.get_issue_title.return_value = "Issue Title"
550
+ self.mock_integration.create_or_update_issue_from_finding.return_value = None
551
+
552
+ result = self.processor._process_single_finding(mock_finding)
553
+
554
+ assert result is False
555
+
556
+ @patch(f"{PATH}.FindingToIssueProcessor._verify_assets_exist")
557
+ def test_process_single_finding_exception_during_creation(self, mock_verify):
558
+ """Test exception handling during issue creation."""
559
+ mock_verify.return_value = True
560
+ mock_finding = MagicMock(spec=IntegrationFinding)
561
+
562
+ self.mock_integration.get_issue_title.side_effect = Exception("Title generation failed")
563
+
564
+ result = self.processor._process_single_finding(mock_finding)
565
+
566
+ assert result is False
567
+
568
+ def test_verify_assets_exist_no_asset_identifier(self):
569
+ """Test verification when finding has no asset_identifier."""
570
+ mock_finding = MagicMock(spec=[])
571
+
572
+ result = self.processor._verify_assets_exist(mock_finding)
573
+
574
+ assert result is False
575
+
576
+ def test_verify_assets_exist_empty_asset_identifier(self):
577
+ """Test verification when asset_identifier is empty."""
578
+ mock_finding = MagicMock(spec=IntegrationFinding)
579
+ mock_finding.asset_identifier = ""
580
+
581
+ result = self.processor._verify_assets_exist(mock_finding)
582
+
583
+ assert result is False
584
+
585
+ def test_verify_assets_exist_single_asset_exists(self):
586
+ """Test verification for single existing asset."""
587
+ mock_finding = MagicMock(spec=IntegrationFinding)
588
+ mock_finding.asset_identifier = "Asset 1 (res-123)"
589
+
590
+ self.mock_integration._asset_exists_in_regscale.return_value = True
591
+
592
+ result = self.processor._verify_assets_exist(mock_finding)
593
+
594
+ assert result is True
595
+ self.mock_integration._asset_exists_in_regscale.assert_called_once_with("res-123")
596
+
597
+ def test_verify_assets_exist_single_asset_not_exists(self):
598
+ """Test verification when single asset doesn't exist."""
599
+ mock_finding = MagicMock(spec=IntegrationFinding)
600
+ mock_finding.asset_identifier = "Asset 1 (res-123)"
601
+
602
+ self.mock_integration._asset_exists_in_regscale.return_value = False
603
+
604
+ result = self.processor._verify_assets_exist(mock_finding)
605
+
606
+ assert result is False
607
+
608
+ def test_verify_assets_exist_multiple_assets_all_exist(self):
609
+ """Test verification for multiple existing assets."""
610
+ mock_finding = MagicMock(spec=IntegrationFinding)
611
+ mock_finding.asset_identifier = "Asset 1 (res-1)\nAsset 2 (res-2)\nAsset 3 (res-3)"
612
+
613
+ self.mock_integration._asset_exists_in_regscale.return_value = True
614
+
615
+ result = self.processor._verify_assets_exist(mock_finding)
616
+
617
+ assert result is True
618
+ assert self.mock_integration._asset_exists_in_regscale.call_count == 3
619
+
620
+ def test_verify_assets_exist_multiple_assets_one_missing(self):
621
+ """Test verification when one asset in multiple is missing."""
622
+ mock_finding = MagicMock(spec=IntegrationFinding)
623
+ mock_finding.asset_identifier = "Asset 1 (res-1)\nAsset 2 (res-2)"
624
+
625
+ self.mock_integration._asset_exists_in_regscale.side_effect = [True, False]
626
+
627
+ result = self.processor._verify_assets_exist(mock_finding)
628
+
629
+ assert result is False
630
+
631
+ def test_verify_assets_exist_skip_empty_lines(self):
632
+ """Test verification skips empty lines in asset_identifier."""
633
+ mock_finding = MagicMock(spec=IntegrationFinding)
634
+ mock_finding.asset_identifier = "Asset 1 (res-1)\n\n\nAsset 2 (res-2)"
635
+
636
+ self.mock_integration._asset_exists_in_regscale.return_value = True
637
+
638
+ result = self.processor._verify_assets_exist(mock_finding)
639
+
640
+ assert result is True
641
+ # Should only check 2 assets, not 4
642
+ assert self.mock_integration._asset_exists_in_regscale.call_count == 2
643
+
644
+ def test_verify_assets_exist_identifier_without_parentheses(self):
645
+ """Test verification with identifier that has no parentheses."""
646
+ mock_finding = MagicMock(spec=IntegrationFinding)
647
+ mock_finding.asset_identifier = "res-simple-id"
648
+
649
+ self.mock_integration._asset_exists_in_regscale.return_value = True
650
+
651
+ result = self.processor._verify_assets_exist(mock_finding)
652
+
653
+ assert result is True
654
+ self.mock_integration._asset_exists_in_regscale.assert_called_once_with("res-simple-id")
655
+
656
+ def test_verify_assets_exist_identifier_extraction(self):
657
+ """Test proper extraction of resource ID from formatted identifier."""
658
+ mock_finding = MagicMock(spec=IntegrationFinding)
659
+ mock_finding.asset_identifier = "Complex Asset Name (res-complex-123)"
660
+
661
+ self.mock_integration._asset_exists_in_regscale.return_value = True
662
+
663
+ result = self.processor._verify_assets_exist(mock_finding)
664
+
665
+ assert result is True
666
+ self.mock_integration._asset_exists_in_regscale.assert_called_once_with("res-complex-123")
667
+
668
+
669
+ # Run tests with pytest
670
+ if __name__ == "__main__":
671
+ pytest.main([__file__, "-v"])