regscale-cli 6.20.7.0__py3-none-any.whl → 6.20.9.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/api.py +8 -1
- regscale/core/app/application.py +130 -20
- regscale/core/utils/date.py +16 -16
- regscale/integrations/commercial/aqua/aqua.py +1 -1
- regscale/integrations/commercial/aws/cli.py +1 -1
- regscale/integrations/commercial/defender.py +1 -1
- regscale/integrations/commercial/ecr.py +1 -1
- regscale/integrations/commercial/ibm.py +1 -1
- regscale/integrations/commercial/nexpose.py +1 -1
- regscale/integrations/commercial/prisma.py +1 -1
- regscale/integrations/commercial/qualys/__init__.py +150 -77
- regscale/integrations/commercial/qualys/containers.py +2 -1
- regscale/integrations/commercial/qualys/scanner.py +5 -3
- regscale/integrations/commercial/snyk.py +14 -4
- regscale/integrations/commercial/synqly/ticketing.py +23 -11
- regscale/integrations/commercial/veracode.py +15 -4
- regscale/integrations/commercial/xray.py +1 -1
- regscale/integrations/public/cisa.py +7 -1
- regscale/integrations/public/nist_catalog.py +8 -2
- regscale/integrations/scanner_integration.py +18 -36
- regscale/models/integration_models/cisa_kev_data.json +51 -6
- regscale/models/integration_models/flat_file_importer/__init__.py +34 -19
- regscale/models/integration_models/send_reminders.py +8 -2
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/regscale_models/control_implementation.py +40 -0
- regscale/models/regscale_models/issue.py +7 -4
- regscale/models/regscale_models/parameter.py +3 -2
- regscale/models/regscale_models/ports_protocol.py +15 -5
- regscale/models/regscale_models/vulnerability.py +1 -1
- regscale/utils/graphql_client.py +3 -6
- regscale/utils/threading/threadhandler.py +12 -2
- {regscale_cli-6.20.7.0.dist-info → regscale_cli-6.20.9.0.dist-info}/METADATA +13 -13
- {regscale_cli-6.20.7.0.dist-info → regscale_cli-6.20.9.0.dist-info}/RECORD +41 -40
- tests/regscale/core/test_app.py +402 -16
- tests/regscale/core/test_version_regscale.py +62 -0
- tests/regscale/test_init.py +2 -0
- {regscale_cli-6.20.7.0.dist-info → regscale_cli-6.20.9.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.20.7.0.dist-info → regscale_cli-6.20.9.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.20.7.0.dist-info → regscale_cli-6.20.9.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.20.7.0.dist-info → regscale_cli-6.20.9.0.dist-info}/top_level.txt +0 -0
tests/regscale/core/test_app.py
CHANGED
|
@@ -54,29 +54,31 @@ class TestApplication:
|
|
|
54
54
|
def test_fetch_config_from_regscale_success(self, mock_get, mock_parse_user_id):
|
|
55
55
|
mock_parse_user_id.return_value = "test_user_id"
|
|
56
56
|
mock_response = MagicMock()
|
|
57
|
-
mock_response.json.return_value = {"
|
|
57
|
+
mock_response.json.return_value = '{"key": "value"}'
|
|
58
58
|
mock_get.return_value = mock_response
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
with patch.object(self.app, "_decrypt_config", return_value='{"key": "value"}'):
|
|
60
|
+
config = self.app._fetch_config_from_regscale(config=self.app.config)
|
|
61
|
+
assert "domain" in config
|
|
62
|
+
assert config["userId"] == "test_user_id"
|
|
63
|
+
assert config["key"] == "value"
|
|
63
64
|
|
|
64
65
|
@patch("regscale.core.app.internal.login.parse_user_id_from_jwt")
|
|
65
66
|
@patch("requests.get")
|
|
66
67
|
def test_fetch_config_from_regscale_success_with_envars(self, mock_get, mock_parse_user_id):
|
|
67
68
|
mock_parse_user_id.return_value = "test_user_id"
|
|
68
69
|
mock_response = MagicMock()
|
|
69
|
-
mock_response.json.return_value = {"
|
|
70
|
+
mock_response.json.return_value = '{"key": "value"}'
|
|
70
71
|
mock_get.return_value = mock_response
|
|
71
72
|
envars = os.environ.copy()
|
|
72
73
|
envars["REGSCALE_DOMAIN"] = self.test_domain
|
|
73
74
|
envars["REGSCALE_TOKEN"] = self.test_token
|
|
74
75
|
with patch.dict(os.environ, envars, clear=True):
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
with patch.object(self.app, "_decrypt_config", return_value='{"key": "value"}'):
|
|
77
|
+
config = self.app._fetch_config_from_regscale(config={})
|
|
78
|
+
assert config["domain"] == self.test_domain
|
|
79
|
+
assert config["token"] == self.test_token
|
|
80
|
+
assert config["userId"] == "test_user_id"
|
|
81
|
+
assert config["key"] == "value"
|
|
80
82
|
|
|
81
83
|
@patch("requests.get")
|
|
82
84
|
def test_fetch_config_from_regscale_failure(self, mock_get):
|
|
@@ -88,12 +90,396 @@ class TestApplication:
|
|
|
88
90
|
envars["REGSCALE_DOMAIN"] = self.test_domain
|
|
89
91
|
envars["REGSCALE_TOKEN"] = self.test_token
|
|
90
92
|
with patch.dict(os.environ, envars, clear=True):
|
|
91
|
-
empty_config = self.app._fetch_config_from_regscale()
|
|
92
|
-
assert empty_config == {}
|
|
93
93
|
with patch.object(self.app.logger, "error") as mock_logger_error:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
empty_config = self.app._fetch_config_from_regscale()
|
|
95
|
+
assert empty_config == {}
|
|
96
|
+
mock_logger_error.assert_called()
|
|
97
|
+
|
|
98
|
+
@patch("regscale.core.app.internal.login.parse_user_id_from_jwt")
|
|
99
|
+
@patch("requests.get")
|
|
100
|
+
def test_fetch_config_from_regscale_unencrypted_dict(self, mock_get, mock_parse_user_id):
|
|
101
|
+
"""Test the case where the response is already a dictionary (not encrypted)"""
|
|
102
|
+
mock_parse_user_id.return_value = "test_user_id"
|
|
103
|
+
mock_response = MagicMock()
|
|
104
|
+
mock_response.json.return_value = {"key": "value", "userId": "test_user_id"}
|
|
105
|
+
mock_get.return_value = mock_response
|
|
106
|
+
config = self.app._fetch_config_from_regscale(config=self.app.config)
|
|
107
|
+
assert "domain" in config
|
|
108
|
+
assert config["userId"] == "test_user_id"
|
|
109
|
+
assert config["key"] == "value"
|
|
110
|
+
|
|
111
|
+
@patch("regscale.core.app.internal.login.parse_user_id_from_jwt")
|
|
112
|
+
@patch("requests.get")
|
|
113
|
+
def test_fetch_config_from_regscale_no_cli_config(self, mock_get, mock_parse_user_id):
|
|
114
|
+
"""Test error handling when the cliConfig field is missing from the response"""
|
|
115
|
+
mock_parse_user_id.return_value = "test_user_id"
|
|
116
|
+
mock_response = MagicMock()
|
|
117
|
+
mock_response.json.return_value = ""
|
|
118
|
+
mock_get.return_value = mock_response
|
|
119
|
+
with patch.object(self.app.logger, "warning") as mock_logger_warning:
|
|
120
|
+
config = self.app._fetch_config_from_regscale(config={})
|
|
121
|
+
mock_logger_warning.assert_called_once()
|
|
122
|
+
assert config == {}
|
|
123
|
+
|
|
124
|
+
@patch("regscale.core.app.internal.login.parse_user_id_from_jwt")
|
|
125
|
+
@patch("requests.get")
|
|
126
|
+
def test_fetch_config_from_regscale_with_existing_user_id(self, mock_get, mock_parse_user_id):
|
|
127
|
+
"""Test that existing userId in response is preserved"""
|
|
128
|
+
mock_response = MagicMock()
|
|
129
|
+
mock_response.json.return_value = '{"key": "value", "userId": "existing_user"}'
|
|
130
|
+
mock_get.return_value = mock_response
|
|
131
|
+
|
|
132
|
+
with patch.object(self.app, "_decrypt_config", return_value='{"key": "value", "userId": "existing_user"}'):
|
|
133
|
+
config = self.app._fetch_config_from_regscale(config=self.app.config)
|
|
134
|
+
assert config["userId"] == "existing_user"
|
|
135
|
+
# parse_user_id_from_jwt should not be called since userId already exists
|
|
136
|
+
mock_parse_user_id.assert_not_called()
|
|
137
|
+
|
|
138
|
+
@patch("regscale.core.app.internal.login.parse_user_id_from_jwt")
|
|
139
|
+
@patch("requests.get")
|
|
140
|
+
def test_fetch_config_from_regscale_empty_response(self, mock_get, mock_parse_user_id):
|
|
141
|
+
"""Test handling of empty response"""
|
|
142
|
+
mock_response = MagicMock()
|
|
143
|
+
mock_response.json.return_value = {}
|
|
144
|
+
mock_get.return_value = mock_response
|
|
145
|
+
|
|
146
|
+
with patch.object(self.app.logger, "warning") as mock_logger_warning:
|
|
147
|
+
config = self.app._fetch_config_from_regscale(config=self.app.config)
|
|
148
|
+
assert config == {}
|
|
149
|
+
mock_logger_warning.assert_called_once()
|
|
150
|
+
|
|
151
|
+
@patch("regscale.core.app.internal.login.parse_user_id_from_jwt")
|
|
152
|
+
@patch("requests.get")
|
|
153
|
+
def test_fetch_config_from_regscale_no_token(self, mock_get, mock_parse_user_id):
|
|
154
|
+
"""Test handling when no token is provided"""
|
|
155
|
+
envars = os.environ.copy()
|
|
156
|
+
envars.pop("REGSCALE_TOKEN", None)
|
|
157
|
+
envars.pop("REGSCALE_DOMAIN", None)
|
|
158
|
+
|
|
159
|
+
with patch.dict(os.environ, envars, clear=True):
|
|
160
|
+
config = self.app._fetch_config_from_regscale(config={})
|
|
161
|
+
assert config == {}
|
|
162
|
+
mock_get.assert_not_called()
|
|
163
|
+
|
|
164
|
+
@patch("regscale.core.app.internal.login.parse_user_id_from_jwt")
|
|
165
|
+
@patch("requests.get")
|
|
166
|
+
def test_fetch_config_from_regscale_no_domain(self, mock_get, mock_parse_user_id):
|
|
167
|
+
"""Test handling when no domain is provided"""
|
|
168
|
+
envars = os.environ.copy()
|
|
169
|
+
envars["REGSCALE_TOKEN"] = self.test_token
|
|
170
|
+
envars.pop("REGSCALE_DOMAIN", None)
|
|
171
|
+
|
|
172
|
+
with patch.dict(os.environ, envars, clear=True):
|
|
173
|
+
with patch.object(self.app, "retrieve_domain", return_value="https://default.com"):
|
|
174
|
+
_ = self.app._fetch_config_from_regscale(config={})
|
|
175
|
+
# Should still attempt to make the request with default domain
|
|
176
|
+
mock_get.assert_called_once()
|
|
177
|
+
|
|
178
|
+
@patch("regscale.core.app.internal.login.parse_user_id_from_jwt")
|
|
179
|
+
@patch("requests.get")
|
|
180
|
+
def test_fetch_config_from_regscale_request_exception(self, mock_get, mock_parse_user_id):
|
|
181
|
+
"""Test handling of request exceptions"""
|
|
182
|
+
mock_get.side_effect = Exception("Network error")
|
|
183
|
+
|
|
184
|
+
with patch.object(self.app.logger, "error") as mock_logger_error:
|
|
185
|
+
config = self.app._fetch_config_from_regscale(config=self.app.config)
|
|
186
|
+
assert config == {}
|
|
187
|
+
mock_logger_error.assert_called_once()
|
|
188
|
+
|
|
189
|
+
@patch("regscale.core.app.internal.login.parse_user_id_from_jwt")
|
|
190
|
+
@patch("requests.get")
|
|
191
|
+
def test_fetch_config_from_regscale_correct_endpoint(self, mock_get, mock_parse_user_id):
|
|
192
|
+
"""Test that the correct API endpoint is called"""
|
|
193
|
+
mock_parse_user_id.return_value = "test_user_id"
|
|
194
|
+
mock_response = MagicMock()
|
|
195
|
+
mock_response.json.return_value = {"cliConfig": '{"key": "value"}'}
|
|
196
|
+
mock_get.return_value = mock_response
|
|
197
|
+
|
|
198
|
+
with patch.object(self.app, "_decrypt_config", return_value='{"key": "value"}'):
|
|
199
|
+
self.app._fetch_config_from_regscale(config=self.app.config)
|
|
200
|
+
|
|
201
|
+
# Verify the correct endpoint was called
|
|
202
|
+
mock_get.assert_called_once()
|
|
203
|
+
call_args = mock_get.call_args
|
|
204
|
+
assert "/api/tenants/getDetailedCliConfig" in call_args[1]["url"]
|
|
205
|
+
|
|
206
|
+
@patch("regscale.core.app.internal.login.parse_user_id_from_jwt")
|
|
207
|
+
@patch("requests.get")
|
|
208
|
+
def test_fetch_config_from_regscale_headers_verification(self, mock_get, mock_parse_user_id):
|
|
209
|
+
"""Test that the correct headers are sent with the request"""
|
|
210
|
+
mock_parse_user_id.return_value = "test_user_id"
|
|
211
|
+
mock_response = MagicMock()
|
|
212
|
+
mock_response.json.return_value = {"cliConfig": '{"key": "value"}'}
|
|
213
|
+
mock_get.return_value = mock_response
|
|
214
|
+
|
|
215
|
+
with patch.object(self.app, "_decrypt_config", return_value='{"key": "value"}'):
|
|
216
|
+
self.app._fetch_config_from_regscale(config=self.app.config)
|
|
217
|
+
|
|
218
|
+
# Verify the correct headers were sent
|
|
219
|
+
mock_get.assert_called_once()
|
|
220
|
+
call_args = mock_get.call_args
|
|
221
|
+
headers = call_args[1]["headers"]
|
|
222
|
+
assert headers["Content-Type"] == "application/json"
|
|
223
|
+
assert headers["Accept"] == "application/json"
|
|
224
|
+
assert headers["Authorization"] == self.app.config.get("token")
|
|
225
|
+
|
|
226
|
+
def test_fetch_config_from_regscale_json_decode_error(self):
|
|
227
|
+
"""Test handling of JSON decode errors in decrypted config"""
|
|
228
|
+
mock_response = MagicMock()
|
|
229
|
+
mock_response.json.return_value = {"cliConfig": "encrypted_data"}
|
|
230
|
+
|
|
231
|
+
with patch("requests.get", return_value=mock_response):
|
|
232
|
+
with patch.object(self.app, "_decrypt_config", return_value="invalid json"):
|
|
233
|
+
with patch.object(self.app.logger, "error") as mock_logger_error:
|
|
234
|
+
config = self.app._fetch_config_from_regscale(config=self.app.config)
|
|
235
|
+
assert config == {}
|
|
236
|
+
mock_logger_error.assert_called_once()
|
|
237
|
+
|
|
238
|
+
def test_decrypt_config(self):
|
|
239
|
+
"""Test the _decrypt_config method with a mock encrypted string"""
|
|
240
|
+
# Mock the cryptography imports
|
|
241
|
+
with patch("cryptography.hazmat.primitives.ciphers.Cipher") as mock_cipher:
|
|
242
|
+
with patch("cryptography.hazmat.backends.default_backend") as mock_backend:
|
|
243
|
+
with patch("base64.b64decode") as mock_b64decode:
|
|
244
|
+
with patch("hashlib.sha256") as mock_sha256:
|
|
245
|
+
# Setup mocks
|
|
246
|
+
mock_backend.return_value = "backend"
|
|
247
|
+
mock_b64decode.side_effect = [
|
|
248
|
+
b"iv_data_cipher_text", # First call for combined data
|
|
249
|
+
b"key_data", # Second call for token
|
|
250
|
+
]
|
|
251
|
+
mock_sha256_instance = MagicMock()
|
|
252
|
+
mock_sha256_instance.digest.return_value = b"key" * 8 # 32 bytes for AES-256
|
|
253
|
+
mock_sha256.return_value = mock_sha256_instance
|
|
254
|
+
|
|
255
|
+
mock_decryptor = MagicMock()
|
|
256
|
+
mock_decryptor.update.return_value = b'{"key": "value"}'
|
|
257
|
+
mock_decryptor.finalize.return_value = b""
|
|
258
|
+
|
|
259
|
+
mock_cipher_instance = MagicMock()
|
|
260
|
+
mock_cipher_instance.decryptor.return_value = mock_decryptor
|
|
261
|
+
mock_cipher.return_value = mock_cipher_instance
|
|
262
|
+
|
|
263
|
+
# Test the method
|
|
264
|
+
result = self.app._decrypt_config(
|
|
265
|
+
"encrypted_string", os.getenv("REGSCALE_TOKEN", self.app.config["token"])
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Verify the result
|
|
269
|
+
assert result == '{"key": "value"}'
|
|
270
|
+
|
|
271
|
+
# Verify the mocks were called correctly
|
|
272
|
+
mock_b64decode.assert_called()
|
|
273
|
+
mock_sha256.assert_called_once()
|
|
274
|
+
mock_cipher.assert_called_once()
|
|
275
|
+
|
|
276
|
+
def test_decrypt_config_with_trailing_characters(self):
|
|
277
|
+
"""Test the _decrypt_config method handles trailing control characters"""
|
|
278
|
+
with patch("cryptography.hazmat.primitives.ciphers.Cipher") as mock_cipher:
|
|
279
|
+
with patch("cryptography.hazmat.backends.default_backend") as mock_backend:
|
|
280
|
+
with patch("base64.b64decode") as mock_b64decode:
|
|
281
|
+
with patch("hashlib.sha256") as mock_sha256:
|
|
282
|
+
# Setup mocks
|
|
283
|
+
mock_backend.return_value = "backend"
|
|
284
|
+
mock_b64decode.side_effect = [
|
|
285
|
+
b"iv_data_cipher_text", # First call for combined data
|
|
286
|
+
b"key_data", # Second call for token
|
|
287
|
+
]
|
|
288
|
+
mock_sha256_instance = MagicMock()
|
|
289
|
+
mock_sha256_instance.digest.return_value = b"key" * 8 # 32 bytes for AES-256
|
|
290
|
+
mock_sha256.return_value = mock_sha256_instance
|
|
291
|
+
|
|
292
|
+
mock_decryptor = MagicMock()
|
|
293
|
+
# Simulate decrypted data with trailing control characters
|
|
294
|
+
mock_decryptor.update.return_value = b'{"key": "value"}\x00\x08\x07'
|
|
295
|
+
mock_decryptor.finalize.return_value = b""
|
|
296
|
+
|
|
297
|
+
mock_cipher_instance = MagicMock()
|
|
298
|
+
mock_cipher_instance.decryptor.return_value = mock_decryptor
|
|
299
|
+
mock_cipher.return_value = mock_cipher_instance
|
|
300
|
+
|
|
301
|
+
# Test the method
|
|
302
|
+
result = self.app._decrypt_config("encrypted_string", "Bearer test_token")
|
|
303
|
+
|
|
304
|
+
# Verify the result is cleaned
|
|
305
|
+
assert result == '{"key": "value"}'
|
|
306
|
+
|
|
307
|
+
def test_decrypt_config_invalid_base64(self):
|
|
308
|
+
"""Test _decrypt_config with invalid base64 input"""
|
|
309
|
+
with patch("base64.b64decode", side_effect=Exception("Invalid base64")):
|
|
310
|
+
with patch.object(self.app.logger, "error") as mock_logger_error:
|
|
311
|
+
result = self.app._decrypt_config("invalid_base64", "Bearer test_token")
|
|
312
|
+
# Should raise an exception that gets caught by the calling method
|
|
313
|
+
assert result is None or mock_logger_error.called
|
|
314
|
+
assert mock_logger_error.call_count == 1
|
|
315
|
+
|
|
316
|
+
def test_decrypt_config_short_data(self):
|
|
317
|
+
"""Test _decrypt_config with data too short for IV extraction"""
|
|
318
|
+
with patch("base64.b64decode", return_value=b"short"):
|
|
319
|
+
with patch.object(self.app.logger, "error") as mock_logger_error:
|
|
320
|
+
result = self.app._decrypt_config("short_data", "Bearer test_token")
|
|
321
|
+
# Should handle the short data gracefully
|
|
322
|
+
assert result is None or mock_logger_error.called
|
|
323
|
+
|
|
324
|
+
def test_decrypt_config_decryption_failure(self):
|
|
325
|
+
"""Test _decrypt_config when decryption fails"""
|
|
326
|
+
with patch("cryptography.hazmat.primitives.ciphers.Cipher") as mock_cipher:
|
|
327
|
+
with patch("cryptography.hazmat.backends.default_backend") as mock_backend:
|
|
328
|
+
with patch("base64.b64decode") as mock_b64decode:
|
|
329
|
+
with patch("hashlib.sha256") as mock_sha256:
|
|
330
|
+
# Setup mocks
|
|
331
|
+
mock_backend.return_value = "backend"
|
|
332
|
+
mock_b64decode.return_value = b"iv_data_cipher_text"
|
|
333
|
+
mock_sha256_instance = MagicMock()
|
|
334
|
+
mock_sha256_instance.digest.return_value = b"key" * 8 # 32 bytes for AES-256
|
|
335
|
+
mock_sha256.return_value = mock_sha256_instance
|
|
336
|
+
|
|
337
|
+
mock_cipher.side_effect = Exception("Decryption failed")
|
|
338
|
+
|
|
339
|
+
with patch.object(self.app.logger, "error") as mock_logger_error:
|
|
340
|
+
result = self.app._decrypt_config("encrypted_string", "Bearer test_token")
|
|
341
|
+
# Should handle decryption failure gracefully
|
|
342
|
+
assert result is None or mock_logger_error.called
|
|
343
|
+
assert mock_logger_error.call_count == 1
|
|
344
|
+
|
|
345
|
+
def test_decrypt_config_unicode_decode_error(self):
|
|
346
|
+
"""Test _decrypt_config when UTF-8 decode fails"""
|
|
347
|
+
with patch("cryptography.hazmat.primitives.ciphers.Cipher") as mock_cipher:
|
|
348
|
+
with patch("cryptography.hazmat.backends.default_backend") as mock_backend:
|
|
349
|
+
with patch("base64.b64decode") as mock_b64decode:
|
|
350
|
+
with patch("hashlib.sha256") as mock_sha256:
|
|
351
|
+
# Setup mocks
|
|
352
|
+
mock_backend.return_value = "backend"
|
|
353
|
+
mock_b64decode.return_value = b"iv_data_cipher_text"
|
|
354
|
+
mock_sha256_instance = MagicMock()
|
|
355
|
+
mock_sha256_instance.digest.return_value = b"key" * 8 # 32 bytes for AES-256
|
|
356
|
+
mock_sha256.return_value = mock_sha256_instance
|
|
357
|
+
|
|
358
|
+
mock_decryptor = MagicMock()
|
|
359
|
+
# Return bytes that can't be decoded as UTF-8
|
|
360
|
+
mock_decryptor.update.return_value = b"\xff\xfe\xfd"
|
|
361
|
+
mock_decryptor.finalize.return_value = b""
|
|
362
|
+
|
|
363
|
+
mock_cipher_instance = MagicMock()
|
|
364
|
+
mock_cipher_instance.decryptor.return_value = mock_decryptor
|
|
365
|
+
mock_cipher.return_value = mock_cipher_instance
|
|
366
|
+
|
|
367
|
+
with patch.object(self.app.logger, "error") as mock_logger_error:
|
|
368
|
+
result = self.app._decrypt_config("encrypted_string", "Bearer test_token")
|
|
369
|
+
# Should handle decode error gracefully
|
|
370
|
+
assert result is None or mock_logger_error.called
|
|
371
|
+
|
|
372
|
+
def test_decrypt_config_multiple_null_bytes(self):
|
|
373
|
+
"""Test _decrypt_config with multiple trailing null bytes"""
|
|
374
|
+
with patch("cryptography.hazmat.primitives.ciphers.Cipher") as mock_cipher:
|
|
375
|
+
with patch("cryptography.hazmat.backends.default_backend") as mock_backend:
|
|
376
|
+
with patch("base64.b64decode") as mock_b64decode:
|
|
377
|
+
with patch("hashlib.sha256") as mock_sha256:
|
|
378
|
+
# Setup mocks
|
|
379
|
+
mock_backend.return_value = "backend"
|
|
380
|
+
mock_b64decode.return_value = b"iv_data_cipher_text"
|
|
381
|
+
mock_sha256_instance = MagicMock()
|
|
382
|
+
mock_sha256_instance.digest.return_value = b"key" * 8 # 32 bytes for AES-256
|
|
383
|
+
mock_sha256.return_value = mock_sha256_instance
|
|
384
|
+
|
|
385
|
+
mock_decryptor = MagicMock()
|
|
386
|
+
# Simulate decrypted data with multiple null bytes
|
|
387
|
+
mock_decryptor.update.return_value = b'{"key": "value"}\x00\x08\x07\x0f\x1b\x1c\x1d'
|
|
388
|
+
mock_decryptor.finalize.return_value = b""
|
|
389
|
+
|
|
390
|
+
mock_cipher_instance = MagicMock()
|
|
391
|
+
mock_cipher_instance.decryptor.return_value = mock_decryptor
|
|
392
|
+
mock_cipher.return_value = mock_cipher_instance
|
|
393
|
+
|
|
394
|
+
# Test the method
|
|
395
|
+
result = self.app._decrypt_config("encrypted_string", "Bearer test_token")
|
|
396
|
+
|
|
397
|
+
# Verify the result is cleaned
|
|
398
|
+
assert result == '{"key": "value"}'
|
|
399
|
+
|
|
400
|
+
def test_decrypt_config_mixed_trailing_characters(self):
|
|
401
|
+
"""Test _decrypt_config with mixed trailing characters"""
|
|
402
|
+
with patch("cryptography.hazmat.primitives.ciphers.Cipher") as mock_cipher:
|
|
403
|
+
with patch("cryptography.hazmat.backends.default_backend") as mock_backend:
|
|
404
|
+
with patch("base64.b64decode") as mock_b64decode:
|
|
405
|
+
with patch("hashlib.sha256") as mock_sha256:
|
|
406
|
+
# Setup mocks
|
|
407
|
+
mock_backend.return_value = "backend"
|
|
408
|
+
mock_b64decode.return_value = b"iv_data_cipher_text"
|
|
409
|
+
mock_sha256_instance = MagicMock()
|
|
410
|
+
mock_sha256_instance.digest.return_value = b"key" * 8 # 32 bytes for AES-256
|
|
411
|
+
mock_sha256.return_value = mock_sha256_instance
|
|
412
|
+
|
|
413
|
+
mock_decryptor = MagicMock()
|
|
414
|
+
# Simulate decrypted data with mixed trailing characters
|
|
415
|
+
mock_decryptor.update.return_value = b'{"key": "value"} \t\n\r\x00\x08\x07\x0f'
|
|
416
|
+
mock_decryptor.finalize.return_value = b""
|
|
417
|
+
|
|
418
|
+
mock_cipher_instance = MagicMock()
|
|
419
|
+
mock_cipher_instance.decryptor.return_value = mock_decryptor
|
|
420
|
+
mock_cipher.return_value = mock_cipher_instance
|
|
421
|
+
|
|
422
|
+
# Test the method
|
|
423
|
+
result = self.app._decrypt_config("encrypted_string", "Bearer test_token")
|
|
424
|
+
|
|
425
|
+
# Verify the result is cleaned
|
|
426
|
+
assert result == '{"key": "value"} '
|
|
427
|
+
|
|
428
|
+
def test_decrypt_config_with_trailing_backslash(self):
|
|
429
|
+
"""Test _decrypt_config with trailing backslash and characters after it"""
|
|
430
|
+
with patch("cryptography.hazmat.primitives.ciphers.Cipher") as mock_cipher:
|
|
431
|
+
with patch("cryptography.hazmat.backends.default_backend") as mock_backend:
|
|
432
|
+
with patch("base64.b64decode") as mock_b64decode:
|
|
433
|
+
with patch("hashlib.sha256") as mock_sha256:
|
|
434
|
+
# Setup mocks
|
|
435
|
+
mock_backend.return_value = "backend"
|
|
436
|
+
mock_b64decode.return_value = b"iv_data_cipher_text"
|
|
437
|
+
mock_sha256_instance = MagicMock()
|
|
438
|
+
mock_sha256_instance.digest.return_value = b"key" * 8 # 32 bytes for AES-256
|
|
439
|
+
mock_sha256.return_value = mock_sha256_instance
|
|
440
|
+
|
|
441
|
+
mock_decryptor = MagicMock()
|
|
442
|
+
# Simulate decrypted data with trailing backslash and characters
|
|
443
|
+
mock_decryptor.update.return_value = b'{"key": "value"}\\abc123'
|
|
444
|
+
mock_decryptor.finalize.return_value = b""
|
|
445
|
+
|
|
446
|
+
mock_cipher_instance = MagicMock()
|
|
447
|
+
mock_cipher_instance.decryptor.return_value = mock_decryptor
|
|
448
|
+
mock_cipher.return_value = mock_cipher_instance
|
|
449
|
+
|
|
450
|
+
# Test the method
|
|
451
|
+
result = self.app._decrypt_config("encrypted_string", "Bearer test_token")
|
|
452
|
+
|
|
453
|
+
# Verify the result is cleaned - should remove \abc123
|
|
454
|
+
assert result == '{"key": "value"}'
|
|
455
|
+
|
|
456
|
+
def test_decrypt_config_with_multiple_backslashes(self):
|
|
457
|
+
"""Test _decrypt_config with multiple backslashes in the string"""
|
|
458
|
+
with patch("cryptography.hazmat.primitives.ciphers.Cipher") as mock_cipher:
|
|
459
|
+
with patch("cryptography.hazmat.backends.default_backend") as mock_backend:
|
|
460
|
+
with patch("base64.b64decode") as mock_b64decode:
|
|
461
|
+
with patch("hashlib.sha256") as mock_sha256:
|
|
462
|
+
# Setup mocks
|
|
463
|
+
mock_backend.return_value = "backend"
|
|
464
|
+
mock_b64decode.return_value = b"iv_data_cipher_text"
|
|
465
|
+
mock_sha256_instance = MagicMock()
|
|
466
|
+
mock_sha256_instance.digest.return_value = b"key" * 8 # 32 bytes for AES-256
|
|
467
|
+
mock_sha256.return_value = mock_sha256_instance
|
|
468
|
+
|
|
469
|
+
mock_decryptor = MagicMock()
|
|
470
|
+
# Simulate decrypted data with multiple backslashes but only remove trailing one
|
|
471
|
+
mock_decryptor.update.return_value = b'{"key": "value\\nested"}\\trailing'
|
|
472
|
+
mock_decryptor.finalize.return_value = b""
|
|
473
|
+
|
|
474
|
+
mock_cipher_instance = MagicMock()
|
|
475
|
+
mock_cipher_instance.decryptor.return_value = mock_decryptor
|
|
476
|
+
mock_cipher.return_value = mock_cipher_instance
|
|
477
|
+
|
|
478
|
+
# Test the method
|
|
479
|
+
result = self.app._decrypt_config("encrypted_string", "Bearer test_token")
|
|
480
|
+
|
|
481
|
+
# Verify the result - should keep nested backslash but remove trailing one
|
|
482
|
+
assert result == '{"key": "value\\nested"}'
|
|
97
483
|
|
|
98
484
|
def test_gen_config(self):
|
|
99
485
|
self.app.local_config = True
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pytest
|
|
3
|
+
from click.testing import CliRunner
|
|
4
|
+
from unittest import mock
|
|
5
|
+
import warnings
|
|
6
|
+
import regscale.regscale as regscale
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestRegscaleCLIVersion:
|
|
10
|
+
|
|
11
|
+
@pytest.fixture(autouse=True)
|
|
12
|
+
def patch_env(self, tmp_path):
|
|
13
|
+
"""
|
|
14
|
+
Fixture to patch environment variables for CLI tests to avoid side effects.
|
|
15
|
+
"""
|
|
16
|
+
with mock.patch.dict(
|
|
17
|
+
os.environ,
|
|
18
|
+
{
|
|
19
|
+
"REGSCALE_WORKDIR": str(tmp_path),
|
|
20
|
+
"REGSCALE_USER": "testuser",
|
|
21
|
+
"REGSCALE_PASSWORD": "testpass",
|
|
22
|
+
"REGSCALE_DOMAIN": "https://testdomain.com",
|
|
23
|
+
"REGSCALE_USER_ID": "1",
|
|
24
|
+
"REGSCALE_TOKEN": "token",
|
|
25
|
+
},
|
|
26
|
+
clear=True,
|
|
27
|
+
):
|
|
28
|
+
yield
|
|
29
|
+
|
|
30
|
+
def test_cli_version_command(self):
|
|
31
|
+
"""
|
|
32
|
+
Test that the CLI 'version' command prints the local RegScale version and exits successfully.
|
|
33
|
+
"""
|
|
34
|
+
runner = CliRunner()
|
|
35
|
+
result = runner.invoke(regscale.cli, ["version"])
|
|
36
|
+
assert result.exit_code == 0
|
|
37
|
+
assert regscale.__version__ in result.output
|
|
38
|
+
|
|
39
|
+
def test_cli_version_server(self):
|
|
40
|
+
"""
|
|
41
|
+
Test that the CLI 'version --server' command prints the server version if available.
|
|
42
|
+
Mocks the API call to return a dummy version.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
class DummyApi:
|
|
46
|
+
def __init__(self):
|
|
47
|
+
self.app = type("App", (), {"config": {"domain": "https://test.com"}})()
|
|
48
|
+
|
|
49
|
+
def get(self, url):
|
|
50
|
+
class DummyRes:
|
|
51
|
+
ok = True
|
|
52
|
+
|
|
53
|
+
def json(self):
|
|
54
|
+
return {"version": "1.2.3"}
|
|
55
|
+
|
|
56
|
+
return DummyRes()
|
|
57
|
+
|
|
58
|
+
with mock.patch("regscale.core.app.api.Api", DummyApi):
|
|
59
|
+
runner = CliRunner()
|
|
60
|
+
result = runner.invoke(regscale.cli, ["version", "--server"])
|
|
61
|
+
assert result.exit_code == 0
|
|
62
|
+
assert "1.2.3" in result.output or "Unable to get version from server." not in result.output
|
tests/regscale/test_init.py
CHANGED
|
@@ -20,6 +20,8 @@ airflow_operators_python = types.ModuleType("airflow.operators.python")
|
|
|
20
20
|
|
|
21
21
|
class DummyPythonOperator:
|
|
22
22
|
def __init__(self, *args, **kwargs):
|
|
23
|
+
# This dummy operator is intentionally left empty because it is only used
|
|
24
|
+
# to mock Airflow's PythonOperator for testing purposes. No initialization logic is needed.
|
|
23
25
|
pass
|
|
24
26
|
|
|
25
27
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|