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.
- regscale/_version.py +1 -1
- regscale/core/app/application.py +1 -1
- regscale/core/app/internal/evidence.py +419 -2
- regscale/dev/code_gen.py +24 -20
- regscale/integrations/commercial/jira.py +367 -126
- regscale/integrations/commercial/qualys/__init__.py +7 -8
- regscale/integrations/commercial/qualys/scanner.py +8 -3
- regscale/integrations/commercial/synqly/assets.py +17 -0
- regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
- regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
- regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
- regscale/integrations/commercial/tenablev2/commands.py +142 -1
- regscale/integrations/commercial/tenablev2/scanner.py +0 -1
- regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
- regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
- regscale/integrations/commercial/wizv2/click.py +44 -59
- regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
- regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
- regscale/integrations/commercial/wizv2/compliance_report.py +10 -9
- regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
- regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
- regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
- regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
- regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
- regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
- regscale/integrations/commercial/wizv2/issue.py +1 -1
- regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
- regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
- regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
- regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
- regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
- regscale/integrations/commercial/wizv2/reports.py +1 -1
- regscale/integrations/commercial/wizv2/sbom.py +1 -1
- regscale/integrations/commercial/wizv2/scanner.py +40 -100
- regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
- regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
- regscale/integrations/commercial/wizv2/variables.py +89 -3
- regscale/integrations/compliance_integration.py +0 -46
- regscale/integrations/control_matcher.py +22 -3
- regscale/integrations/due_date_handler.py +14 -8
- regscale/integrations/public/fedramp/docx_parser.py +10 -1
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
- regscale/integrations/public/fedramp/fedramp_five.py +1 -1
- regscale/integrations/scanner_integration.py +127 -57
- regscale/models/integration_models/cisa_kev_data.json +132 -9
- regscale/models/integration_models/qualys.py +3 -4
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
- regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
- regscale/models/regscale_models/control_implementation.py +1 -1
- regscale/models/regscale_models/issue.py +0 -1
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/METADATA +1 -17
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/RECORD +93 -60
- tests/regscale/integrations/commercial/test_jira.py +481 -91
- tests/regscale/integrations/commercial/test_wiz.py +96 -200
- tests/regscale/integrations/commercial/wizv2/__init__.py +1 -1
- tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
- tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
- tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
- tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
- tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
- tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
- tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
- tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
- tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
- tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
- tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
- tests/regscale/integrations/commercial/wizv2/test_issue.py +1 -1
- tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
- tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
- tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
- tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +1 -1
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +72 -29
- tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +946 -78
- tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +97 -202
- tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
- tests/regscale/integrations/public/test_fedramp.py +301 -0
- tests/regscale/integrations/test_control_matcher.py +83 -0
- regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
- tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +0 -750
- /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Comprehensive unit tests for WizDataMixin module.
|
|
5
|
+
|
|
6
|
+
Tests cover:
|
|
7
|
+
- fetch_data_if_needed with various file states (exists/not exists, fresh/stale)
|
|
8
|
+
- write_to_file functionality
|
|
9
|
+
- load_file functionality
|
|
10
|
+
- fetch_data with GraphQL client mocking
|
|
11
|
+
- Error handling and edge cases
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import tempfile
|
|
17
|
+
from datetime import datetime, timedelta
|
|
18
|
+
from unittest.mock import MagicMock, patch, mock_open
|
|
19
|
+
|
|
20
|
+
import pytest
|
|
21
|
+
|
|
22
|
+
from regscale.integrations.commercial.wizv2.WizDataMixin import WizMixin
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestWizMixinFetchDataIfNeeded:
|
|
26
|
+
"""Test fetch_data_if_needed method with various scenarios."""
|
|
27
|
+
|
|
28
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.load_file")
|
|
29
|
+
@patch("os.path.exists")
|
|
30
|
+
@patch("os.path.getmtime")
|
|
31
|
+
def test_fetch_data_if_needed_uses_fresh_cache(self, mock_getmtime, mock_exists, mock_load_file):
|
|
32
|
+
"""Test that fetch_data_if_needed uses cache when file is fresh."""
|
|
33
|
+
# Setup
|
|
34
|
+
wiz = WizMixin()
|
|
35
|
+
file_path = "artifacts/test_data.json"
|
|
36
|
+
cached_data = [{"id": "1", "name": "cached"}]
|
|
37
|
+
|
|
38
|
+
# Mock file exists and is fresh (modified 1 hour ago, interval is 2 hours)
|
|
39
|
+
mock_exists.return_value = True
|
|
40
|
+
current_time = datetime.now()
|
|
41
|
+
file_mod_time = current_time - timedelta(hours=1)
|
|
42
|
+
mock_getmtime.return_value = file_mod_time.timestamp()
|
|
43
|
+
mock_load_file.return_value = cached_data
|
|
44
|
+
|
|
45
|
+
# Execute
|
|
46
|
+
result = wiz.fetch_data_if_needed(
|
|
47
|
+
file_path=file_path, query="query {}", topic_key="test", interval_hours=2, variables=None
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Verify
|
|
51
|
+
assert result == cached_data
|
|
52
|
+
mock_load_file.assert_called_once_with(file_path)
|
|
53
|
+
|
|
54
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.write_to_file")
|
|
55
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.fetch_data")
|
|
56
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.load_file")
|
|
57
|
+
@patch("os.path.exists")
|
|
58
|
+
@patch("os.path.getmtime")
|
|
59
|
+
def test_fetch_data_if_needed_fetches_when_cache_stale(
|
|
60
|
+
self, mock_getmtime, mock_exists, mock_load_file, mock_fetch_data, mock_write_to_file
|
|
61
|
+
):
|
|
62
|
+
"""Test that fetch_data_if_needed fetches new data when cache is stale."""
|
|
63
|
+
# Setup
|
|
64
|
+
wiz = WizMixin()
|
|
65
|
+
file_path = "artifacts/test_data.json"
|
|
66
|
+
fresh_data = [{"id": "2", "name": "fresh"}]
|
|
67
|
+
|
|
68
|
+
# Mock file exists but is stale (modified 3 hours ago, interval is 2 hours)
|
|
69
|
+
mock_exists.return_value = True
|
|
70
|
+
current_time = datetime.now()
|
|
71
|
+
file_mod_time = current_time - timedelta(hours=3)
|
|
72
|
+
mock_getmtime.return_value = file_mod_time.timestamp()
|
|
73
|
+
mock_fetch_data.return_value = fresh_data
|
|
74
|
+
|
|
75
|
+
# Execute
|
|
76
|
+
result = wiz.fetch_data_if_needed(
|
|
77
|
+
file_path=file_path, query="query {}", topic_key="test", interval_hours=2, variables=None
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Verify
|
|
81
|
+
assert result == fresh_data
|
|
82
|
+
mock_fetch_data.assert_called_once_with("query {}", "test", None)
|
|
83
|
+
mock_write_to_file.assert_called_once_with(file_path, fresh_data)
|
|
84
|
+
# Should not load the stale file
|
|
85
|
+
mock_load_file.assert_not_called()
|
|
86
|
+
|
|
87
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.write_to_file")
|
|
88
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.fetch_data")
|
|
89
|
+
@patch("os.path.exists")
|
|
90
|
+
def test_fetch_data_if_needed_fetches_when_no_cache(self, mock_exists, mock_fetch_data, mock_write_to_file):
|
|
91
|
+
"""Test that fetch_data_if_needed fetches data when cache doesn't exist."""
|
|
92
|
+
# Setup
|
|
93
|
+
wiz = WizMixin()
|
|
94
|
+
file_path = "artifacts/test_data.json"
|
|
95
|
+
fresh_data = [{"id": "3", "name": "new"}]
|
|
96
|
+
|
|
97
|
+
# Mock file doesn't exist
|
|
98
|
+
mock_exists.return_value = False
|
|
99
|
+
mock_fetch_data.return_value = fresh_data
|
|
100
|
+
|
|
101
|
+
# Execute
|
|
102
|
+
result = wiz.fetch_data_if_needed(
|
|
103
|
+
file_path=file_path, query="query {}", topic_key="test", interval_hours=2, variables={"key": "value"}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Verify
|
|
107
|
+
assert result == fresh_data
|
|
108
|
+
mock_fetch_data.assert_called_once_with("query {}", "test", {"key": "value"})
|
|
109
|
+
mock_write_to_file.assert_called_once_with(file_path, fresh_data)
|
|
110
|
+
|
|
111
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.load_file")
|
|
112
|
+
@patch("os.path.exists")
|
|
113
|
+
@patch("os.path.getmtime")
|
|
114
|
+
def test_fetch_data_if_needed_with_custom_interval(self, mock_getmtime, mock_exists, mock_load_file):
|
|
115
|
+
"""Test fetch_data_if_needed respects custom interval hours."""
|
|
116
|
+
# Setup
|
|
117
|
+
wiz = WizMixin()
|
|
118
|
+
file_path = "artifacts/test_data.json"
|
|
119
|
+
cached_data = [{"id": "4", "name": "cached"}]
|
|
120
|
+
|
|
121
|
+
# Mock file exists and is fresh with custom 24 hour interval
|
|
122
|
+
mock_exists.return_value = True
|
|
123
|
+
current_time = datetime.now()
|
|
124
|
+
file_mod_time = current_time - timedelta(hours=12)
|
|
125
|
+
mock_getmtime.return_value = file_mod_time.timestamp()
|
|
126
|
+
mock_load_file.return_value = cached_data
|
|
127
|
+
|
|
128
|
+
# Execute with 24 hour interval - should use cache
|
|
129
|
+
result = wiz.fetch_data_if_needed(
|
|
130
|
+
file_path=file_path, query="query {}", topic_key="test", interval_hours=24, variables=None
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Verify
|
|
134
|
+
assert result == cached_data
|
|
135
|
+
mock_load_file.assert_called_once_with(file_path)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class TestWizMixinWriteToFile:
|
|
139
|
+
"""Test write_to_file static method."""
|
|
140
|
+
|
|
141
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.check_file_path")
|
|
142
|
+
def test_write_to_file_creates_json(self, mock_check_file_path):
|
|
143
|
+
"""Test write_to_file creates valid JSON file."""
|
|
144
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
145
|
+
file_path = os.path.join(tmpdir, "test.json")
|
|
146
|
+
test_data = [{"id": "1", "name": "test"}, {"id": "2", "name": "test2"}]
|
|
147
|
+
|
|
148
|
+
# Execute
|
|
149
|
+
WizMixin.write_to_file(file_path, test_data)
|
|
150
|
+
|
|
151
|
+
# Verify
|
|
152
|
+
mock_check_file_path.assert_called_once_with("artifacts")
|
|
153
|
+
assert os.path.exists(file_path)
|
|
154
|
+
with open(file_path, "r") as f:
|
|
155
|
+
loaded_data = json.load(f)
|
|
156
|
+
assert loaded_data == test_data
|
|
157
|
+
|
|
158
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.check_file_path")
|
|
159
|
+
def test_write_to_file_empty_list(self, mock_check_file_path):
|
|
160
|
+
"""Test write_to_file handles empty list."""
|
|
161
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
162
|
+
file_path = os.path.join(tmpdir, "empty.json")
|
|
163
|
+
test_data = []
|
|
164
|
+
|
|
165
|
+
# Execute
|
|
166
|
+
WizMixin.write_to_file(file_path, test_data)
|
|
167
|
+
|
|
168
|
+
# Verify
|
|
169
|
+
assert os.path.exists(file_path)
|
|
170
|
+
with open(file_path, "r") as f:
|
|
171
|
+
loaded_data = json.load(f)
|
|
172
|
+
assert loaded_data == []
|
|
173
|
+
|
|
174
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.check_file_path")
|
|
175
|
+
def test_write_to_file_complex_data(self, mock_check_file_path):
|
|
176
|
+
"""Test write_to_file handles complex nested data structures."""
|
|
177
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
178
|
+
file_path = os.path.join(tmpdir, "complex.json")
|
|
179
|
+
test_data = [
|
|
180
|
+
{"id": "1", "nested": {"key": "value"}, "list": [1, 2, 3]},
|
|
181
|
+
{"id": "2", "nested": {"key2": "value2"}, "list": [4, 5, 6]},
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
# Execute
|
|
185
|
+
WizMixin.write_to_file(file_path, test_data)
|
|
186
|
+
|
|
187
|
+
# Verify
|
|
188
|
+
assert os.path.exists(file_path)
|
|
189
|
+
with open(file_path, "r") as f:
|
|
190
|
+
loaded_data = json.load(f)
|
|
191
|
+
assert loaded_data == test_data
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class TestWizMixinLoadFile:
|
|
195
|
+
"""Test load_file static method."""
|
|
196
|
+
|
|
197
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.check_file_path")
|
|
198
|
+
def test_load_file_loads_json(self, mock_check_file_path):
|
|
199
|
+
"""Test load_file successfully loads JSON data."""
|
|
200
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
201
|
+
file_path = os.path.join(tmpdir, "test.json")
|
|
202
|
+
test_data = [{"id": "1", "name": "test"}]
|
|
203
|
+
|
|
204
|
+
# Create test file
|
|
205
|
+
with open(file_path, "w") as f:
|
|
206
|
+
json.dump(test_data, f)
|
|
207
|
+
|
|
208
|
+
# Execute
|
|
209
|
+
result = WizMixin.load_file(file_path)
|
|
210
|
+
|
|
211
|
+
# Verify
|
|
212
|
+
mock_check_file_path.assert_called_once_with("artifacts")
|
|
213
|
+
assert result == test_data
|
|
214
|
+
|
|
215
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.check_file_path")
|
|
216
|
+
def test_load_file_empty_json(self, mock_check_file_path):
|
|
217
|
+
"""Test load_file handles empty JSON array."""
|
|
218
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
219
|
+
file_path = os.path.join(tmpdir, "empty.json")
|
|
220
|
+
|
|
221
|
+
# Create empty JSON file
|
|
222
|
+
with open(file_path, "w") as f:
|
|
223
|
+
json.dump([], f)
|
|
224
|
+
|
|
225
|
+
# Execute
|
|
226
|
+
result = WizMixin.load_file(file_path)
|
|
227
|
+
|
|
228
|
+
# Verify
|
|
229
|
+
assert result == []
|
|
230
|
+
|
|
231
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.check_file_path")
|
|
232
|
+
def test_load_file_complex_data(self, mock_check_file_path):
|
|
233
|
+
"""Test load_file handles complex nested structures."""
|
|
234
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
235
|
+
file_path = os.path.join(tmpdir, "complex.json")
|
|
236
|
+
test_data = [
|
|
237
|
+
{"id": "1", "nested": {"deep": {"deeper": "value"}}, "list": [1, 2, {"key": "value"}]},
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
# Create test file
|
|
241
|
+
with open(file_path, "w") as f:
|
|
242
|
+
json.dump(test_data, f)
|
|
243
|
+
|
|
244
|
+
# Execute
|
|
245
|
+
result = WizMixin.load_file(file_path)
|
|
246
|
+
|
|
247
|
+
# Verify
|
|
248
|
+
assert result == test_data
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class TestWizMixinFetchData:
|
|
252
|
+
"""Test fetch_data method with GraphQL client mocking."""
|
|
253
|
+
|
|
254
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.PaginatedGraphQLClient")
|
|
255
|
+
def test_fetch_data_success(self, mock_client_class):
|
|
256
|
+
"""Test fetch_data successfully fetches data from Wiz."""
|
|
257
|
+
# Setup
|
|
258
|
+
wiz = WizMixin()
|
|
259
|
+
wiz.config = {"wizUrl": "https://api.wiz.io/graphql", "wizAccessToken": "test-token-123"}
|
|
260
|
+
|
|
261
|
+
mock_client = MagicMock()
|
|
262
|
+
mock_client.fetch_all.return_value = [
|
|
263
|
+
{"id": "1", "name": "result1"},
|
|
264
|
+
{"id": "2", "name": "result2"},
|
|
265
|
+
]
|
|
266
|
+
mock_client_class.return_value = mock_client
|
|
267
|
+
|
|
268
|
+
# Execute
|
|
269
|
+
result = wiz.fetch_data(query="query {}", topic_key="test", variables={"key": "value"})
|
|
270
|
+
|
|
271
|
+
# Verify
|
|
272
|
+
assert len(result) == 2
|
|
273
|
+
assert result[0]["name"] == "result1"
|
|
274
|
+
assert result[1]["name"] == "result2"
|
|
275
|
+
|
|
276
|
+
# Verify client was created with correct parameters
|
|
277
|
+
mock_client_class.assert_called_once_with(
|
|
278
|
+
endpoint="https://api.wiz.io/graphql",
|
|
279
|
+
query="query {}",
|
|
280
|
+
headers={"Content-Type": "application/json", "Authorization": "Bearer test-token-123"},
|
|
281
|
+
)
|
|
282
|
+
mock_client.fetch_all.assert_called_once_with(variables={"key": "value"}, topic_key="test")
|
|
283
|
+
|
|
284
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.error_and_exit")
|
|
285
|
+
def test_fetch_data_missing_url_config(self, mock_error_and_exit):
|
|
286
|
+
"""Test fetch_data handles missing Wiz URL configuration."""
|
|
287
|
+
# Setup
|
|
288
|
+
wiz = WizMixin()
|
|
289
|
+
wiz.config = {"wizAccessToken": "test-token"} # Missing wizUrl
|
|
290
|
+
|
|
291
|
+
mock_error_and_exit.side_effect = SystemExit(1)
|
|
292
|
+
|
|
293
|
+
# Execute & Verify
|
|
294
|
+
with pytest.raises(SystemExit):
|
|
295
|
+
wiz.fetch_data(query="query {}", topic_key="test", variables=None)
|
|
296
|
+
|
|
297
|
+
mock_error_and_exit.assert_called_once_with("Wiz API endpoint not configured")
|
|
298
|
+
|
|
299
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.error_and_exit")
|
|
300
|
+
def test_fetch_data_empty_url_config(self, mock_error_and_exit):
|
|
301
|
+
"""Test fetch_data handles empty Wiz URL configuration."""
|
|
302
|
+
# Setup
|
|
303
|
+
wiz = WizMixin()
|
|
304
|
+
wiz.config = {"wizUrl": "", "wizAccessToken": "test-token"}
|
|
305
|
+
|
|
306
|
+
mock_error_and_exit.side_effect = SystemExit(1)
|
|
307
|
+
|
|
308
|
+
# Execute & Verify
|
|
309
|
+
with pytest.raises(SystemExit):
|
|
310
|
+
wiz.fetch_data(query="query {}", topic_key="test", variables=None)
|
|
311
|
+
|
|
312
|
+
mock_error_and_exit.assert_called_once_with("Wiz API endpoint not configured")
|
|
313
|
+
|
|
314
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.PaginatedGraphQLClient")
|
|
315
|
+
def test_fetch_data_no_token_returns_empty(self, mock_client_class):
|
|
316
|
+
"""Test fetch_data returns empty list when no access token."""
|
|
317
|
+
# Setup
|
|
318
|
+
wiz = WizMixin()
|
|
319
|
+
wiz.config = {"wizUrl": "https://api.wiz.io/graphql"} # No token
|
|
320
|
+
|
|
321
|
+
# Execute
|
|
322
|
+
result = wiz.fetch_data(query="query {}", topic_key="test", variables=None)
|
|
323
|
+
|
|
324
|
+
# Verify
|
|
325
|
+
assert result == []
|
|
326
|
+
mock_client_class.assert_not_called()
|
|
327
|
+
|
|
328
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.PaginatedGraphQLClient")
|
|
329
|
+
def test_fetch_data_empty_token_returns_empty(self, mock_client_class):
|
|
330
|
+
"""Test fetch_data returns empty list when access token is empty."""
|
|
331
|
+
# Setup
|
|
332
|
+
wiz = WizMixin()
|
|
333
|
+
wiz.config = {"wizUrl": "https://api.wiz.io/graphql", "wizAccessToken": ""}
|
|
334
|
+
|
|
335
|
+
# Execute
|
|
336
|
+
result = wiz.fetch_data(query="query {}", topic_key="test", variables=None)
|
|
337
|
+
|
|
338
|
+
# Verify
|
|
339
|
+
assert result == []
|
|
340
|
+
mock_client_class.assert_not_called()
|
|
341
|
+
|
|
342
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.PaginatedGraphQLClient")
|
|
343
|
+
def test_fetch_data_with_no_variables(self, mock_client_class):
|
|
344
|
+
"""Test fetch_data works without variables parameter."""
|
|
345
|
+
# Setup
|
|
346
|
+
wiz = WizMixin()
|
|
347
|
+
wiz.config = {"wizUrl": "https://api.wiz.io/graphql", "wizAccessToken": "token"}
|
|
348
|
+
|
|
349
|
+
mock_client = MagicMock()
|
|
350
|
+
mock_client.fetch_all.return_value = [{"id": "1"}]
|
|
351
|
+
mock_client_class.return_value = mock_client
|
|
352
|
+
|
|
353
|
+
# Execute
|
|
354
|
+
result = wiz.fetch_data(query="query {}", topic_key="test", variables=None)
|
|
355
|
+
|
|
356
|
+
# Verify
|
|
357
|
+
assert len(result) == 1
|
|
358
|
+
mock_client.fetch_all.assert_called_once_with(variables=None, topic_key="test")
|
|
359
|
+
|
|
360
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.PaginatedGraphQLClient")
|
|
361
|
+
def test_fetch_data_returns_empty_list_from_client(self, mock_client_class):
|
|
362
|
+
"""Test fetch_data handles empty results from GraphQL client."""
|
|
363
|
+
# Setup
|
|
364
|
+
wiz = WizMixin()
|
|
365
|
+
wiz.config = {"wizUrl": "https://api.wiz.io/graphql", "wizAccessToken": "token"}
|
|
366
|
+
|
|
367
|
+
mock_client = MagicMock()
|
|
368
|
+
mock_client.fetch_all.return_value = []
|
|
369
|
+
mock_client_class.return_value = mock_client
|
|
370
|
+
|
|
371
|
+
# Execute
|
|
372
|
+
result = wiz.fetch_data(query="query {}", topic_key="test", variables=None)
|
|
373
|
+
|
|
374
|
+
# Verify
|
|
375
|
+
assert result == []
|
|
376
|
+
|
|
377
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.PaginatedGraphQLClient")
|
|
378
|
+
def test_fetch_data_bearer_token_format(self, mock_client_class):
|
|
379
|
+
"""Test fetch_data properly formats Bearer token in headers."""
|
|
380
|
+
# Setup
|
|
381
|
+
wiz = WizMixin()
|
|
382
|
+
wiz.config = {"wizUrl": "https://api.wiz.io/graphql", "wizAccessToken": "my-access-token"}
|
|
383
|
+
|
|
384
|
+
mock_client = MagicMock()
|
|
385
|
+
mock_client.fetch_all.return_value = []
|
|
386
|
+
mock_client_class.return_value = mock_client
|
|
387
|
+
|
|
388
|
+
# Execute
|
|
389
|
+
wiz.fetch_data(query="query {}", topic_key="test", variables=None)
|
|
390
|
+
|
|
391
|
+
# Verify Bearer token format
|
|
392
|
+
call_args = mock_client_class.call_args
|
|
393
|
+
assert call_args[1]["headers"]["Authorization"] == "Bearer my-access-token"
|
|
394
|
+
|
|
395
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.PaginatedGraphQLClient")
|
|
396
|
+
def test_fetch_data_content_type_header(self, mock_client_class):
|
|
397
|
+
"""Test fetch_data includes correct Content-Type header."""
|
|
398
|
+
# Setup
|
|
399
|
+
wiz = WizMixin()
|
|
400
|
+
wiz.config = {"wizUrl": "https://api.wiz.io/graphql", "wizAccessToken": "token"}
|
|
401
|
+
|
|
402
|
+
mock_client = MagicMock()
|
|
403
|
+
mock_client.fetch_all.return_value = []
|
|
404
|
+
mock_client_class.return_value = mock_client
|
|
405
|
+
|
|
406
|
+
# Execute
|
|
407
|
+
wiz.fetch_data(query="query {}", topic_key="test", variables=None)
|
|
408
|
+
|
|
409
|
+
# Verify Content-Type header
|
|
410
|
+
call_args = mock_client_class.call_args
|
|
411
|
+
assert call_args[1]["headers"]["Content-Type"] == "application/json"
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class TestWizMixinEdgeCases:
|
|
415
|
+
"""Test edge cases and error scenarios."""
|
|
416
|
+
|
|
417
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.write_to_file")
|
|
418
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.fetch_data")
|
|
419
|
+
@patch("os.path.exists")
|
|
420
|
+
@patch("os.path.getmtime")
|
|
421
|
+
def test_fetch_data_if_needed_exactly_at_interval_boundary(
|
|
422
|
+
self, mock_getmtime, mock_exists, mock_fetch_data, mock_write_to_file
|
|
423
|
+
):
|
|
424
|
+
"""Test behavior when file age exactly equals interval."""
|
|
425
|
+
# Setup
|
|
426
|
+
wiz = WizMixin()
|
|
427
|
+
file_path = "artifacts/test.json"
|
|
428
|
+
fresh_data = [{"id": "1"}]
|
|
429
|
+
|
|
430
|
+
# Mock file modified exactly 2 hours ago with 2 hour interval
|
|
431
|
+
mock_exists.return_value = True
|
|
432
|
+
current_time = datetime.now()
|
|
433
|
+
file_mod_time = current_time - timedelta(hours=2)
|
|
434
|
+
mock_getmtime.return_value = file_mod_time.timestamp()
|
|
435
|
+
mock_fetch_data.return_value = fresh_data
|
|
436
|
+
|
|
437
|
+
# Execute
|
|
438
|
+
result = wiz.fetch_data_if_needed(
|
|
439
|
+
file_path=file_path, query="query {}", topic_key="test", interval_hours=2, variables=None
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
# Verify - at boundary, should fetch new data
|
|
443
|
+
assert result == fresh_data
|
|
444
|
+
mock_fetch_data.assert_called_once()
|
|
445
|
+
mock_write_to_file.assert_called_once()
|
|
446
|
+
|
|
447
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.load_file")
|
|
448
|
+
@patch("os.path.exists")
|
|
449
|
+
@patch("os.path.getmtime")
|
|
450
|
+
def test_fetch_data_if_needed_very_fresh_file(self, mock_getmtime, mock_exists, mock_load_file):
|
|
451
|
+
"""Test with very recently modified file (seconds old)."""
|
|
452
|
+
# Setup
|
|
453
|
+
wiz = WizMixin()
|
|
454
|
+
file_path = "artifacts/test.json"
|
|
455
|
+
cached_data = [{"id": "1"}]
|
|
456
|
+
|
|
457
|
+
# Mock file modified 30 seconds ago
|
|
458
|
+
mock_exists.return_value = True
|
|
459
|
+
current_time = datetime.now()
|
|
460
|
+
file_mod_time = current_time - timedelta(seconds=30)
|
|
461
|
+
mock_getmtime.return_value = file_mod_time.timestamp()
|
|
462
|
+
mock_load_file.return_value = cached_data
|
|
463
|
+
|
|
464
|
+
# Execute with 1 hour interval
|
|
465
|
+
result = wiz.fetch_data_if_needed(
|
|
466
|
+
file_path=file_path, query="query {}", topic_key="test", interval_hours=1, variables=None
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# Verify - should use cache
|
|
470
|
+
assert result == cached_data
|
|
471
|
+
mock_load_file.assert_called_once()
|
|
472
|
+
|
|
473
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.write_to_file")
|
|
474
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.fetch_data")
|
|
475
|
+
@patch("os.path.exists")
|
|
476
|
+
def test_fetch_data_if_needed_with_zero_interval(self, mock_exists, mock_fetch_data, mock_write_to_file):
|
|
477
|
+
"""Test with zero hour interval (always fetch)."""
|
|
478
|
+
# Setup
|
|
479
|
+
wiz = WizMixin()
|
|
480
|
+
file_path = "artifacts/test.json"
|
|
481
|
+
fresh_data = [{"id": "1"}]
|
|
482
|
+
|
|
483
|
+
mock_exists.return_value = False
|
|
484
|
+
mock_fetch_data.return_value = fresh_data
|
|
485
|
+
|
|
486
|
+
# Execute with 0 hour interval
|
|
487
|
+
result = wiz.fetch_data_if_needed(
|
|
488
|
+
file_path=file_path, query="query {}", topic_key="test", interval_hours=0, variables=None
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# Verify
|
|
492
|
+
assert result == fresh_data
|
|
493
|
+
mock_fetch_data.assert_called_once()
|
|
494
|
+
|
|
495
|
+
@patch("regscale.integrations.commercial.wizv2.WizDataMixin.PaginatedGraphQLClient")
|
|
496
|
+
def test_fetch_data_with_complex_variables(self, mock_client_class):
|
|
497
|
+
"""Test fetch_data with complex nested variables."""
|
|
498
|
+
# Setup
|
|
499
|
+
wiz = WizMixin()
|
|
500
|
+
wiz.config = {"wizUrl": "https://api.wiz.io/graphql", "wizAccessToken": "token"}
|
|
501
|
+
|
|
502
|
+
complex_variables = {
|
|
503
|
+
"filterBy": {"status": ["OPEN", "IN_PROGRESS"], "severity": ["HIGH", "CRITICAL"]},
|
|
504
|
+
"first": 100,
|
|
505
|
+
"orderBy": {"field": "CREATED_AT", "direction": "DESC"},
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
mock_client = MagicMock()
|
|
509
|
+
mock_client.fetch_all.return_value = [{"id": "1"}]
|
|
510
|
+
mock_client_class.return_value = mock_client
|
|
511
|
+
|
|
512
|
+
# Execute
|
|
513
|
+
result = wiz.fetch_data(query="query {}", topic_key="test", variables=complex_variables)
|
|
514
|
+
|
|
515
|
+
# Verify
|
|
516
|
+
assert len(result) == 1
|
|
517
|
+
mock_client.fetch_all.assert_called_once_with(variables=complex_variables, topic_key="test")
|
|
518
|
+
|
|
519
|
+
@patch("regscale.core.app.utils.app_utils.check_file_path")
|
|
520
|
+
def test_write_to_file_overwrites_existing(self, mock_check_file_path):
|
|
521
|
+
"""Test write_to_file overwrites existing file."""
|
|
522
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
523
|
+
file_path = os.path.join(tmpdir, "test.json")
|
|
524
|
+
|
|
525
|
+
# Write initial data
|
|
526
|
+
initial_data = [{"id": "1", "name": "old"}]
|
|
527
|
+
WizMixin.write_to_file(file_path, initial_data)
|
|
528
|
+
|
|
529
|
+
# Overwrite with new data
|
|
530
|
+
new_data = [{"id": "2", "name": "new"}]
|
|
531
|
+
WizMixin.write_to_file(file_path, new_data)
|
|
532
|
+
|
|
533
|
+
# Verify only new data exists
|
|
534
|
+
with open(file_path, "r") as f:
|
|
535
|
+
loaded_data = json.load(f)
|
|
536
|
+
assert loaded_data == new_data
|
|
537
|
+
assert len(loaded_data) == 1
|