regscale-cli 6.20.3.1__py3-none-any.whl → 6.20.4.1__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.

@@ -23,6 +23,7 @@ from regscale.integrations.scanner_integration import (
23
23
  )
24
24
  from regscale.integrations.variables import ScannerVariables
25
25
  from regscale.models import AssetStatus, IssueSeverity, IssueStatus
26
+ from regscale.integrations.commercial.qualys.qualys_error_handler import QualysErrorHandler
26
27
 
27
28
  logger = logging.getLogger("regscale")
28
29
 
@@ -107,6 +108,13 @@ class QualysTotalCloudJSONLIntegration(JSONLScannerIntegration):
107
108
  logger.warning("Data is not a dictionary")
108
109
  return False, None
109
110
 
111
+ # Check for Qualys errors in the data
112
+ error_details = QualysErrorHandler.extract_error_details(data)
113
+ if error_details.get("has_error"):
114
+ logger.error("Data contains Qualys error response")
115
+ QualysErrorHandler.log_error_details(error_details)
116
+ return False, None
117
+
110
118
  if "HOST_LIST_VM_DETECTION_OUTPUT" not in data:
111
119
  logger.warning("Data does not contain HOST_LIST_VM_DETECTION_OUTPUT")
112
120
  return False, None
@@ -142,85 +150,164 @@ class QualysTotalCloudJSONLIntegration(JSONLScannerIntegration):
142
150
  :return: IntegrationAsset object
143
151
  :rtype: IntegrationAsset
144
152
  """
153
+ # Determine which host data to use
154
+ host_data = self._determine_host_data(file_path, data, host)
155
+
156
+ # Handle None input gracefully
157
+ if host_data is None:
158
+ return self._create_placeholder_asset()
159
+
160
+ # Convert XML Element to dict if necessary
161
+ if not isinstance(host_data, dict) and hasattr(host_data, "tag"):
162
+ host_data = self._xml_element_to_dict(host_data)
163
+
164
+ # Process dictionary data
165
+ if isinstance(host_data, dict):
166
+ return self._create_asset_from_dict(host_data)
167
+
168
+ # If we got here, we don't know how to handle the data
169
+ logger.error(f"Unexpected host data type: {type(host_data)}")
170
+ raise ValueError(f"Cannot parse asset from data type: {type(host_data)}")
171
+
172
+ def _determine_host_data(self, file_path, data, host):
173
+ """
174
+ Determine which host data to use based on provided parameters.
175
+
176
+ :param file_path: File path parameter (may contain host data)
177
+ :param data: Data parameter
178
+ :param host: Host parameter
179
+ :return: Host data to use
180
+ """
145
181
  # Handle the case when the file_path contains the host data (common in tests)
146
182
  if isinstance(file_path, dict) and not host and not data:
147
- host = file_path
183
+ return file_path
148
184
 
149
185
  # Use host parameter if provided (for backward compatibility)
150
- if host is None:
151
- host = data # In this implementation, we treat data as the host
186
+ if host is not None:
187
+ return host
152
188
 
153
- # Handle None input gracefully
154
- if host is None:
155
- logger.warning("No host data provided to parse_asset")
156
- # Generate a placeholder asset with minimal information
157
- return IntegrationAsset(
158
- name="Unknown-Qualys-Asset",
159
- identifier=str(int(time.time())), # Use timestamp as fallback ID
160
- asset_type="Server",
161
- asset_category="IT",
162
- status=AssetStatus.Active,
163
- parent_id=self.plan_id,
164
- parent_module="securityplans",
165
- notes="Generated for missing Qualys data",
166
- )
189
+ # Fall back to data parameter
190
+ return data
167
191
 
168
- # Convert XML Element to dict if necessary
169
- if not isinstance(host, dict) and hasattr(host, "tag"): # It's an XML Element
170
- host = self._xml_element_to_dict(host)
192
+ def _create_placeholder_asset(self):
193
+ """
194
+ Create a placeholder asset when no valid host data is provided.
171
195
 
