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

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

Potentially problematic release.


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

Files changed (95) 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/jira.py +367 -126
  6. regscale/integrations/commercial/qualys/__init__.py +7 -8
  7. regscale/integrations/commercial/qualys/scanner.py +8 -3
  8. regscale/integrations/commercial/synqly/assets.py +17 -0
  9. regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
  10. regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
  11. regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
  12. regscale/integrations/commercial/tenablev2/commands.py +142 -1
  13. regscale/integrations/commercial/tenablev2/scanner.py +0 -1
  14. regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
  15. regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
  16. regscale/integrations/commercial/wizv2/click.py +44 -59
  17. regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
  18. regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
  19. regscale/integrations/commercial/wizv2/compliance_report.py +10 -9
  20. regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
  21. regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
  22. regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
  23. regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
  24. regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
  25. regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
  26. regscale/integrations/commercial/wizv2/issue.py +1 -1
  27. regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
  28. regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
  29. regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
  30. regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
  31. regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
  32. regscale/integrations/commercial/wizv2/reports.py +1 -1
  33. regscale/integrations/commercial/wizv2/sbom.py +1 -1
  34. regscale/integrations/commercial/wizv2/scanner.py +40 -100
  35. regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
  36. regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
  37. regscale/integrations/commercial/wizv2/variables.py +89 -3
  38. regscale/integrations/compliance_integration.py +0 -46
  39. regscale/integrations/control_matcher.py +22 -3
  40. regscale/integrations/due_date_handler.py +14 -8
  41. regscale/integrations/public/fedramp/docx_parser.py +10 -1
  42. regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
  43. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  44. regscale/integrations/scanner_integration.py +127 -57
  45. regscale/models/integration_models/cisa_kev_data.json +132 -9
  46. regscale/models/integration_models/qualys.py +3 -4
  47. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  48. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
  49. regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
  50. regscale/models/regscale_models/control_implementation.py +1 -1
  51. regscale/models/regscale_models/issue.py +0 -1
  52. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/METADATA +1 -17
  53. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/RECORD +93 -60
  54. tests/regscale/integrations/commercial/test_jira.py +481 -91
  55. tests/regscale/integrations/commercial/test_wiz.py +96 -200
  56. tests/regscale/integrations/commercial/wizv2/__init__.py +1 -1
  57. tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
  58. tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
  59. tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
  60. tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
  61. tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
  62. tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
  63. tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
  64. tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
  65. tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
  66. tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
  67. tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
  68. tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
  69. tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
  70. tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
  71. tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
  72. tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
  73. tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
  74. tests/regscale/integrations/commercial/wizv2/test_issue.py +1 -1
  75. tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
  76. tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
  77. tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
  78. tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
  79. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +1 -1
  80. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +72 -29
  81. tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
  82. tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
  83. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +946 -78
  84. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +97 -202
  85. tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
  86. tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
  87. tests/regscale/integrations/public/test_fedramp.py +301 -0
  88. tests/regscale/integrations/test_control_matcher.py +83 -0
  89. regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
  90. tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +0 -750
  91. /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
  92. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/LICENSE +0 -0
  93. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/WHEEL +0 -0
  94. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/entry_points.txt +0 -0
  95. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,643 @@
