regscale-cli 6.20.8.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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: regscale-cli
3
- Version: 6.20.8.0
3
+ Version: 6.20.9.0
4
4
  Summary: Command Line Interface (CLI) for bulk processing/loading data into RegScale
5
5
  Home-page: https://github.com/RegScale/regscale-cli
6
6
  Author: Travis Howerton
@@ -1,5 +1,5 @@
1
1
  regscale/__init__.py,sha256=ZygAIkX6Nbjag1czWdQa-yP-GM1mBE_9ss21Xh__JFc,34
2
- regscale/_version.py,sha256=aXLddsHksLv_jqag1IjPFcn2x1LxmKgREyfyqpttkPo,1198
2
+ regscale/_version.py,sha256=gKT5YQ9lUTUBh7pTS4c88lS7iYu1716siI0f5SPm0uc,1198
3
3
  regscale/regscale.py,sha256=xcxnTwEwWgfO3Fnp0LVo32SZCJzAswq3WDZgm21nHnI,30914
4
4
  regscale/airflow/__init__.py,sha256=yMwN0Bz4JbM0nl5qY_hPegxo_O2ilhTOL9PY5Njhn-s,270
5
5
  regscale/airflow/click_dags.py,sha256=H3SUR5jkvInNMv1gu-VG-Ja_H-kH145CpQYNalWNAbE,4520
@@ -36,7 +36,7 @@ regscale/core/lazy_group.py,sha256=S2-nA5tzm47A929NOTqGkzrzKuZQDlq2OAPbNnG1W1Q,2
36
36
  regscale/core/login.py,sha256=-8vy1HVAtv1iARnZh6uzYtwmx8VFYPwLYR0QAf1ttCk,2714
37
37
  regscale/core/app/__init__.py,sha256=nGcCN1vWBAnZzoccIlt0jwWQdegCOrBWOB7LPhQkQSs,96
38
38
  regscale/core/app/api.py,sha256=CSyUCV6haBAQ9IyE1FViJcAfTcoS5GJRaULwnRoAV9U,23499
39
- regscale/core/app/application.py,sha256=SWqDm2JiF62OCv3dMG1OS_5WPKAqWwFC9y9nrqIUX9o,28737
39
+ regscale/core/app/application.py,sha256=6EHkTkx9u9Y1CBz6HYJMGbui4S2Nq8kqIngZfnWkZfg,31407
40
40
  regscale/core/app/logz.py,sha256=8AdBKmquv45JGi5fCMc38JqQg6-FpUONGmqfd5EGwrI,2583
41
41
  regscale/core/app/internal/__init__.py,sha256=rod4nmE7rrYDdbuYF4mCWz49TK_2r-v4tWy1UHW63r0,4808
42
42
  regscale/core/app/internal/admin_actions.py,sha256=hOdma7QGwFblmxEj9UTjiWWmZGUS5_ZFtxL2qZdhf-Q,7443
@@ -341,7 +341,7 @@ regscale/models/integration_models/flat_file_importer/__init__.py,sha256=Z-Gzk6v
341
341
  regscale/models/integration_models/sbom/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
342
342
  regscale/models/integration_models/sbom/cyclone_dx.py,sha256=0pFR0BWBrF5c8_cC_8mj2MXvNOMHOdHbBYXvTVfFAh8,4058
343
343
  regscale/models/integration_models/synqly_models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
344
- regscale/models/integration_models/synqly_models/capabilities.json,sha256=e9dO4myvbF-eRhIAepZXXU1CjEp1gJKxhGqDojk15KU,356660
344
+ regscale/models/integration_models/synqly_models/capabilities.json,sha256=kpHT6EWkFvjSmCOj1UErmPPIPuLhh-AjUjtvAewX36s,356796
345
345
  regscale/models/integration_models/synqly_models/connector_types.py,sha256=8nxptkTexpskySnmL0obNAff_iu_fx6tJ7i1-4hJvao,461
346
346
  regscale/models/integration_models/synqly_models/ocsf_mapper.py,sha256=e2kTOhWSNRnzbgMchMx-7c21pCgSv2DqWnxvajKEKJM,16960
347
347
  regscale/models/integration_models/synqly_models/param.py,sha256=Xt5Zm6lC_VkLj7LF2qXo72TJZHysqttsp5ai0NCf1po,2643
@@ -492,7 +492,7 @@ tests/regscale/test_authorization.py,sha256=fls5ODCYiu0DdkwXFepO_GM-BP6tRaPmMCZW
492
492
  tests/regscale/test_init.py,sha256=O_3Dh7s7ObycIZyd0-Y10NCTKdGnrnotcpU6CUB5pno,4180
