regscale-cli 6.20.3.0__py3-none-any.whl → 6.20.4.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.

@@ -0,0 +1,519 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Qualys Error Handler
5
+
6
+ This module provides comprehensive error handling for Qualys API responses
7
+ and XML parsing. It handles various error codes and conditions that can be
8
+ returned by the Qualys API.
9
+ """
10
+
11
+ import logging
12
+ import xml.etree.ElementTree as ET
13
+ from typing import Dict, Optional, Tuple, Union, Any
14
+ import xmltodict
15
+ from requests import Response
16
+
17
+ logger = logging.getLogger("regscale")
18
+
19
+
20
+ class QualysErrorCode:
21
+ """Qualys API error codes and their meanings"""
22
+
23
+ # Authentication errors
24
+ INVALID_LOGIN = "999"
25
+ AUTHENTICATION_FAILED = "1001"
26
+ INVALID_USERNAME_PASSWORD = "1002"
27
+ ACCOUNT_DISABLED = "1003"
28
+
29
+ # Authorization errors
30
+ INSUFFICIENT_PRIVILEGES = "2001"
31
+ FEATURE_NOT_ENABLED = "2002"
32
+ API_ACCESS_DISABLED = "2003"
33
+
34
+ # Request errors
35
+ INVALID_REQUEST = "3001"
36
+ MISSING_REQUIRED_PARAMETER = "3002"
37
+ INVALID_PARAMETER_VALUE = "3003"
38
+ REQUEST_TOO_LARGE = "3004"
39
+
40
+ # Rate limiting
41
+ RATE_LIMIT_EXCEEDED = "4001"
42
+ CONCURRENT_LIMIT_EXCEEDED = "4002"
43
+
44
+ # Server errors
45
+ INTERNAL_SERVER_ERROR = "5001"
46
+ SERVICE_UNAVAILABLE = "5002"
47
+ TIMEOUT = "5003"
48
+ DATABASE_ERROR = "5004"
49
+
50
+ # Data errors
51
+ NO_DATA_FOUND = "6001"
52
+ DATA_PROCESSING_ERROR = "6002"
53
+ INVALID_DATA_FORMAT = "6003"
54
+
55
+
56
+ class QualysErrorHandler:
57
+ """Handler for Qualys API errors and XML parsing issues"""
58
+
59
+ ERROR_CODE_MESSAGES = {
60
+ QualysErrorCode.INVALID_LOGIN: "Invalid login credentials",
61
+ QualysErrorCode.AUTHENTICATION_FAILED: "Authentication failed",
62
+ QualysErrorCode.INVALID_USERNAME_PASSWORD: "Invalid username or password",
63
+ QualysErrorCode.ACCOUNT_DISABLED: "Account is disabled",
64
+ QualysErrorCode.INSUFFICIENT_PRIVILEGES: ("Insufficient privileges for this operation"),
65
+ QualysErrorCode.FEATURE_NOT_ENABLED: "Required feature is not enabled",
66
+ QualysErrorCode.API_ACCESS_DISABLED: "API access is disabled",
67
+ QualysErrorCode.INVALID_REQUEST: "Invalid request format",
68
+ QualysErrorCode.MISSING_REQUIRED_PARAMETER: "Missing required parameter",
69
+ QualysErrorCode.INVALID_PARAMETER_VALUE: "Invalid parameter value",
70
+ QualysErrorCode.REQUEST_TOO_LARGE: "Request is too large",
71
+ QualysErrorCode.RATE_LIMIT_EXCEEDED: "Rate limit exceeded",
72
+ QualysErrorCode.CONCURRENT_LIMIT_EXCEEDED: ("Concurrent request limit exceeded"),
73
+ QualysErrorCode.INTERNAL_SERVER_ERROR: "Internal server error",
74
+ QualysErrorCode.SERVICE_UNAVAILABLE: "Service temporarily unavailable",
75
+ QualysErrorCode.TIMEOUT: "Request timeout",
76
+ QualysErrorCode.DATABASE_ERROR: "Database error",
77
+ QualysErrorCode.NO_DATA_FOUND: "No data found",
78
+ QualysErrorCode.DATA_PROCESSING_ERROR: "Data processing error",
79
+ QualysErrorCode.INVALID_DATA_FORMAT: "Invalid data format",
80
+ }
81
+
82
+ RETRY_CODES = {
83
+ QualysErrorCode.RATE_LIMIT_EXCEEDED,
84
+ QualysErrorCode.CONCURRENT_LIMIT_EXCEEDED,
85
+ QualysErrorCode.SERVICE_UNAVAILABLE,
86
+ QualysErrorCode.TIMEOUT,
87
+ QualysErrorCode.DATABASE_ERROR,
88
+ }
89
+
90
+ FATAL_CODES = {
91
+ QualysErrorCode.INVALID_LOGIN,
92
+ QualysErrorCode.AUTHENTICATION_FAILED,
93
+ QualysErrorCode.INVALID_USERNAME_PASSWORD,
94
+ QualysErrorCode.ACCOUNT_DISABLED,
95
+ QualysErrorCode.INSUFFICIENT_PRIVILEGES,
96
+ QualysErrorCode.API_ACCESS_DISABLED,
97
+ }
98
+
99
+ @staticmethod
100
+ def validate_response(response: Response) -> Tuple[bool, Optional[str], Optional[Dict[str, Any]]]:
101
+ """
102
+ Validate a Qualys API response and check for errors.
103
+
104
+ :param Response response: HTTP response from Qualys API
105
+ :return: Tuple of (is_valid, error_message, parsed_data)
106
+ :rtype: Tuple[bool, Optional[str], Optional[Dict[str, Any]]]
107
+ """
108
+ if not response:
109
+ return False, "No response received from Qualys API", None
110
+
111
+ if not response.ok:
112
+ return False, f"HTTP {response.status_code}: {response.text}", None
113
+
114
+ if not response.text:
115
+ return False, "Empty response from Qualys API", None
116
+
117
+ # Try to parse the XML response
118
+ try:
119
+ parsed_data = xmltodict.parse(response.text)
120
+
121
+ # Check for Qualys-specific errors in the parsed data
122
+ error_info = QualysErrorHandler._check_qualys_errors(parsed_data)
123
+ if error_info:
124
+ return False, error_info, parsed_data
125
+
126
+ return True, None, parsed_data
127
+
128
+ except Exception as e:
129
+ logger.error(f"Failed to parse Qualys response as XML: {e}")
130
+ return False, f"XML parsing error: {e}", None
131
+
132
+ @staticmethod
133
+ def _check_qualys_errors(parsed_data: Dict[str, Any]) -> Optional[str]:
134
+ """
135
+ Check parsed XML data for Qualys-specific error conditions.
136
+
137
+ :param Dict[str, Any] parsed_data: Parsed XML data
138
+ :return: Error message if found, None otherwise
139
+ :rtype: Optional[str]
140
+ """
141
+ # Check for SIMPLE_RETURN error format
142
+ simple_return_error = QualysErrorHandler._check_simple_return_errors(parsed_data)
143
+ if simple_return_error:
144
+ return simple_return_error
145
+
146
+ # Check for HOST_LIST_VM_DETECTION_OUTPUT errors
147
+ detection_error = QualysErrorHandler._check_detection_output_errors(parsed_data)
148
+ if detection_error:
149
+ return detection_error
150
+
151
+ # Check for other common error patterns
152
+ return QualysErrorHandler._check_common_error_patterns(parsed_data)
153
+
154
+ @staticmethod
155
+ def _check_simple_return_errors(parsed_data: Dict[str, Any]) -> Optional[str]:
156
+ """
157
+ Check for SIMPLE_RETURN error format.
158
+
159
+ :param Dict[str, Any] parsed_data: Parsed XML data
160
+ :return: Error message if found, None otherwise
161
+ :rtype: Optional[str]
162
+ """
163
+ if "SIMPLE_RETURN" not in parsed_data:
164
+ return None
165
+
166
+ simple_return = parsed_data["SIMPLE_RETURN"]
167
+ response = simple_return.get("RESPONSE", {})
168
+
169
+ if "TEXT" not in response:
170
+ return None
171
+
172
+ error_text = response["TEXT"]
173
+ logger.error(f"Qualys API error: {error_text}")
174
+
175
+ if "CODE" in response:
176
+ error_code = response["CODE"]
177
+ return QualysErrorHandler._format_error_message(error_code, error_text)
178
+
179
+ return f"Qualys API error: {error_text}"
180
+
181
+ @staticmethod
182
+ def _check_detection_output_errors(parsed_data: Dict[str, Any]) -> Optional[str]:
183
+ """
184
+ Check for HOST_LIST_VM_DETECTION_OUTPUT errors.
185
+
186
+ :param Dict[str, Any] parsed_data: Parsed XML data
187
+ :return: Error message if found, None otherwise
188
+ :rtype: Optional[str]
189
+ """
190
+ if "HOST_LIST_VM_DETECTION_OUTPUT" not in parsed_data:
191
+ return None
192
+
193
+ output = parsed_data["HOST_LIST_VM_DETECTION_OUTPUT"]
194
+ response = output.get("RESPONSE", {})
195
+
196
+ # Check for error text in the response
197
+ if "TEXT" in response and "error" in str(response["TEXT"]).lower():
198
+ error_text = response["TEXT"]
199
+ logger.error(f"Qualys detection output error: {error_text}")
200
+ return f"Qualys detection error: {error_text}"
201
+
202
+ # Check for warning messages (non-fatal)
203
+ QualysErrorHandler._log_warnings_if_present(response)
204
+ return None
205
+
206
+ @staticmethod
207
+ def _log_warnings_if_present(response: Dict[str, Any]) -> None:
208
+ """
209
+ Log warning messages if present in response.
210
+
211
+ :param Dict[str, Any] response: Response data
212
+ """
213
+ if "WARNING" not in response:
214
+ return
215
+
216
+ warning = response["WARNING"]
217
+ if isinstance(warning, dict) and "TEXT" in warning:
218
+ warning_text = warning["TEXT"]
219
+ logger.warning(f"Qualys API warning: {warning_text}")
220
+
221
+ @staticmethod
222
+ def _check_common_error_patterns(parsed_data: Dict[str, Any]) -> Optional[str]:
223
+ """
224
+ Check for other common error patterns.
225
+
226
+ :param Dict[str, Any] parsed_data: Parsed XML data
227
+ :return: Error message if found, None otherwise
228
+ :rtype: Optional[str]
229
+ """
230
+ error_patterns = [
231
+ ("ERROR", "error"),
232
+ ("FAULT", "fault"),
233
+ ("EXCEPTION", "exception"),
234
+ ]
235
+
236
+ for pattern_key, pattern_name in error_patterns:
237
+ error_message = QualysErrorHandler._check_error_pattern(parsed_data, pattern_key, pattern_name)
238
+ if error_message:
239
+ return error_message
240
+
241
+ return None
242
+
243
+ @staticmethod
244
+ def _check_error_pattern(parsed_data: Dict[str, Any], pattern_key: str, pattern_name: str) -> Optional[str]:
245
+ """
246
+ Check for a specific error pattern in parsed data.
247
+
248
+ :param Dict[str, Any] parsed_data: Parsed XML data
249
+ :param str pattern_key: Key to look for in the data
250
+ :param str pattern_name: Human-readable name for the pattern
251
+ :return: Error message if found, None otherwise
252
+ :rtype: Optional[str]
253
+ """
254
+ if pattern_key not in parsed_data:
255
+ return None
256
+
257
+ error_data = parsed_data[pattern_key]
258
+
259
+ if isinstance(error_data, dict) and "TEXT" in error_data:
260
+ error_text = error_data["TEXT"]
261
+ logger.error(f"Qualys {pattern_name} error: {error_text}")
262
+ return f"Qualys {pattern_name} error: {error_text}"
263
+
264
+ return None
265
+
266
+ @staticmethod
267
+ def _format_error_message(error_code: str, error_text: str) -> str:
268
+ """
269
+ Format an error message with code and description.
270
+
271
+ :param str error_code: Qualys error code
272
+ :param str error_text: Error text from API
273
+ :return: Formatted error message
274
+ :rtype: str
275
+ """
276
+ code_description = QualysErrorHandler.ERROR_CODE_MESSAGES.get(error_code, "Unknown error code")
277
+ return f"Qualys Error {error_code}: {code_description} - {error_text}"
278
+
279
+ @staticmethod
280
+ def should_retry(error_code: str) -> bool:
281
+ """
282
+ Determine if a request should be retried based on the error code.
283
+
284
+ :param str error_code: Qualys error code
285
+ :return: True if the request should be retried
286
+ :rtype: bool
287
+ """
288
+ return error_code in QualysErrorHandler.RETRY_CODES
289
+
290
+ @staticmethod
291
+ def is_fatal_error(error_code: str) -> bool:
292
+ """
293
+ Determine if an error is fatal and should stop processing.
294
+
295
+ :param str error_code: Qualys error code
296
+ :return: True if the error is fatal
297
+ :rtype: bool
298
+ """
299
+ return error_code in QualysErrorHandler.FATAL_CODES
300
+
301
+ @staticmethod
302
+ def parse_xml_safely(xml_content: str) -> Tuple[bool, Optional[Dict[str, Any]], Optional[str]]:
303
+ """
304
+ Safely parse XML content with comprehensive error handling.
305
+
306
+ :param str xml_content: XML content to parse
307
+ :return: Tuple of (success, parsed_data, error_message)
308
+ :rtype: Tuple[bool, Optional[Dict[str, Any]], Optional[str]]
309
+ """
310
+ if not xml_content:
311
+ return False, None, "Empty XML content"
312
+
313
+ if not xml_content.strip():
314
+ return False, None, "XML content contains only whitespace"
315
+
316
+ try:
317
+ # First try with xmltodict for better structure
318
+ parsed_data = xmltodict.parse(xml_content)
319
+
320
+ # Check for Qualys-specific errors
321
+ error_info = QualysErrorHandler._check_qualys_errors(parsed_data)
322
+ if error_info:
323
+ return False, parsed_data, error_info
324
+
325
+ return True, parsed_data, None
326
+
327
+ except xmltodict.expat.ExpatError as e:
328
+ logger.error(f"XML parsing error (expat): {e}")
329
+ return False, None, f"XML parsing error: {e}"
330
+
331
+ except ET.ParseError as e:
332
+ logger.error(f"XML parsing error (ElementTree): {e}")
333
+ return False, None, f"XML parsing error: {e}"
334
+
335
+ except Exception as e:
336
+ logger.error(f"Unexpected error parsing XML: {e}")
337
+ return False, None, f"Unexpected XML parsing error: {e}"
338
+
339
+ @staticmethod
340
+ def extract_error_details(parsed_data: Dict[str, Any]) -> Dict[str, Any]:
341
+ """
342
+ Extract detailed error information from parsed Qualys response.
343
+
344
+ :param Dict[str, Any] parsed_data: Parsed XML data
345
+ :return: Dictionary containing error details
346
+ :rtype: Dict[str, Any]
347
+ """
348
+ error_details = QualysErrorHandler._create_empty_error_details()
349
+
350
+ # Check SIMPLE_RETURN format
351
+ QualysErrorHandler._extract_simple_return_details(parsed_data, error_details)
352
+
353
+ # Check for other error formats if no SIMPLE_RETURN error found
354
+ if not error_details["has_error"]:
355
+ QualysErrorHandler._extract_other_error_formats(parsed_data, error_details)
356
+
357
+ return error_details
358
+
359
+ @staticmethod
360
+ def _create_empty_error_details() -> Dict[str, Any]:
361
+ """
362
+ Create an empty error details dictionary with default values.
363
+
364
+ :return: Dictionary with default error detail values
365
+ :rtype: Dict[str, Any]
366
+ """
367
+ return {
368
+ "has_error": False,
369
+ "error_code": None,
370
+ "error_message": None,
371
+ "error_type": None,
372
+ "retry_after": None,
373
+ "additional_info": {},
374
+ }
375
+
376
+ @staticmethod
377
+ def _extract_simple_return_details(parsed_data: Dict[str, Any], error_details: Dict[str, Any]) -> None:
378
+ """
379
+ Extract error details from SIMPLE_RETURN format.
380
+
381
+ :param Dict[str, Any] parsed_data: Parsed XML data
382
+ :param Dict[str, Any] error_details: Error details dictionary to update
383
+ """
384
+ if "SIMPLE_RETURN" not in parsed_data:
385
+ return
386
+
387
+ simple_return = parsed_data["SIMPLE_RETURN"]
388
+ response = simple_return.get("RESPONSE", {})
389
+
390
+ if "TEXT" not in response:
391
+ return
392
+
393
+ error_details["has_error"] = True
394
+ error_details["error_message"] = response["TEXT"]
395
+ error_details["error_type"] = "SIMPLE_RETURN"
396
+
397
+ if "CODE" in response:
398
+ error_details["error_code"] = response["CODE"]
399
+
400
+ # Extract retry-after information if available
401
+ QualysErrorHandler._extract_retry_after_info(response, error_details)
402
+
403
+ @staticmethod
404
+ def _extract_retry_after_info(response: Dict[str, Any], error_details: Dict[str, Any]) -> None:
405
+ """
406
+ Extract retry-after information from response.
407
+
408
+ :param Dict[str, Any] response: Response data
409
+ :param Dict[str, Any] error_details: Error details dictionary to update
410
+ """
411
+ if "ITEM_LIST" not in response:
412
+ return
413
+
414
+ item_list = response["ITEM_LIST"]
415
+ if not isinstance(item_list, dict) or "ITEM" not in item_list:
416
+ return
417
+
418
+ item = item_list["ITEM"]
419
+ if not isinstance(item, dict) or "VALUE" not in item:
420
+ return
421
+
422
+ try:
423
+ error_details["retry_after"] = int(item["VALUE"])
424
+ except (ValueError, TypeError):
425
+ pass
426
+
427
+ @staticmethod
428
+ def _extract_other_error_formats(parsed_data: Dict[str, Any], error_details: Dict[str, Any]) -> None:
429
+ """
430
+ Extract error details from other error formats.
431
+
432
+ :param Dict[str, Any] parsed_data: Parsed XML data
433
+ :param Dict[str, Any] error_details: Error details dictionary to update
434
+ """
435
+ error_keys = ["ERROR", "FAULT", "EXCEPTION"]
436
+
437
+ for error_key in error_keys:
438
+ if QualysErrorHandler._extract_error_format_details(parsed_data, error_details, error_key):
439
+ break # Stop after finding the first error
440
+
441
+ @staticmethod
442
+ def _extract_error_format_details(
443
+ parsed_data: Dict[str, Any], error_details: Dict[str, Any], error_key: str
444
+ ) -> bool:
445
+ """
446
+ Extract error details for a specific error format.
447
+
448
+ :param Dict[str, Any] parsed_data: Parsed XML data
449
+ :param Dict[str, Any] error_details: Error details dictionary to update
450
+ :param str error_key: Error key to look for
451
+ :return: True if error was found and extracted, False otherwise
452
+ :rtype: bool
453
+ """
454
+ if error_key not in parsed_data:
455
+ return False
456
+
457
+ error_info = parsed_data[error_key]
458
+ error_details["has_error"] = True
459
+ error_details["error_type"] = error_key
460
+
461
+ if not isinstance(error_info, dict):
462
+ return True
463
+
464
+ if "TEXT" in error_info:
465
+ error_details["error_message"] = error_info["TEXT"]
466
+ if "CODE" in error_info:
467
+ error_details["error_code"] = error_info["CODE"]
468
+
469
+ # Store additional error information
470
+ QualysErrorHandler._store_additional_error_info(error_info, error_details)
471
+ return True
472
+
473
+ @staticmethod
474
+ def _store_additional_error_info(error_info: Dict[str, Any], error_details: Dict[str, Any]) -> None:
475
+ """
476
+ Store additional error information excluding standard fields.
477
+
478
+ :param Dict[str, Any] error_info: Error information from parsed data
479
+ :param Dict[str, Any] error_details: Error details dictionary to update
480
+ """
481
+ excluded_keys = {"TEXT", "CODE"}
482
+
483
+ for key, value in error_info.items():
484
+ if key not in excluded_keys:
485
+ error_details["additional_info"][key] = value
486
+
487
+ @staticmethod
488
+ def log_error_details(error_details: Dict[str, Any]) -> None:
489
+ """
490
+ Log detailed error information.
491
+
492
+ :param Dict[str, Any] error_details: Error details dictionary
493
+ """
494
+ if not error_details.get("has_error"):
495
+ return
496
+
497
+ error_code = error_details.get("error_code")
498
+ error_message = error_details.get("error_message")
499
+ error_type = error_details.get("error_type")
500
+ retry_after = error_details.get("retry_after")
501
+
502
+ log_message = f"Qualys API Error ({error_type})"
503
+
504
+ if error_code:
505
+ code_description = QualysErrorHandler.ERROR_CODE_MESSAGES.get(error_code, "Unknown error code")
506
+ log_message += f" - Code {error_code}: {code_description}"
507
+
508
+ if error_message:
509
+ log_message += f" - Message: {error_message}"
510
+
511
+ if retry_after:
512
+ log_message += f" - Retry after: {retry_after} seconds"
513
+
514
+ logger.error(log_message)
515
+
516
+ # Log additional information if available
517
+ additional_info = error_details.get("additional_info", {})
518
+ if additional_info:
519
+ logger.debug(f"Additional error information: {additional_info}")