1
+ """
2
+ Unit tests for WizSbomIntegration
3
+ """
4
+
5
+ import json
6
+ import logging
7
+ import sys
8
+ import unittest
9
+ from unittest.mock import patch, MagicMock, mock_open
10
+
11
+ from regscale.integrations.commercial.wizv2.sbom import WizSbomIntegration
12
+ from regscale.models.regscale_models.sbom import Sbom
13
+
14
+ logger = logging.getLogger("regscale")
15
+
16
+ PATH = "regscale.integrations.commercial.wizv2.sbom"
17
+
18
+
19
+ class TestWizSbomIntegration(unittest.TestCase):
20
+ """Test cases for WizSbomIntegration class"""
21
+
22
+ def setUp(self):
23
+ """Set up test fixtures"""
24
+ self.wiz_project_id = "test-project-123"
25
+ self.regscale_id = 100
26
+ self.regscale_module = "securityPlans"
27
+ self.client_id = "test-client-id"
28
+ self.client_secret = "test-client-secret"
29
+ self.filter_by_override = None
30
+
31
+ # Sample SBOM data from Wiz
32
+ self.sample_sbom_nodes = [
33
+ {
34
+ "id": "sbom-1",
35
+ "name": "express",
36
+ "type": {
37
+ "group": "npm",
38
+ "codeLibraryLanguage": "JavaScript",
39
+ "osPackageManager": None,
40
+ "hostedTechnology": None,
41
+ "plugin": None,
42
+ },
43
+ "versions": {
44
+ "nodes": [
45
+ {"version": "4.18.0"},
46
+ {"version": "4.18.1"},
47
+ {"version": "4.18.2"},
48
+ ]
49
+ },
50
+ },
51
+ {
52
+ "id": "sbom-2",
53
+ "name": "lodash",
54
+ "type": {
55
+ "group": "npm",
56
+ "codeLibraryLanguage": "JavaScript",
57
+ "osPackageManager": None,
58
+ "hostedTechnology": None,
59
+ "plugin": None,
60
+ },
61
+ "versions": {
62
+ "nodes": [
63
+ {"version": "4.17.21"},
64
+ ]
65
+ },
66
+ },
67
+ {
68
+ "id": "sbom-3",
69
+ "name": "pytest",
70
+ "type": {
71
+ "group": "pip",
72
+ "codeLibraryLanguage": "Python",
73
+ "osPackageManager": None,
74
+ "hostedTechnology": None,
75
+ "plugin": None,
76
+ },
77
+ "versions": {"nodes": []},
78
+ },
79
+ ]
80
+
81
+ @patch(f"{PATH}.WizMixin.__init__")
82
+ def test_init_with_project_id_from_parameter(self, mock_wiz_mixin_init):
83
+ """Test initialization with project ID passed as parameter"""
84
+ mock_wiz_mixin_init.return_value = None
85
+
86
+ # Mock the config attribute on the instance
87
+ mock_config = {"wizProjectId": "config-project-id"}
88
+
89
+ integration = WizSbomIntegration(
90
+ wiz_project_id=self.wiz_project_id,
91
+ regscale_id=self.regscale_id,
92
+ regscale_module=self.regscale_module,
93
+ filter_by_override=None,
94
+ client_id=self.client_id,
95
+ client_secret=self.client_secret,
96
+ )
97
+ integration.config = mock_config
98
+
99
+ self.assertEqual(integration.wiz_project_id, self.wiz_project_id)
100
+ self.assertEqual(integration.regscale_id, self.regscale_id)
101
+ self.assertEqual(integration.regscale_module, self.regscale_module)
102
+ self.assertEqual(integration.filter_by, None)
103
+ self.assertEqual(integration.topic_key, "sbomArtifactsGroupedByName")
104
+ self.assertIsInstance(integration.sbom_list, list)
105
+ self.assertEqual(len(integration.sbom_list), 0)
106
+
107
+ @patch(f"{PATH}.WizMixin.__init__")
108
+ def test_init_with_project_id_from_config(self, mock_wiz_mixin_init):
109
+ """Test initialization with project ID from config when parameter is None"""
110
+ mock_wiz_mixin_init.return_value = None
111
+
112
+ # Create the instance first and manually set config before checking wiz_project_id
113
+ integration = WizSbomIntegration.__new__(WizSbomIntegration)
114
+ integration.config = {"wizProjectId": "config-project-id"}
115
+
116
+ # Now call __init__ with None for wiz_project_id
117
+ WizSbomIntegration.__init__(
118
+ integration,
119
+ wiz_project_id=None,
120
+ regscale_id=self.regscale_id,
121
+ regscale_module=self.regscale_module,
122
+ filter_by_override=None,
123
+ client_id=self.client_id,
124
+ client_secret=self.client_secret,
125
+ )
126
+
127
+ # Should pick up project ID from config
128
+ self.assertEqual(integration.wiz_project_id, "config-project-id")
129
+
130
+ @patch(f"{PATH}.error_and_exit")
131
+ @patch(f"{PATH}.WizMixin.__init__")
132
+ def test_init_without_project_id_exits(self, mock_wiz_mixin_init, mock_error_exit):
133
+ """Test initialization without project ID calls error_and_exit"""
134
+ mock_wiz_mixin_init.return_value = None
135
+ mock_error_exit.side_effect = SystemExit(1)
136
+
137
+ # Create an instance and manually set config to empty dict
138
+ with self.assertRaises(SystemExit):
139
+ integration = WizSbomIntegration.__new__(WizSbomIntegration)
140
+ integration.config = {}
141
+ # Call __init__ manually to trigger the error check
142
+ WizSbomIntegration.__init__(
143
+ integration,
144
+ wiz_project_id=None,
145
+ regscale_id=self.regscale_id,
146
+ regscale_module=self.regscale_module,
147
+ filter_by_override=None,
148
+ client_id=self.client_id,
149
+ client_secret=self.client_secret,
150
+ )
151
+
152
+ mock_error_exit.assert_called_once_with("Wiz project ID not provided")
153
+
154
+ @patch(f"{PATH}.WizMixin.__init__")
155
+ def test_init_with_filter_by_override(self, mock_wiz_mixin_init):
156
+ """Test initialization with filterBy override"""
157
+ mock_wiz_mixin_init.return_value = None
158
+ filter_by_json = '{"project": "override-project", "custom": "filter"}'
159
+
160
+ # Create the instance first and manually set config
161
+ integration = WizSbomIntegration.__new__(WizSbomIntegration)
162
+ integration.config = {"wizProjectId": self.wiz_project_id}
163
+
164
+ # Now call __init__ with filter_by_override
165
+ WizSbomIntegration.__init__(
166
+ integration,
167
+ wiz_project_id=self.wiz_project_id,
168
+ regscale_id=self.regscale_id,
169
+ regscale_module=self.regscale_module,
170
+ filter_by_override=filter_by_json,
171
+ client_id=self.client_id,
172
+ client_secret=self.client_secret,
173
+ )
174
+
175
+ expected_filter = {"project": "override-project", "custom": "filter"}
176
+ self.assertEqual(integration.filter_by, expected_filter)
177
+ # Verify variables use the override filter
178
+ self.assertEqual(integration.variables["filterBy"], expected_filter)
179
+
180
+ @patch(f"{PATH}.Sbom.get_all_by_parent")
181
+ @patch(f"{PATH}.WizSbomIntegration.fetch_sbom_data")
182
+ @patch(f"{PATH}.WizMixin.__init__")
183
+ def test_run_with_no_sbom_data(self, mock_wiz_mixin_init, mock_fetch_sbom, mock_get_all):
184
+ """Test run method when no SBOM data is found"""
185
+ mock_wiz_mixin_init.return_value = None
186
+
187
+ with patch.object(WizSbomIntegration, "config", {"wizProjectId": self.wiz_project_id}):
188
+ integration = WizSbomIntegration(
189
+ wiz_project_id=self.wiz_project_id,
190
+ regscale_id=self.regscale_id,
191
+ regscale_module=self.regscale_module,
192
+ filter_by_override=None,
193
+ client_id=self.client_id,
194
+ client_secret=self.client_secret,
195
+ )
196
+
197
+ # Mock fetch_sbom_data to set empty list
198
+ def set_empty_list():
199
+ integration.sbom_list = []
200
+
201
+ mock_fetch_sbom.side_effect = set_empty_list
202
+
203
+ with self.assertRaises(SystemExit) as cm:
204
+ integration.run()
205
+
206
+ self.assertEqual(cm.exception.code, 0)
207
+ mock_fetch_sbom.assert_called_once()
208
+ mock_get_all.assert_not_called()
209
+
210
+ @patch(f"{PATH}.Sbom.get_all_by_parent")
211
+ @patch(f"{PATH}.WizSbomIntegration.fetch_sbom_data")
212
+ @patch(f"{PATH}.WizMixin.__init__")
213
+ def test_run_creates_new_sboms(self, mock_wiz_mixin_init, mock_fetch_sbom, mock_get_all):
214
+ """Test run method creates new SBOMs when they don't exist"""
215
+ mock_wiz_mixin_init.return_value = None
216
+
217
+ with patch.object(WizSbomIntegration, "config", {"wizProjectId": self.wiz_project_id}):
218
+ integration = WizSbomIntegration(
219
+ wiz_project_id=self.wiz_project_id,
220
+ regscale_id=self.regscale_id,
221
+ regscale_module=self.regscale_module,
222
+ filter_by_override=None,
223
+ client_id=self.client_id,
224
+ client_secret=self.client_secret,
225
+ )
226
+
227
+ # Create mock SBOM objects
228
+ mock_sbom1 = MagicMock(spec=Sbom)
229
+ mock_sbom1.name = "express"
230
+ mock_sbom1.create = MagicMock()
231
+
232
+ mock_sbom2 = MagicMock(spec=Sbom)
233
+ mock_sbom2.name = "lodash"
234
+ mock_sbom2.create = MagicMock()
235
+
236
+ # Mock fetch_sbom_data to populate sbom_list
237
+ def populate_sbom_list():
238
+ integration.sbom_list = [mock_sbom1, mock_sbom2]
239
+
240
+ mock_fetch_sbom.side_effect = populate_sbom_list
241
+
242
+ # Mock get_all_by_parent to return empty list (no existing SBOMs)
243
+ mock_get_all.return_value = []
244
+
245
+ integration.run()
246
+
247
+ mock_fetch_sbom.assert_called_once()
248
+ mock_get_all.assert_called_once_with(parent_id=self.regscale_id, parent_module=self.regscale_module)
249
+ mock_sbom1.create.assert_called_once()
250
+ mock_sbom2.create.assert_called_once()
251
+
252
+ @patch(f"{PATH}.Sbom.get_all_by_parent")
253
+ @patch(f"{PATH}.WizSbomIntegration.fetch_sbom_data")
254
+ @patch(f"{PATH}.WizMixin.__init__")
255
+ def test_run_skips_existing_sboms(self, mock_wiz_mixin_init, mock_fetch_sbom, mock_get_all):
256
+ """Test run method skips SBOMs that already exist"""
257
+ mock_wiz_mixin_init.return_value = None
258
+
259
+ with patch.object(WizSbomIntegration, "config", {"wizProjectId": self.wiz_project_id}):
260
+ integration = WizSbomIntegration(
261
+ wiz_project_id=self.wiz_project_id,
262
+ regscale_id=self.regscale_id,
263
+ regscale_module=self.regscale_module,
264
+ filter_by_override=None,
265
+ client_id=self.client_id,
266
+ client_secret=self.client_secret,
267
+ )
268
+
269
+ # Create mock SBOM objects
270
+ mock_sbom1 = MagicMock(spec=Sbom)
271
+ mock_sbom1.name = "express"
272
+ mock_sbom1.create = MagicMock()
273
+
274
+ mock_sbom2 = MagicMock(spec=Sbom)
275
+ mock_sbom2.name = "lodash"
276
+ mock_sbom2.create = MagicMock()
277
+
278
+ # Mock fetch_sbom_data to populate sbom_list
279
+ def populate_sbom_list():
280
+ integration.sbom_list = [mock_sbom1, mock_sbom2]
281
+
282
+ mock_fetch_sbom.side_effect = populate_sbom_list
283
+
284
+ # Mock existing SBOM with name "express"
285
+ existing_sbom = MagicMock(spec=Sbom)
286
+ existing_sbom.name = "express"
287
+ mock_get_all.return_value = [existing_sbom]
288
+
289
+ integration.run()
290
+
291
+ mock_fetch_sbom.assert_called_once()
292
+ mock_get_all.assert_called_once_with(parent_id=self.regscale_id, parent_module=self.regscale_module)
293
+ # express should not be created (already exists)
294
+ mock_sbom1.create.assert_not_called()
295
+ # lodash should be created (doesn't exist)
296
+ mock_sbom2.create.assert_called_once()
297
+
298
+ @patch(f"{PATH}.WizSbomIntegration.map_sbom_data")
299
+ @patch(f"{PATH}.WizSbomIntegration.fetch_data_if_needed")
300
+ @patch(f"{PATH}.WizMixin.__init__")
301
+ def test_fetch_sbom_data(self, mock_wiz_mixin_init, mock_fetch_data, mock_map_sbom):
302
+ """Test fetch_sbom_data method"""
303
+ mock_wiz_mixin_init.return_value = None
304
+
305
+ with patch.object(
306
+ WizSbomIntegration, "config", {"wizProjectId": self.wiz_project_id, "wizFullPullLimitHours": 8}
307
+ ):
308
+ integration = WizSbomIntegration(
309
+ wiz_project_id=self.wiz_project_id,
310
+ regscale_id=self.regscale_id,
311
+ regscale_module=self.regscale_module,
312
+ filter_by_override=None,
313
+ client_id=self.client_id,
314
+ client_secret=self.client_secret,
315
+ )
316
+
317
+ mock_fetch_data.return_value = self.sample_sbom_nodes
318
+
319
+ integration.fetch_sbom_data()
320
+
321
+ # Verify fetch_data_if_needed was called with correct parameters
322
+ from regscale.integrations.commercial.wizv2.core.constants import SBOM_QUERY, SBOM_FILE_PATH
323
+
324
+ mock_fetch_data.assert_called_once_with(
325
+ file_path=SBOM_FILE_PATH,
326
+ query=SBOM_QUERY,
327
+ topic_key="sbomArtifactsGroupedByName",
328
+ interval_hours=8,
329
+ variables={
330
+ "first": 200,
331
+ "filterBy": {"project": self.wiz_project_id},
332
+ "orderBy": {"field": "NAME", "direction": "ASC"},
333
+ },
334
+ )
335
+
336
+ # Verify map_sbom_data was called with the nodes
337
+ mock_map_sbom.assert_called_once_with(self.sample_sbom_nodes)
338
+
339
+ @patch(f"{PATH}.WizSbomIntegration.map_sbom_data")
340
+ @patch(f"{PATH}.WizSbomIntegration.fetch_data_if_needed")
341
+ @patch(f"{PATH}.WizMixin.__init__")
342
+ def test_fetch_sbom_data_with_custom_interval(self, mock_wiz_mixin_init, mock_fetch_data, mock_map_sbom):
343
+ """Test fetch_sbom_data with custom interval hours from config"""
344
+ mock_wiz_mixin_init.return_value = None
345
+
346
+ with patch.object(
347
+ WizSbomIntegration, "config", {"wizProjectId": self.wiz_project_id, "wizFullPullLimitHours": 12}
348
+ ):
349
+ integration = WizSbomIntegration(
350
+ wiz_project_id=self.wiz_project_id,
351
+ regscale_id=self.regscale_id,
352
+ regscale_module=self.regscale_module,
353
+ filter_by_override=None,
354
+ client_id=self.client_id,
355
+ client_secret=self.client_secret,
356
+ )
357
+
358
+ mock_fetch_data.return_value = self.sample_sbom_nodes
359
+
360
+ integration.fetch_sbom_data()
361
+
362
+ # Verify interval_hours was set to custom value
363
+ call_kwargs = mock_fetch_data.call_args[1]
364
+ self.assertEqual(call_kwargs["interval_hours"], 12)
365
+
366
+ @patch("regscale.models.regscale_models.sbom.RegScaleModel.get_user_id")
367
+ @patch(f"{PATH}.WizMixin.__init__")
368
+ def test_map_sbom_data(self, mock_wiz_mixin_init, mock_get_user_id):
369
+ """Test map_sbom_data method with valid data"""
370
+ mock_wiz_mixin_init.return_value = None
371
+ mock_get_user_id.return_value = "test-user-id"
372
+
373
+ with patch.object(WizSbomIntegration, "config", {"wizProjectId": self.wiz_project_id}):
374
+ integration = WizSbomIntegration(
375
+ wiz_project_id=self.wiz_project_id,
376
+ regscale_id=self.regscale_id,
377
+ regscale_module=self.regscale_module,
378
+ filter_by_override=None,
379
+ client_id=self.client_id,
380
+ client_secret=self.client_secret,
381
+ )
382
+
383
+ integration.map_sbom_data(self.sample_sbom_nodes)
384
+
385
+ # Verify SBOM objects were created
386
+ self.assertEqual(len(integration.sbom_list), 3)
387
+
388
+ # Check first SBOM
389
+ sbom1 = integration.sbom_list[0]
390
+ self.assertEqual(sbom1.name, "express")
391
+ self.assertEqual(sbom1.sbomStandard, "JavaScript")
392
+ self.assertEqual(sbom1.standardVersion, "4.18.0,4.18.1,4.18.2")
393
+ self.assertEqual(sbom1.tool, "npm")
394
+ self.assertEqual(sbom1.parentId, self.regscale_id)
395
+ self.assertEqual(sbom1.parentModule, self.regscale_module)
396
+ self.assertIsNotNone(sbom1.results)
397
+ self.assertIsInstance(json.loads(sbom1.results), dict)
398
+
399
+ # Check second SBOM
400
+ sbom2 = integration.sbom_list[1]
401
+ self.assertEqual(sbom2.name, "lodash")
402
+ self.assertEqual(sbom2.sbomStandard, "JavaScript")
403
+ self.assertEqual(sbom2.standardVersion, "4.17.21")
404
+ self.assertEqual(sbom2.tool, "npm")
405
+
406
+ # Check third SBOM (with no versions)
407
+ sbom3 = integration.sbom_list[2]
408
+ self.assertEqual(sbom3.name, "pytest")
409
+ self.assertEqual(sbom3.sbomStandard, "Python")
410
+ self.assertEqual(sbom3.standardVersion, "")
411
+ self.assertEqual(sbom3.tool, "pip")
412
+
413
+ @patch("regscale.models.regscale_models.sbom.RegScaleModel.get_user_id")
414
+ @patch(f"{PATH}.WizMixin.__init__")
415
+ def test_map_sbom_data_with_missing_type_fields(self, mock_wiz_mixin_init, mock_get_user_id):
416
+ """Test map_sbom_data with missing type fields falls back to name"""
417
+ mock_wiz_mixin_init.return_value = None
418
+ mock_get_user_id.return_value = "test-user-id"
419
+
420
+ integration = WizSbomIntegration(
421
+ wiz_project_id=self.wiz_project_id,
422
+ regscale_id=self.regscale_id,
423
+ regscale_module=self.regscale_module,
424
+ filter_by_override=None,
425
+ client_id=self.client_id,
426
+ client_secret=self.client_secret,
427
+ )
428
+ integration.config = {"wizProjectId": self.wiz_project_id}
429
+ # Clear any existing SBOM data
430
+ integration.sbom_list = []
431
+
432
+ # SBOM data with missing type fields
433
+ nodes_with_missing_fields = [
434
+ {
435
+ "id": "sbom-4",
436
+ "name": "unknown-package",
437
+ "type": {
438
+ "group": None,
439
+ "codeLibraryLanguage": None,
440
+ "osPackageManager": None,
441
+ "hostedTechnology": None,
442
+ "plugin": None,
443
+ },
444
+ "versions": {"nodes": [{"version": "1.0.0"}]},
445
+ }
446
+ ]
447
+
448
+ integration.map_sbom_data(nodes_with_missing_fields)
449
+
450
+ self.assertEqual(len(integration.sbom_list), 1)
451
+ sbom = integration.sbom_list[0]
452
+ # When codeLibraryLanguage is None, it should fall back to name
453
+ self.assertEqual(sbom.sbomStandard, "unknown-package")
454
+ self.assertEqual(sbom.tool, None)
455
+
456
+ @patch("regscale.models.regscale_models.sbom.RegScaleModel.get_user_id")
457
+ @patch(f"{PATH}.WizMixin.__init__")
458
+ def test_map_sbom_data_with_empty_versions(self, mock_wiz_mixin_init, mock_get_user_id):
459
+ """Test map_sbom_data handles empty versions gracefully"""
460
+ mock_wiz_mixin_init.return_value = None
461
+ mock_get_user_id.return_value = "test-user-id"
462
+
463
+ integration = WizSbomIntegration(
464
+ wiz_project_id=self.wiz_project_id,
465
+ regscale_id=self.regscale_id,
466
+ regscale_module=self.regscale_module,
467
+ filter_by_override=None,
468
+ client_id=self.client_id,
469
+ client_secret=self.client_secret,
470
+ )
471
+ integration.config = {"wizProjectId": self.wiz_project_id}
472
+ # Clear any existing SBOM data
473
+ integration.sbom_list = []
474
+
475
+ nodes_with_no_versions = [
476
+ {
477
+ "id": "sbom-5",
478
+ "name": "no-version-package",
479
+ "type": {
480
+ "group": "maven",
481
+ "codeLibraryLanguage": "Java",
482
+ "osPackageManager": None,
483
+ "hostedTechnology": None,
484
+ "plugin": None,
485
+ },
486
+ "versions": {"nodes": []},
487
+ }
488
+ ]
489
+
490
+ integration.map_sbom_data(nodes_with_no_versions)
491
+
492
+ self.assertEqual(len(integration.sbom_list), 1)
493
+ sbom = integration.sbom_list[0]
494
+ self.assertEqual(sbom.standardVersion, "")
495
+
496
+ @patch("regscale.models.regscale_models.sbom.RegScaleModel.get_user_id")
497
+ @patch(f"{PATH}.WizMixin.__init__")
498
+ def test_map_sbom_data_preserves_json_structure(self, mock_wiz_mixin_init, mock_get_user_id):
499
+ """Test that map_sbom_data preserves the original JSON in results field"""
500
+ mock_wiz_mixin_init.return_value = None
501
+ mock_get_user_id.return_value = "test-user-id"
502
+
503
+ with patch.object(WizSbomIntegration, "config", {"wizProjectId": self.wiz_project_id}):
504
+ integration = WizSbomIntegration(
505
+ wiz_project_id=self.wiz_project_id,
506
+ regscale_id=self.regscale_id,
507
+ regscale_module=self.regscale_module,
508
+ filter_by_override=None,
509
+ client_id=self.client_id,
510
+ client_secret=self.client_secret,
511
+ )
512
+
513
+ integration.map_sbom_data(self.sample_sbom_nodes)
514
+
515
+ # Verify the results field contains valid JSON
516
+ sbom = integration.sbom_list[0]
517
+ results_data = json.loads(sbom.results)
518
+
519
+ self.assertEqual(results_data["id"], "sbom-1")
520
+ self.assertEqual(results_data["name"], "express")
521
+ self.assertIn("type", results_data)
522
+ self.assertIn("versions", results_data)
523
+
524
+ @patch(f"{PATH}.WizMixin.__init__")
525
+ def test_map_sbom_data_with_none_values(self, mock_wiz_mixin_init):
526
+ """Test map_sbom_data with None values raises TypeError due to get_value returning None"""
527
+ mock_wiz_mixin_init.return_value = None
528
+
529
+ integration = WizSbomIntegration(
530
+ wiz_project_id=self.wiz_project_id,
531
+ regscale_id=self.regscale_id,
532
+ regscale_module=self.regscale_module,
533
+ filter_by_override=None,
534
+ client_id=self.client_id,
535
+ client_secret=self.client_secret,
536
+ )
537
+ integration.config = {"wizProjectId": self.wiz_project_id}
538
+ # Clear any existing SBOM data
539
+ integration.sbom_list = []
540
+
541
+ nodes_with_nones = [
542
+ {
543
+ "id": "sbom-6",
544
+ "name": "test-package",
545
+ "type": None,
546
+ "versions": None,
547
+ }
548
+ ]
549
+
550
+ # This should raise a TypeError since get_value returns None for versions.nodes
551
+ # and we try to iterate over it
552
+ with self.assertRaises(TypeError) as context:
553
+ integration.map_sbom_data(nodes_with_nones)
554
+
555
+ self.assertIn("'NoneType' object is not iterable", str(context.exception))
556
+
557
+ @patch(f"{PATH}.WizMixin.__init__")
558
+ def test_variables_structure(self, mock_wiz_mixin_init):
559
+ """Test that variables are correctly structured for GraphQL query"""
560
+ mock_wiz_mixin_init.return_value = None
561
+
562
+ with patch.object(WizSbomIntegration, "config", {"wizProjectId": self.wiz_project_id}):
563
+ integration = WizSbomIntegration(
564
+ wiz_project_id=self.wiz_project_id,
565
+ regscale_id=self.regscale_id,
566
+ regscale_module=self.regscale_module,
567
+ filter_by_override=None,
568
+ client_id=self.client_id,
569
+ client_secret=self.client_secret,
570
+ )
571
+
572
+ expected_variables = {
573
+ "first": 200,
574
+ "filterBy": {"project": self.wiz_project_id},
575
+ "orderBy": {"field": "NAME", "direction": "ASC"},
576
+ }
577
+
578
+ self.assertEqual(integration.variables, expected_variables)
579
+
580
+ @patch(f"{PATH}.WizMixin.__init__")
581
+ def test_filter_by_override_logging(self, mock_wiz_mixin_init):
582
+ """Test that filterBy override is applied correctly"""
583
+ mock_wiz_mixin_init.return_value = None
584
+ filter_by_json = '{"custom": "filter"}'
585
+
586
+ # Create the instance first and manually set config
587
+ integration = WizSbomIntegration.__new__(WizSbomIntegration)
588
+ integration.config = {"wizProjectId": self.wiz_project_id}
589
+
590
+ # Now call __init__ with filter_by_override
591
+ WizSbomIntegration.__init__(
592
+ integration,
593
+ wiz_project_id=self.wiz_project_id,
594
+ regscale_id=self.regscale_id,
595
+ regscale_module=self.regscale_module,
596
+ filter_by_override=filter_by_json,
597
+ client_id=self.client_id,
598
+ client_secret=self.client_secret,
599
+ )
600
+
601
+ # Verify that filter_by was set correctly
602
+ expected_filter = {"custom": "filter"}
603
+ self.assertEqual(integration.filter_by, expected_filter)
604
+ self.assertEqual(integration.variables["filterBy"], expected_filter)
605
+
606
+ @patch(f"{PATH}.Sbom")
607
+ @patch(f"{PATH}.WizMixin.__init__")
608
+ def test_map_sbom_data_creates_sbom_instances(self, mock_wiz_mixin_init, mock_sbom_class):
609
+ """Test that map_sbom_data creates Sbom model instances with correct parameters"""
610
+ mock_wiz_mixin_init.return_value = None
611
+
612
+ with patch.object(WizSbomIntegration, "config", {"wizProjectId": self.wiz_project_id}):
613
+ integration = WizSbomIntegration(
614
+ wiz_project_id=self.wiz_project_id,
615
+ regscale_id=self.regscale_id,
616
+ regscale_module=self.regscale_module,
617
+ filter_by_override=None,
618
+ client_id=self.client_id,
619
+ client_secret=self.client_secret,
620
+ )
621
+
622
+ # Create a simple mock for Sbom that returns itself
623
+ mock_sbom_instance = MagicMock(spec=Sbom)
624
+ mock_sbom_class.return_value = mock_sbom_instance
625
+
626
+ single_node = [self.sample_sbom_nodes[0]]
627
+ integration.map_sbom_data(single_node)
628
+
629
+ # Verify Sbom was instantiated with correct parameters
630
+ mock_sbom_class.assert_called_once()
631
+ call_kwargs = mock_sbom_class.call_args[1]
632
+
633
+ self.assertEqual(call_kwargs["name"], "express")
634
+ self.assertEqual(call_kwargs["sbomStandard"], "JavaScript")
635
+ self.assertEqual(call_kwargs["standardVersion"], "4.18.0,4.18.1,4.18.2")
636
+ self.assertEqual(call_kwargs["tool"], "npm")
637
+ self.assertEqual(call_kwargs["parentId"], self.regscale_id)
638
+ self.assertEqual(call_kwargs["parentModule"], self.regscale_module)
639
+ self.assertIsNotNone(call_kwargs["results"])
640
+
641
+
642
+ if __name__ == "__main__":
643
+ unittest.main()