493
493
  tests/regscale/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
494
494
  tests/regscale/core/test_api.py,sha256=25AsIT-jg8SrlkPeynUwwm4PvTOrHcRO2Ba_omnNLUY,7512
495
- tests/regscale/core/test_app.py,sha256=8HvRRD37pI_x4VjXyjs1g7JWXr-jFIbK_lYst4tLXjI,18922
495
+ tests/regscale/core/test_app.py,sha256=naFMgYRXWSHmYghhd8zsRJf1MsZwu0F1DClujmp-gtU,40593
496
496
  tests/regscale/core/test_login.py,sha256=Kl7ySS8JU7SzhmupaDexeUH8VOMjtMRJvW8-CimxHqU,1166
497
497
  tests/regscale/core/test_logz.py,sha256=Yf6tAthETLlYOEp3hee3ovDw-WnZ_6fTw3e1rjx4xSw,2621
498
498
  tests/regscale/core/test_sbom_generator.py,sha256=lgzo1HRbkNIIDZIeKiM2JbbIYQsak0BpU0GlvbrcexM,2935
@@ -518,9 +518,9 @@ tests/regscale/models/test_regscale_model.py,sha256=ZsrEZkC4EtdIsoQuayn1xv2gEGcV
518
518
  tests/regscale/models/test_report.py,sha256=eiSvS_zS0aVeL0HBvtmHVvEzcfF9ZFVn2twj5g8KttY,970
519
519
  tests/regscale/models/test_tenable_integrations.py,sha256=PNJC2Zu6lv1xj7y6e1yOsz5FktSU3PRKb5x3n5YG3w0,4072
520
520
  tests/regscale/models/test_user_model.py,sha256=e9olv28qBApgnvK6hFHOgXjUC-pkaV8aGDirEIWASL4,4427
521
- regscale_cli-6.20.8.0.dist-info/LICENSE,sha256=ytNhYQ9Rmhj_m-EX2pPq9Ld6tH5wrqqDYg-fCf46WDU,1076
522
- regscale_cli-6.20.8.0.dist-info/METADATA,sha256=8r3zKQbqO3RijflyHEAEzijvbRq8H7nyXd4fndzlASo,34955
523
- regscale_cli-6.20.8.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
524
- regscale_cli-6.20.8.0.dist-info/entry_points.txt,sha256=cLOaIP1eRv1yZ2u7BvpE3aB4x3kDrDwkpeisKOu33z8,269
525
- regscale_cli-6.20.8.0.dist-info/top_level.txt,sha256=Uv8VUCAdxRm70bgrD4YNEJUmDhBThad_1aaEFGwRByc,15
526
- regscale_cli-6.20.8.0.dist-info/RECORD,,
521
+ regscale_cli-6.20.9.0.dist-info/LICENSE,sha256=ytNhYQ9Rmhj_m-EX2pPq9Ld6tH5wrqqDYg-fCf46WDU,1076
522
+ regscale_cli-6.20.9.0.dist-info/METADATA,sha256=JMEea5DFwZ0PLNWKrLxGzq6yKwrMCWTrp5qPxYCydR4,34955
523
+ regscale_cli-6.20.9.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
524
+ regscale_cli-6.20.9.0.dist-info/entry_points.txt,sha256=cLOaIP1eRv1yZ2u7BvpE3aB4x3kDrDwkpeisKOu33z8,269
525
+ regscale_cli-6.20.9.0.dist-info/top_level.txt,sha256=Uv8VUCAdxRm70bgrD4YNEJUmDhBThad_1aaEFGwRByc,15
526
+ regscale_cli-6.20.9.0.dist-info/RECORD,,
@@ -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 = {"cliConfig": "key: value"}
57
+ mock_response.json.return_value = '{"key": "value"}'
58
58
  mock_get.return_value = mock_response
59
- config = self.app._fetch_config_from_regscale(config=self.app.config)
60
- assert "domain" in config
61
- assert config["userId"] == "test_user_id"
62
- assert config["key"] == "value"
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 = {"cliConfig": "key: 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
- config = self.app._fetch_config_from_regscale(config={})
76
- assert config["domain"] == self.test_domain
77
- assert config["token"] == self.test_token
78
- assert config["userId"] == "test_user_id"
79
- assert config["key"] == "value"
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
- config = self.app._fetch_config_from_regscale(config=self.app.config)
95
- mock_logger_error.assert_called_once()
96
- assert config == {}
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