172
- # Process dictionary
173
- if isinstance(host, dict):
174
- # Check if we got the full data structure or just a host dictionary
175
- if "HOST_LIST_VM_DETECTION_OUTPUT" in host:
176
- # Navigate to the HOST data within the nested structure
177
- try:
178
- host = (
179
- host.get("HOST_LIST_VM_DETECTION_OUTPUT", {})
180
- .get("RESPONSE", {})
181
- .get("HOST_LIST", {})
182
- .get("HOST", {})
183
- )
184
- except (AttributeError, KeyError):
185
- logger.error("Could not navigate to HOST data in dictionary")
186
- raise ValueError("Invalid host data structure")
187
-
188
- # Extract host data from dictionary
189
- host_id = host.get("ID", "")
190
- ip = host.get("IP", "")
191
- dns = host.get("DNS", "")
192
- os = host.get("OS", "")
193
- last_scan = host.get("LAST_SCAN_DATETIME", "")
194
- network_id = host.get("NETWORK_ID", "")
195
-
196
- # Try to get FQDN from DNS_DATA if available
197
- fqdn = None
198
- dns_data = host.get("DNS_DATA", {})
199
- if dns_data:
200
- fqdn = dns_data.get("FQDN", "")
201
-
202
- asset = IntegrationAsset(
203
- name=dns or ip or f"QualysAsset-{host_id}",
204
- identifier=host_id,
205
- asset_type="Server",
206
- asset_category="IT",
207
- ip_address=ip,
208
- fqdn=fqdn,
209
- operating_system=os,
210
- status=AssetStatus.Active,
211
- external_id=host_id,
212
- date_last_updated=last_scan,
213
- mac_address=None,
214
- vlan_id=network_id,
215
- notes=f"Qualys Asset ID: {host_id}",
216
- parent_id=self.plan_id,
217
- parent_module="securityplans",
196
+ :return: IntegrationAsset with placeholder data
197
+ :rtype: IntegrationAsset
198
+ """
199
+ logger.warning("No host data provided to parse_asset")
200
+ return IntegrationAsset(
201
+ name="Unknown-Qualys-Asset",
202
+ identifier=str(int(time.time())), # Use timestamp as fallback ID
203
+ asset_type="Server",
204
+ asset_category="IT",
205
+ status=AssetStatus.Active,
206
+ parent_id=self.plan_id,
207
+ parent_module="securityplans",
208
+ notes="Generated for missing Qualys data",
209
+ )
210
+
211
+ def _create_asset_from_dict(self, host_data):
212
+ """
213
+ Create an IntegrationAsset from dictionary host data.
214
+
215
+ :param host_data: Dictionary containing host information
216
+ :return: IntegrationAsset object
217
+ :rtype: IntegrationAsset
218
+ """
219
+ # Navigate to host data if we have the full structure
220
+ processed_host = self._extract_host_from_structure(host_data)
221
+
222
+ # Extract host information
223
+ host_info = self._extract_host_information(processed_host)
224
+
225
+ # Create and return the asset
226
+ return IntegrationAsset(
227
+ name=host_info["name"],
228
+ identifier=host_info["host_id"],
229
+ asset_type="Server",
230
+ asset_category="IT",
231
+ ip_address=host_info["ip"],
232
+ fqdn=host_info["fqdn"],
233
+ operating_system=host_info["os"],
234
+ status=AssetStatus.Active,
235
+ external_id=host_info["host_id"],
236
+ date_last_updated=host_info["last_scan"],
237
+ mac_address=None,
238
+ vlan_id=host_info["network_id"],
239
+ notes=f"Qualys Asset ID: {host_info['host_id']}",
240
+ parent_id=self.plan_id,
241
+ parent_module="securityplans",
242
+ )
243
+
244
+ def _extract_host_from_structure(self, host_data):
245
+ """
246
+ Extract host data from nested structure if needed.
247
+
248
+ :param host_data: Host data dictionary
249
+ :return: Processed host data
250
+ """
251
+ # Check if we got the full data structure or just a host dictionary
252
+ if "HOST_LIST_VM_DETECTION_OUTPUT" not in host_data:
253
+ return host_data
254
+
255
+ # Navigate to the HOST data within the nested structure
256
+ try:
257
+ return (
258
+ host_data.get("HOST_LIST_VM_DETECTION_OUTPUT", {})
259
+ .get("RESPONSE", {})
260
+ .get("HOST_LIST", {})
261
+ .get("HOST", {})
218
262
  )
219
- return asset
263
+ except (AttributeError, KeyError):
264
+ logger.error("Could not navigate to HOST data in dictionary")
265
+ raise ValueError("Invalid host data structure")
220
266
 
221
- # If we got here, we don't know how to handle the data
222
- logger.error(f"Unexpected host data type: {type(host)}")
223
- raise ValueError(f"Cannot parse asset from data type: {type(host)}")
267
+ def _extract_host_information(self, host):
268
+ """
269
+ Extract host information from host dictionary.
270
+
271
+ :param host: Host dictionary
272
+ :return: Dictionary with extracted host information
273
+ :rtype: dict
274
+ """
275
+ host_id = host.get("ID", "")
276
+ ip = host.get("IP", "")
277
+ dns = host.get("DNS", "")
278
+ os = host.get("OS", "")
279
+ last_scan = host.get("LAST_SCAN_DATETIME", "")
280
+ network_id = host.get("NETWORK_ID", "")
281
+
282
+ # Try to get FQDN from DNS_DATA if available
283
+ fqdn = self._extract_fqdn(host)
284
+
285
+ # Determine asset name
286
+ name = dns or ip or f"QualysAsset-{host_id}"
287
+
288
+ return {
289
+ "host_id": host_id,
290
+ "ip": ip,
291
+ "dns": dns,
292
+ "os": os,
293
+ "last_scan": last_scan,
294
+ "network_id": network_id,
295
+ "fqdn": fqdn,
296
+ "name": name,
297
+ }
298
+
299
+ def _extract_fqdn(self, host):
300
+ """
301
+ Extract FQDN from host DNS_DATA if available.
302
+
303
+ :param host: Host dictionary
304
+ :return: FQDN string or None
305
+ :rtype: Optional[str]
306
+ """
307
+ dns_data = host.get("DNS_DATA", {})
308
+ if dns_data:
309
+ return dns_data.get("FQDN", "")
310
+ return None
224
311
 
225
312
  def parse_finding(
226
313
  self,
@@ -241,37 +328,66 @@ class QualysTotalCloudJSONLIntegration(JSONLScannerIntegration):
241
328
  :return: IntegrationFinding object
242
329
  :rtype: IntegrationFinding
243
330
  """
244
- # For backward compatibility
245
- detection_to_use = detection if detection is not None else item
246
- host_id_to_use = host_id if host_id is not None else asset_identifier
331
+ # Determine which detection and host_id to use
332
+ detection_to_use, host_id_to_use = self._determine_finding_parameters(
333
+ detection, item, host_id, asset_identifier
334
+ )
247
335
 
248
336
  # Handle None input gracefully
249
337
  if detection_to_use is None:
250
- logger.warning("No detection data provided to parse_finding")
251
- # Use host_id or generate a placeholder if none provided
252
- if not host_id_to_use:
253
- host_id_to_use = f"unknown-host-{int(time.time())}"
254
-
255
- # Generate a placeholder finding with minimal information
256
- return IntegrationFinding(
257
- title="Unknown Qualys Finding",
258
- description="No detection data was provided",
259
- severity=IssueSeverity.Low.value,
260
- status=IssueStatus.Open,
261
- plugin_name="QID-unknown",
262
- plugin_id=self.title,
263
- asset_identifier=host_id_to_use,
264
- category="Vulnerability",
265
- scan_date=self.scan_date or get_current_datetime(),
266
- external_id=f"unknown-finding-{int(time.time())}",
267
- )
338
+ return self._create_placeholder_finding(host_id_to_use)
268
339
 
269
340
  # Convert XML Element to dict if necessary
270
- if not isinstance(detection_to_use, dict) and hasattr(detection_to_use, "tag"): # It's an XML Element
341
+ if not isinstance(detection_to_use, dict) and hasattr(detection_to_use, "tag"):
271
342
  detection_to_use = self._xml_element_to_dict(detection_to_use)
272
343
 
273
344
  return self._parse_finding_from_dict(detection_to_use, host_id_to_use)
274
345
 
346
+ def _determine_finding_parameters(self, detection, item, host_id, asset_identifier):
347
+ """
348
+ Determine which detection and host_id to use based on provided parameters.
349
+
350
+ :param detection: Detection parameter
351
+ :param item: Item parameter (for compatibility)
352
+ :param host_id: Host ID parameter
353
+ :param asset_identifier: Asset identifier parameter (for compatibility)
354
+ :return: Tuple of (detection_to_use, host_id_to_use)
355
+ :rtype: tuple
356
+ """
357
+ # For backward compatibility
358
+ detection_to_use = detection if detection is not None else item
359
+ host_id_to_use = host_id if host_id is not None else asset_identifier
360
+
361
+ return detection_to_use, host_id_to_use
362
+
363
+ def _create_placeholder_finding(self, host_id_to_use):
364
+ """
365
+ Create a placeholder finding when no valid detection data is provided.
366
+
367
+ :param host_id_to_use: Host ID to use for the finding
368
+ :return: IntegrationFinding with placeholder data
369
+ :rtype: IntegrationFinding
370
+ """
371
+ logger.warning("No detection data provided to parse_finding")
372
+
373
+ # Use host_id or generate a placeholder if none provided
374
+ if not host_id_to_use:
375
+ host_id_to_use = f"unknown-host-{int(time.time())}"
376
+
377
+ # Generate a placeholder finding with minimal information
378
+ return IntegrationFinding(
379
+ title="Unknown Qualys Finding",
380
+ description="No detection data was provided",
381
+ severity=IssueSeverity.Low.value,
382
+ status=IssueStatus.Open,
383
+ plugin_name="QID-unknown",
384
+ plugin_id=self.title,
385
+ asset_identifier=host_id_to_use,
386
+ category="Vulnerability",
387
+ scan_date=self.scan_date or get_current_datetime(),
388
+ external_id=f"unknown-finding-{int(time.time())}",
389
+ )
390
+
275
391
  def _parse_finding_from_dict(self, detection, host_id):
276
392
  """
277
393
  Parse a finding from a dictionary representation.
@@ -725,13 +841,22 @@ class QualysTotalCloudJSONLIntegration(JSONLScannerIntegration):
725
841
  :return: Dictionary representation of the XML
726
842
  :rtype: Dict[str, Any]
727
843
  """
728
- try:
729
- root = ET.fromstring(xml_string)
730
- return self._xml_element_to_dict(root)
731
- except ET.ParseError as e:
732
- logger.error(f"Error parsing XML: {str(e)}")
844
+ success, parsed_data, error_message = QualysErrorHandler.parse_xml_safely(xml_string)
845
+
846
+ if not success:
847
+ logger.error(f"Failed to parse XML string: {error_message}")
733
848
  return {}
734
849
 
850
+ # Check for Qualys-specific errors
851
+ if parsed_data:
852
+ error_details = QualysErrorHandler.extract_error_details(parsed_data)
853
+ if error_details.get("has_error"):
854
+ logger.error("XML contains Qualys error response")
855
+ QualysErrorHandler.log_error_details(error_details)
856
+ return {}
857
+
858
+ return parsed_data or {}
859
+
735
860
  def _process_xml_dict(self):
736
861
  """Process XML data provided as a dictionary."""
737
862
  logger.debug("Using already parsed XML data (dict)")
@@ -38,7 +38,7 @@ def sync_axonius(regscale_ssp_id: int) -> None:
38
38
  @click.option(
39
39
  "--url",
40
40
  type=click.STRING,
41
- help="The root domain where your CrowdStrike Falcon tenant is located.",
41
+ help="Base URL for the CrowdStrike Falcon Spotlight API.",
42
42
  required=False,
43
43
  )
44
44
  def sync_crowdstrike(regscale_ssp_id: int, url: str) -> None:
@@ -79,6 +79,16 @@ def sync_servicenow(regscale_ssp_id: int) -> None:
79
79
  assets_servicenow.run_sync(regscale_ssp_id=regscale_ssp_id)
80
80
 
81
81
 
82
+ @assets.command(name="sync_sevco")
83
+ @regscale_ssp_id()
84
+ def sync_sevco(regscale_ssp_id: int) -> None:
85
+ """Sync Assets from Sevco to RegScale."""
86
+ from regscale.models.integration_models.synqly_models.connectors import Assets
87
+
88
+ assets_sevco = Assets("sevco")
89
+ assets_sevco.run_sync(regscale_ssp_id=regscale_ssp_id)
90
+
91
+
82
92
  @assets.command(name="sync_tanium_cloud")
83
93
  @regscale_ssp_id()
84
94
  def sync_tanium_cloud(regscale_ssp_id: int) -> None:
@@ -18,7 +18,7 @@ def edr() -> None:
18
18
  @click.option(
19
19
  "--url",
20
20
  type=click.STRING,
21
- help="The root domain where your CrowdStrike Falcon tenant is located.",
21
+ help="Base URL for the CrowdStrike Falcon® API.",
22
22
  required=False,
23
23
  )
24
24
  def sync_crowdstrike(regscale_ssp_id: int, url: str) -> None:
@@ -44,7 +44,7 @@ def sync_defender(regscale_ssp_id: int) -> None:
44
44
  @click.option(
45
45
  "--url",
46
46
  type=click.STRING,
47
- help="URL for the Malwarebytes EDR Provider",
47
+ help="Base URL for the ThreatDown EDR API.",
48
48
  required=False,
49
49
  )
50
50
  def sync_malwarebytes(regscale_ssp_id: int, url: str) -> None:
@@ -60,7 +60,7 @@ def sync_malwarebytes(regscale_ssp_id: int, url: str) -> None:
60
60
  @click.option(
61
61
  "--edr_events_url",
62
62
  type=click.STRING,
63
- help="Base URL for the SentinelOne Singularity Data Lake API. This URL is required if you plan to use the EDR Events API.",
63
+ help="Base URL for the SentinelOne Singularity Data Lake API. This URL is required is required when querying EDR events.",
64
64
  required=False,
65
65
  )
66
66
  def sync_sentinelone(regscale_ssp_id: int, edr_events_url: str) -> None:
@@ -76,7 +76,7 @@ def sync_sentinelone(regscale_ssp_id: int, edr_events_url: str) -> None:
76
76
  @click.option(
77
77
  "--url",
78
78
  type=click.STRING,
79
- help="Optional root domain where your Sophos tenant is located.",
79
+ help="Base URL for the Sophos Endpoint API.",
80
80
  required=False,
81
81
  )
82
82
  def sync_sophos(regscale_ssp_id: int, url: str) -> None:
@@ -134,7 +134,7 @@ def sync_pagerduty(regscale_id: int, regscale_module: str, project: str, name: s
134
134
  @click.option(
135
135
  "--default_project",
136
136
  type=click.STRING,
137
- help="Default Project for the integration. This maps to the custom table for tickets. This table should be derived from Incident table. If not provided, defaults to the incident table.",
137
+ help="Default Project for the integration. This maps to the custom table for tickets. This table should be derived from Incident table. Defaults to the incident table if not specified.",
138
138
  required=False,
139
139
  )
140
140
  def sync_servicenow(regscale_id: int, regscale_module: str, issue_type: str, default_project: str) -> None:
@@ -40,7 +40,7 @@ def vulnerabilities() -> None:
40
40
  @click.option(
41
41
  "--url",
42
42
  type=click.STRING,
43
- help="The root domain where your CrowdStrike Falcon tenant is located.",
43
+ help="Base URL for the CrowdStrike Falcon® Spotlight API.",
44
44
  required=False,
45
45
  )
46
46
  def sync_crowdstrike(regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, all_scans: bool, url: str) -> None:
@@ -207,7 +207,7 @@ def sync_tanium_cloud(regscale_ssp_id: int, vuln_filter: str, scan_date: datetim
207
207
  @click.option(
208
208
  "--url",
209
209
  type=click.STRING,
210
- help='URL for the Tenable Cloud API. This should be the base URL for the API, without any path components and must be HTTPS. If not provided, defaults to "https://cloud.tenable.com".',
210
+ help="Base URL for the Tenable Cloud API.",
211
211
  required=False,
212
212
  )
213
213
  def sync_tenable_cloud(regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, all_scans: bool, url: str) -> None: