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

Files changed (43) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/api.py +8 -1
  3. regscale/core/app/application.py +49 -3
  4. regscale/core/utils/date.py +16 -16
  5. regscale/integrations/commercial/aqua/aqua.py +1 -1
  6. regscale/integrations/commercial/aws/cli.py +1 -1
  7. regscale/integrations/commercial/defender.py +1 -1
  8. regscale/integrations/commercial/ecr.py +1 -1
  9. regscale/integrations/commercial/ibm.py +1 -1
  10. regscale/integrations/commercial/nexpose.py +1 -1
  11. regscale/integrations/commercial/prisma.py +1 -1
  12. regscale/integrations/commercial/qualys/__init__.py +157 -84
  13. regscale/integrations/commercial/qualys/containers.py +2 -1
  14. regscale/integrations/commercial/qualys/scanner.py +5 -3
  15. regscale/integrations/commercial/snyk.py +14 -4
  16. regscale/integrations/commercial/synqly/ticketing.py +23 -11
  17. regscale/integrations/commercial/veracode.py +15 -4
  18. regscale/integrations/commercial/xray.py +1 -1
  19. regscale/integrations/public/cisa.py +7 -1
  20. regscale/integrations/public/nist_catalog.py +8 -2
  21. regscale/integrations/scanner_integration.py +18 -36
  22. regscale/models/app_models/import_validater.py +5 -1
  23. regscale/models/app_models/mapping.py +3 -1
  24. regscale/models/integration_models/cisa_kev_data.json +139 -4
  25. regscale/models/integration_models/flat_file_importer/__init__.py +36 -22
  26. regscale/models/integration_models/qualys.py +24 -4
  27. regscale/models/integration_models/send_reminders.py +8 -2
  28. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  29. regscale/models/regscale_models/control_implementation.py +40 -0
  30. regscale/models/regscale_models/issue.py +7 -4
  31. regscale/models/regscale_models/parameter.py +3 -2
  32. regscale/models/regscale_models/ports_protocol.py +15 -5
  33. regscale/models/regscale_models/vulnerability.py +1 -1
  34. regscale/utils/graphql_client.py +3 -6
  35. regscale/utils/threading/threadhandler.py +12 -2
  36. {regscale_cli-6.20.6.0.dist-info → regscale_cli-6.20.8.0.dist-info}/METADATA +13 -13
  37. {regscale_cli-6.20.6.0.dist-info → regscale_cli-6.20.8.0.dist-info}/RECORD +43 -42
  38. tests/regscale/core/test_version_regscale.py +62 -0
  39. tests/regscale/test_init.py +2 -0
  40. {regscale_cli-6.20.6.0.dist-info → regscale_cli-6.20.8.0.dist-info}/LICENSE +0 -0
  41. {regscale_cli-6.20.6.0.dist-info → regscale_cli-6.20.8.0.dist-info}/WHEEL +0 -0
  42. {regscale_cli-6.20.6.0.dist-info → regscale_cli-6.20.8.0.dist-info}/entry_points.txt +0 -0
  43. {regscale_cli-6.20.6.0.dist-info → regscale_cli-6.20.8.0.dist-info}/top_level.txt +0 -0
@@ -35,7 +35,7 @@ from regscale.integrations.commercial.qualys.variables import QualysVariables
35
35
  from regscale.integrations.scanner_integration import IntegrationAsset, IntegrationFinding
36
36
  from regscale.integrations.variables import ScannerVariables
37
37
  from regscale.models import Asset, Issue, Search, regscale_models
38
- from regscale.models.app_models.click import NotRequiredIf, regscale_ssp_id, save_output_to
38
+ from regscale.models.app_models.click import NotRequiredIf, regscale_ssp_id, save_output_to, ssp_or_component_id
39
39
  from regscale.models.integration_models.flat_file_importer import FlatFileImporter
40
40
  from regscale.models.integration_models.qualys import (
41
41
  Qualys,
@@ -303,7 +303,7 @@ class FindingProgressTracker:
303
303
 
304
304
 
305
305
  @click.command(name="import_total_cloud")
306
- @regscale_ssp_id()
306
+ @ssp_or_component_id()
307
307
  @click.option(
308
308
  "--include_tags",
309
309
  "-t",
@@ -341,21 +341,37 @@ class FindingProgressTracker:
341
341
  default=True,
342
342
  )
343
343
  def import_total_cloud(
344
- regscale_ssp_id: int,
345
- include_tags: str,
346
- exclude_tags: str,
347
- vulnerability_creation: str,
348
- ssl_verify: bool,
349
- containers: bool,
344
+ regscale_ssp_id: int = None,
345
+ component_id: int = None,
346
+ include_tags: str = None,
347
+ exclude_tags: str = None,
348
+ vulnerability_creation: str = None,
349
+ ssl_verify: bool = None,
350
+ containers: bool = True,
350
351
  ):
351
352
  """
352
353
  Import Qualys Total Cloud Assets and Vulnerabilities using JSONL scanner implementation.
353
354
 
354
355
  This command uses the JSONLScannerIntegration class for improved efficiency and memory management.
355
356
  """
356
- if not validate_regscale_object(regscale_ssp_id, "securityplans"):
357
- logger.warning("SSP #%i is not a valid RegScale Security Plan.", regscale_ssp_id)
358
- return
357
+ # Determine which ID to use and whether it's a component
358
+ if component_id:
359
+ plan_id = component_id
360
+ is_component = True
361
+ if not validate_regscale_object(component_id, "components"):
362
+ logger.warning("Component #%i is not a valid RegScale Component.", component_id)
363
+ return
364
+ elif regscale_ssp_id:
365
+ plan_id = regscale_ssp_id
366
+ is_component = False
367
+ if not validate_regscale_object(regscale_ssp_id, "securityplans"):
368
+ logger.warning("SSP #%i is not a valid RegScale Security Plan.", regscale_ssp_id)
369
+ return
370
+ else:
371
+ raise click.UsageError(
372
+ "You must provide either a --regscale_ssp_id or a --component_id to import Qualys Total Cloud data."
373
+ )
374
+
359
375
  containers_lst = []
360
376
  try:
361
377
  # Configure scanner variables and fetch data
@@ -370,7 +386,7 @@ def import_total_cloud(
370
386
 
371
387
  # Initialize and run integration
372
388
  integration = _initialize_integration(
373
- regscale_ssp_id, response_data, vulnerability_creation, ssl_verify, containers_lst
389
+ plan_id, response_data, vulnerability_creation, ssl_verify, containers_lst, is_component
374
390
  )
375
391
  _run_integration_import(integration)
376
392
 
@@ -483,31 +499,18 @@ def _fetch_qualys_api_data(include_tags, exclude_tags):
483
499
  return None
484
500
 
485
501
 
486
- def _initialize_integration(regscale_ssp_id, response_data, vulnerability_creation, ssl_verify, containers):
487
- """Initialize the scanner integration with appropriate settings.
488
-
489
- :param int regscale_ssp_id: RegScale SSP ID
490
- :param dict response_data: Parsed XML data from API
491
- :param str vulnerability_creation: Vulnerability creation mode
492
- :param bool ssl_verify: SSL verification setting
493
- :param list containers: List of containers
494
- :return: Initialized integration object
495
- """
496
- # Build integration kwargs
502
+ def _initialize_integration(plan_id, response_data, vulnerability_creation, ssl_verify, containers, is_component=False):
497
503
  integration_kwargs = {
498
- "plan_id": regscale_ssp_id,
504
+ "plan_id": plan_id,
499
505
  "xml_data": response_data,
500
506
  "vulnerability_creation": vulnerability_creation or ScannerVariables.vulnerabilityCreation,
501
507
  "ssl_verify": ssl_verify if ssl_verify is not None else ScannerVariables.sslVerify,
502
508
  "containers": containers,
509
+ "is_component": is_component,
503
510
  }
504
-
505
- # Add thread workers if available
506
511
  if hasattr(ScannerVariables, "threadMaxWorkers"):
507
512
  integration_kwargs["max_workers"] = ScannerVariables.threadMaxWorkers
508
513
  logger.debug(f"Using thread max workers: {ScannerVariables.threadMaxWorkers}")
509
-
510
- # Initialize and return integration
511
514
  integration = QualysTotalCloudJSONLIntegration(**integration_kwargs)
512
515
  return integration
513
516
 
@@ -539,7 +542,7 @@ def _run_integration_import(integration):
539
542
 
540
543
 
541
544
  @click.command(name="import_total_cloud_xml")
542
- @regscale_ssp_id()
545
+ @ssp_or_component_id()
543
546
  @click.option(
544
547
  "--xml_file",
545
548
  "-f",
@@ -547,7 +550,7 @@ def _run_integration_import(integration):
547
550
  required=True,
548
551
  help="Path to Qualys Total Cloud XML file to process.",
549
552
  )
550
- def import_total_cloud_from_xml(regscale_ssp_id: int, xml_file: str):
553
+ def import_total_cloud_from_xml(xml_file: str, regscale_ssp_id: int = None, component_id: int = None):
551
554
  """
552
555
  Import Qualys Total Cloud Assets and Vulnerabilities from an existing XML file using JSONL scanner.
553
556
 
@@ -582,9 +585,27 @@ def import_total_cloud_from_xml(regscale_ssp_id: int, xml_file: str):
582
585
  QualysErrorHandler.log_error_details(error_details)
583
586
  return
584
587
 
588
+ # Determine which ID to use and whether it's a component
589
+ if component_id:
590
+ plan_id = component_id
591
+ is_component = True
592
+ if not validate_regscale_object(component_id, "components"):
593
+ logger.warning("Component #%i is not a valid RegScale Component.", component_id)
594
+ return
595
+ elif regscale_ssp_id:
596
+ plan_id = regscale_ssp_id
597
+ is_component = False
598
+ if not validate_regscale_object(regscale_ssp_id, "securityplans"):
599
+ logger.warning("SSP #%i is not a valid RegScale Security Plan.", regscale_ssp_id)
600
+ return
601
+ else:
602
+ raise click.UsageError(
603
+ "You must provide either a --regscale_ssp_id or a --component_id to import Qualys Total Cloud data."
604
+ )
605
+
585
606
  # Initialize the JSONLScannerIntegration implementation
586
607
  integration = QualysTotalCloudJSONLIntegration(
587
- plan_id=regscale_ssp_id, xml_data=response_data, file_path=xml_file
608
+ plan_id=plan_id, xml_data=response_data, file_path=xml_file, is_component=is_component
588
609
  )
589
610
 
590
611
  # Process data and generate JSONL files
@@ -791,7 +812,7 @@ def export_past_scans(save_output_to: Path, days: int, export: bool = True):
791
812
 
792
813
  @qualys.command(name="import_scans")
793
814
  @FlatFileImporter.common_scanner_options(
794
- message="File path to the folder containing Qualys .csv files to process to RegScale.",
815
+ message="File path to the folder containing Qualys .csv or .xlsx files to process to RegScale.",
795
816
  prompt="File path for Qualys files",
796
817
  import_name="qualys",
797
818
  )
@@ -814,16 +835,16 @@ def import_scans(
814
835
  upload_file: bool,
815
836
  ):
816
837
  """
817
- Import vulnerability scans from Qualys CSV files.
838
+ Import vulnerability scans from Qualys CSV or Excel (.xlsx) files.
818
839
 
819
- This command processes Qualys CSV export files and imports assets and vulnerabilities
820
- into RegScale. The CSV files must contain specific required headers.
840
+ This command processes Qualys CSV or Excel export files and imports assets and vulnerabilities
841
+ into RegScale. The files must contain specific required headers.
821
842
 
822
843
  TROUBLESHOOTING:
823
844
  If you encounter "No columns to parse from file" errors, try:
824
845
  1. Run 'regscale qualys validate_csv -f <file_path>' first
825
846
  2. Adjust the --skip_rows parameter (default: 129)
826
- 3. Check that your CSV file has the required headers
847
+ 3. Check that your file has the required headers
827
848
 
828
849
  REQUIRED HEADERS:
829
850
  Severity, Title, Exploitability, CVE ID, Solution, DNS, IP,
@@ -861,7 +882,7 @@ def import_qualys_scans(
861
882
  """
862
883
  Import scans from Qualys
863
884
 
864
- :param os.PathLike[str] folder_path: File path to the folder containing Qualys .csv files to process to RegScale
885
+ :param os.PathLike[str] folder_path: File path to the folder containing Qualys .csv or .xlsx files to process to RegScale
865
886
  :param int regscale_ssp_id: The RegScale SSP ID
866
887
  :param datetime scan_date: The date of the scan
867
888
  :param os.PathLike[str] mappings_path: The path to the mappings file
@@ -876,9 +897,9 @@ def import_qualys_scans(
876
897
  FlatFileImporter.import_files(
877
898
  import_type=Qualys,
878
899
  import_name="Qualys",
879
- file_types=".csv",
900
+ file_types=[".csv", ".xlsx"],
880
901
  folder_path=folder_path,
881
- regscale_ssp_id=regscale_ssp_id,
902
+ object_id=regscale_ssp_id,
882
903
  scan_date=scan_date,
883
904
  mappings_path=mappings_path,
884
905
  disable_mapping=disable_mapping,
@@ -951,13 +972,22 @@ def save_results(save_output_to: Path, scan_id: str):
951
972
  save_scan_results_by_id(save_path=save_output_to, scan_id=scan_id)
952
973
 
953
974
 
975
+ def _resolve_plan_and_component(regscale_ssp_id: int = None, component_id: int = None):
976
+ """
977
+ Utility to resolve plan_id and is_component from regscale_ssp_id and component_id.
978
+ Returns (plan_id, is_component)
979
+ """
980
+ if (regscale_ssp_id is None and component_id is None) or (regscale_ssp_id and component_id):
981
+ raise click.UsageError("You must provide either --regscale_ssp_id or --component_id, but not both.")
982
+ is_component = component_id is not None
983
+ plan_id = component_id if is_component else regscale_ssp_id
984
+ return plan_id, is_component
985
+
986
+
954
987
  @qualys.command(name="sync_qualys")
955
- @click.option(
956
- "--regscale_ssp_id",
957
- type=click.INT,
958
- required=True,
959
- prompt="Enter RegScale System Security Plan ID",
960
- help="The ID number from RegScale of the System Security Plan",
988
+ @ssp_or_component_id(
989
+ ssp_kwargs={"help": "The ID number from RegScale of the System Security Plan."},
990
+ component_kwargs={"help": "The ID number from RegScale of the Component record to sync to."},
961
991
  )
962
992
  @click.option(
963
993
  "--create_issue",
@@ -983,20 +1013,22 @@ def save_results(save_output_to: Path, scan_id: str):
983
1013
  not_required_if=["asset_group_id"],
984
1014
  )
985
1015
  def sync_qualys(
986
- regscale_ssp_id: int,
1016
+ regscale_ssp_id: int = None,
1017
+ component_id: int = None,
987
1018
  create_issue: bool = False,
988
1019
  asset_group_id: int = None,
989
1020
  asset_group_name: str = None,
990
1021
  ):
991
1022
  """
992
- Query Qualys and sync assets & their associated
993
- vulnerabilities to a Security Plan in RegScale.
1023
+ Query Qualys and sync assets & their associated vulnerabilities to a Security Plan or Component in RegScale.
994
1024
  """
1025
+ plan_id, is_component = _resolve_plan_and_component(regscale_ssp_id, component_id)
995
1026
  sync_qualys_to_regscale(
996
- regscale_ssp_id=regscale_ssp_id,
1027
+ plan_id=plan_id,
997
1028
  create_issue=create_issue,
998
1029
  asset_group_id=asset_group_id,
999
1030
  asset_group_name=asset_group_name,
1031
+ is_component=is_component,
1000
1032
  )
1001
1033
 
1002
1034
 
@@ -1259,18 +1291,20 @@ def save_scan_results_by_id(save_path: Path, scan_id: str) -> None:
1259
1291
 
1260
1292
 
1261
1293
  def sync_qualys_to_regscale(
1262
- regscale_ssp_id: int,
1294
+ plan_id: int,
1263
1295
  create_issue: bool = False,
1264
1296
  asset_group_id: int = None,
1265
1297
  asset_group_name: str = None,
1298
+ is_component: bool = False,
1266
1299
  ) -> None:
1267
1300
  """
1268
- Sync Qualys assets and vulnerabilities to a security plan in RegScale
1301
+ Sync Qualys assets and vulnerabilities to a security plan or component in RegScale
1269
1302
 
1270
- :param int regscale_ssp_id: ID # of the SSP in RegScale
1303
+ :param int plan_id: ID # of the SSP or Component in RegScale
1271
1304
  :param bool create_issue: Flag whether to create an issue in RegScale from Qualys vulnerabilities, defaults to False
1272
1305
  :param int asset_group_id: Optional filter for assets in Qualys with an asset group ID, defaults to None
1273
1306
  :param str asset_group_name: Optional filter for assets in Qualys with an asset group name, defaults to None
1307
+ :param bool is_component: Whether the sync is for a component (True) or security plan (False)
1274
1308
  :rtype: None
1275
1309
  """
1276
1310
  # see if user has enterprise license
@@ -1278,21 +1312,21 @@ def sync_qualys_to_regscale(
1278
1312
 
1279
1313
  # check if the user provided an asset group id or name
1280
1314
  if asset_group_id:
1281
- # get the assets from Qualys using the group name
1282
1315
  sync_qualys_assets_and_vulns(
1283
- ssp_id=regscale_ssp_id,
1316
+ ssp_id=plan_id,
1284
1317
  create_issue=create_issue,
1285
1318
  asset_group_filter=asset_group_name,
1319
+ is_component=is_component,
1286
1320
  )
1287
1321
  elif asset_group_name:
1288
- # get the assets from Qualys using the group name
1289
1322
  sync_qualys_assets_and_vulns(
1290
- ssp_id=regscale_ssp_id,
1323
+ ssp_id=plan_id,
1291
1324
  create_issue=create_issue,
1292
1325
  asset_group_filter=asset_group_id,
1326
+ is_component=is_component,
1293
1327
  )
1294
1328
  else:
1295
- sync_qualys_assets_and_vulns(ssp_id=regscale_ssp_id, create_issue=create_issue)
1329
+ sync_qualys_assets_and_vulns(ssp_id=plan_id, create_issue=create_issue, is_component=is_component)
1296
1330
 
1297
1331
 
1298
1332
  def get_scan_results(scans: Any, task: TaskID) -> dict:
@@ -1594,28 +1628,32 @@ def sync_qualys_assets_and_vulns(
1594
1628
  ssp_id: int,
1595
1629
  create_issue: bool,
1596
1630
  asset_group_filter: Optional[Union[int, str]] = None,
1631
+ is_component: bool = False,
1597
1632
  ) -> None:
1598
1633
  """
1599
- Function to query Qualys and sync assets & associated vulnerabilities to RegScale
1634
+ Function to query Qualys and sync assets & associated vulnerabilities to RegScale (Security Plan or Component)
1600
1635
 
1601
- :param int ssp_id: RegScale System Security Plan ID
1636
+ :param int ssp_id: RegScale System Security Plan or Component ID
1602
1637
  :param bool create_issue: Flag to create an issue in RegScale for each vulnerability from Qualys
1603
1638
  :param Optional[Union[int, str]] asset_group_filter: Filter the Qualys assets by an asset group ID or name, if any
1639
+ :param bool is_component: Whether the sync is for a component (True) or security plan (False)
1604
1640
  :rtype: None
1605
1641
  """
1606
1642
  config = _get_config()
1643
+ parent_module = "components" if is_component else "securityplans"
1607
1644
 
1608
- # Get the assets from RegScale with the provided SSP ID
1609
- logger.info("Getting assets from RegScale for SSP #%s...", ssp_id)
1610
- reg_assets = Asset.get_all_by_search(search=Search(parentID=ssp_id, module="securityplans"))
1645
+ # Get the assets from RegScale with the provided parent ID
1646
+ logger.info(f"Getting assets from RegScale for {parent_module} #{ssp_id}...")
1647
+ reg_assets = Asset.get_all_by_search(search=Search(parentID=ssp_id, module=parent_module))
1611
1648
  logger.info(
1612
- "Located %s asset(s) associated with SSP #%s in RegScale.",
1649
+ "Located %s asset(s) associated with %s #%s in RegScale.",
1613
1650
  len(reg_assets),
1651
+ parent_module,
1614
1652
  ssp_id,
1615
1653
  )
1616
1654
  logger.debug(reg_assets)
1617
1655
 
1618
- if qualys_assets := get_qualys_assets_and_scan_results(asset_group_filter):
1656
+ if qualys_assets := get_qualys_assets_and_scan_results(asset_group_filter=asset_group_filter):
1619
1657
  logger.info("Received %s assets from Qualys.", len(qualys_assets))
1620
1658
  logger.debug(qualys_assets)
1621
1659
  else:
@@ -1625,6 +1663,7 @@ def sync_qualys_assets_and_vulns(
1625
1663
  reg_assets=reg_assets,
1626
1664
  ssp_id=ssp_id,
1627
1665
  config=config,
1666
+ is_component=is_component,
1628
1667
  )
1629
1668
  if create_issue:
1630
1669
  # Get vulnerabilities from Qualys for the Qualys assets
@@ -1635,26 +1674,35 @@ def sync_qualys_assets_and_vulns(
1635
1674
  sync_issues(
1636
1675
  ssp_id=ssp_id,
1637
1676
  qualys_assets_and_issues=qualys_assets_and_issues,
1677
+ is_component=is_component,
1638
1678
  )
1639
1679
 
1640
1680
 
1641
- def sync_assets(qualys_assets: list[dict], reg_assets: list[Asset], ssp_id: int, config: dict) -> None:
1681
+ def sync_assets(
1682
+ qualys_assets: list[dict], reg_assets: list[Asset], ssp_id: int, config: dict, is_component: bool = False
1683
+ ) -> None:
1642
1684
  """
1643
- Function to sync Qualys assets to RegScale
1685
+ Function to sync Qualys assets to RegScale (Security Plan or Component)
1644
1686
 
1645
1687
  :param list[dict] qualys_assets: List of Qualys assets
1646
1688
  :param list[Asset] reg_assets: List of RegScale assets
1647
- :param int ssp_id: RegScale System Security Plan ID
1689
+ :param int ssp_id: RegScale System Security Plan or Component ID
1648
1690
  :param dict config: Configuration dictionary
1691
+ :param bool is_component: Whether the sync is for a component (True) or security plan (False)
1649
1692
  :rtype: None
1650
1693
  """
1694
+ parent_module = "components" if is_component else "securityplans"
1651
1695
  update_assets = []
1652
1696
  for qualys_asset in qualys_assets: # you can list as many input dicts as you want here
1653
- # Update parent id to SSP on insert
1697
+ logger.debug("qualys_asset: %s", qualys_asset)
1698
+ if not isinstance(qualys_asset, dict):
1699
+ logger.error("Expected dict, got %s: %s", type(qualys_asset), qualys_asset)
1700
+ continue
1701
+ # Update parent id to SSP or Component on insert
1654
1702
  if lookup_assets := lookup_asset(reg_assets, qualys_asset["ASSET_ID"]):
1655
1703
  for asset in set(lookup_assets):
1656
1704
  asset.parentId = ssp_id
1657
- asset.parentModule = "securityplans"
1705
+ asset.parentModule = parent_module
1658
1706
  asset.otherTrackingNumber = qualys_asset["ID"]
1659
1707
  asset.ipAddress = qualys_asset["IP"]
1660
1708
  asset.qualysId = qualys_asset["ASSET_ID"]
@@ -1666,23 +1714,35 @@ def sync_assets(qualys_assets: list[dict], reg_assets: list[Asset], ssp_id: int,
1666
1714
  except AssertionError as aex:
1667
1715
  logger.error("Asset does not have an id, unable to update!\n%s", aex)
1668
1716
  update_and_insert_assets(
1669
- qualys_assets=qualys_assets, reg_assets=reg_assets, ssp_id=ssp_id, config=config, update_assets=update_assets
1717
+ qualys_assets=qualys_assets,
1718
+ reg_assets=reg_assets,
1719
+ ssp_id=ssp_id,
1720
+ config=config,
1721
+ update_assets=update_assets,
1722
+ is_component=is_component,
1670
1723
  )
1671
1724
 
1672
1725
 
1673
1726
  def update_and_insert_assets(
1674
- qualys_assets: list[dict], reg_assets: list[Asset], ssp_id: int, config: dict, update_assets: list[Asset]
1727
+ qualys_assets: list[dict],
1728
+ reg_assets: list[Asset],
1729
+ ssp_id: int,
1730
+ config: dict,
1731
+ update_assets: list[Asset],
1732
+ is_component: bool = False,
1675
1733
  ) -> None:
1676
1734
  """
1677
- Function to update and insert Qualys assets into RegScale
1735
+ Function to update and insert Qualys assets into RegScale (Security Plan or Component)
1678
1736
 
1679
1737
  :param list[dict] qualys_assets: List of Qualys assets as dictionaries
1680
1738
  :param list[Asset] reg_assets: List of RegScale assets
1681
- :param int ssp_id: RegScale System Security Plan ID
1739
+ :param int ssp_id: RegScale System Security Plan or Component ID
1682
1740
  :param dict config: RegScale CLI Configuration dictionary
1683
1741
  :param list[Asset] update_assets: List of assets to update in RegScale
1742
+ :param bool is_component: Whether the sync is for a component (True) or security plan (False)
1684
1743
  :rtype: None
1685
1744
  """
1745
+ parent_module = "components" if is_component else "securityplans"
1686
1746
  insert_assets = []
1687
1747
  if assets_to_be_inserted := [
1688
1748
  qualys_asset
@@ -1695,7 +1755,7 @@ def update_and_insert_assets(
1695
1755
  name=f'Qualys Asset #{qualys_asset["ASSET_ID"]} IP: {qualys_asset["IP"]}',
1696
1756
  otherTrackingNumber=qualys_asset["ID"],
1697
1757
  parentId=ssp_id,
1698
- parentModule="securityplans",
1758
+ parentModule=parent_module,
1699
1759
  ipAddress=qualys_asset["IP"],
1700
1760
  assetOwnerId=config["userId"],
1701
1761
  assetType="Other",
@@ -1727,22 +1787,28 @@ def update_and_insert_assets(
1727
1787
  logger.error("Unable to Update Qualys Assets to RegScale\n%s", rex)
1728
1788
 
1729
1789
 
1730
- def sync_issues(ssp_id: int, qualys_assets_and_issues: list[dict]) -> None:
1790
+ def sync_issues(ssp_id: int, qualys_assets_and_issues: list[dict], is_component: bool = False) -> None:
1731
1791
  """
1732
- Function to sync Qualys issues to RegScale
1792
+ Function to sync Qualys issues to RegScale (Security Plan or Component)
1733
1793
 
1734
- :param int ssp_id: RegScale System Security Plan ID
1794
+ :param int ssp_id: RegScale System Security Plan or Component ID
1735
1795
  :param list[dict] qualys_assets_and_issues: List of Qualys assets and their issues
1796
+ :param bool is_component: Whether the sync is for a component (True) or security plan (False)
1736
1797
  :rtype: None
1737
1798
  """
1799
+ parent_module = "components" if is_component else "securityplans"
1738
1800
  update_issues = []
1739
1801
  insert_issues = []
1740
1802
  vuln_count = 0
1741
- ssp_assets = Asset.get_all_by_parent(parent_id=ssp_id, parent_module="securityplans")
1803
+ ssp_assets = Asset.get_all_by_parent(parent_id=ssp_id, parent_module=parent_module)
1742
1804
  for asset in qualys_assets_and_issues:
1743
1805
  # Create issues in RegScale from Qualys vulnerabilities
1744
1806
  regscale_issue_updates, regscale_new_issues = create_regscale_issue_from_vuln(
1745
- regscale_ssp_id=ssp_id, qualys_asset=asset, regscale_assets=ssp_assets, vulns=asset["ISSUES"]
1807
+ regscale_ssp_id=ssp_id,
1808
+ qualys_asset=asset,
1809
+ regscale_assets=ssp_assets,
1810
+ vulns=asset["ISSUES"],
1811
+ is_component=is_component,
1746
1812
  )
1747
1813
  update_issues.extend(regscale_issue_updates)
1748
1814
  insert_issues.extend(regscale_new_issues)
@@ -1840,6 +1906,11 @@ def get_qualys_assets_and_scan_results(
1840
1906
  # parse the xml data from response.text and convert it to a dictionary
1841
1907
  # and try to extract the data from the parsed XML dictionary
1842
1908
  asset_data = res_data["HOST_LIST_VM_DETECTION_OUTPUT"]["RESPONSE"]["HOST_LIST"]["HOST"]
1909
+ # Always make asset_data a list
1910
+ if isinstance(asset_data, dict):
1911
+ asset_data = [asset_data]
1912
+ elif not isinstance(asset_data, list):
1913
+ asset_data = []
1843
1914
  # check if we need to paginate the asset data
1844
1915
  if "WARNING" in res_data["HOST_LIST_VM_DETECTION_OUTPUT"]["RESPONSE"]:
1845
1916
  logger.warning("Not all assets were fetched, fetching more assets from Qualys...")
@@ -2025,22 +2096,24 @@ def map_qualys_severity_to_regscale(severity: int) -> tuple[str, str]:
2025
2096
 
2026
2097
 
2027
2098
  def create_regscale_issue_from_vuln(
2028
- regscale_ssp_id: int, qualys_asset: dict, regscale_assets: list[Asset], vulns: dict
2099
+ regscale_ssp_id: int, qualys_asset: dict, regscale_assets: list[Asset], vulns: dict, is_component: bool = False
2029
2100
  ) -> Tuple[list[Issue], list[Issue]]:
2030
2101
  """
2031
- Sync Qualys vulnerabilities to RegScale issues.
2102
+ Sync Qualys vulnerabilities to RegScale issues (Security Plan or Component).
2032
2103
 
2033
- :param int regscale_ssp_id: RegScale SSP ID
2104
+ :param int regscale_ssp_id: RegScale SSP or Component ID
2034
2105
  :param dict qualys_asset: Qualys asset as a dictionary
2035
2106
  :param list[Asset] regscale_assets: list of RegScale assets
2036
2107
  :param dict vulns: dictionary of Qualys vulnerabilities associated with the provided asset
2108
+ :param bool is_component: Whether the sync is for a component (True) or security plan (False)
2037
2109
  :return: list of RegScale issues to update, and a list of issues to be created
2038
2110
  :rtype: Tuple[list[Issue], list[Issue]]
2039
2111
  """
2040
2112
  config = _get_config()
2041
2113
  default_status = config["issues"]["qualys"]["status"]
2042
2114
  regscale_issues = []
2043
- regscale_existing_issues = Issue.get_all_by_parent(parent_id=regscale_ssp_id, parent_module="securityplans")
2115
+ parent_module = "components" if is_component else "securityplans"
2116
+ regscale_existing_issues = Issue.get_all_by_parent(parent_id=regscale_ssp_id, parent_module=parent_module)
2044
2117
  for vuln in vulns.values():
2045
2118
  asset_identifier = None
2046
2119
  severity, key = map_qualys_severity_to_regscale(int(vuln["SEVERITY"]))
@@ -2071,7 +2144,7 @@ def create_regscale_issue_from_vuln(
2071
2144
  dueDate=due_date.strftime(fmt),
2072
2145
  identification="Vulnerability Assessment",
2073
2146
  parentId=regscale_ssp_id,
2074
- parentModule="securityplans",
2147
+ parentModule=parent_module,
2075
2148
  recommendedActions=vuln["ISSUE_DATA"]["SOLUTION"],
2076
2149
  assetIdentifier=asset_identifier,
2077
2150
  )
@@ -150,6 +150,7 @@ def _fetch_paginated_data(endpoint: str, filters: Optional[Dict] = None, limit:
150
150
  """
151
151
  all_items = []
152
152
  page: int = 1
153
+ current_url = None # Ensure current_url is always defined
153
154
 
154
155
  try:
155
156
  # Get authentication
@@ -214,7 +215,7 @@ def _fetch_paginated_data(endpoint: str, filters: Optional[Dict] = None, limit:
214
215
  params = {}
215
216
 
216
217
  except Exception as e:
217
- logger.error("Error fetching data from %s: %s", current_url, e)
218
+ logger.error("Error fetching data from %s: %s", current_url if current_url else "N/A", e)
218
219
  logger.debug(traceback.format_exc())
219
220
 
220
221
  logger.info("Completed: Fetched %s total items from %s", len(all_items), endpoint)
@@ -67,10 +67,12 @@ class QualysTotalCloudJSONLIntegration(JSONLScannerIntegration):
67
67
 
68
68
  :param Any *args: Variable positional arguments
69
69
  :param Any **kwargs: Variable keyword arguments
70
+ :param bool is_component: Whether to upload to a component record (default: False)
70
71
  """
71
72
  self.type = ScannerIntegrationType.VULNERABILITY
72
73
  self.xml_data = kwargs.pop("xml_data", None)
73
74
  self.containers = kwargs.pop("containers", None)
75
+ self.is_component = kwargs.get("is_component", False)
74
76
  # Setting a dummy file path to avoid validation errors
75
77
  if self.xml_data and "file_path" not in kwargs:
76
78
  kwargs["file_path"] = None
@@ -210,7 +212,7 @@ class QualysTotalCloudJSONLIntegration(JSONLScannerIntegration):
210
212
  asset_category="IT",
211
213
  status=AssetStatus.Active,
212
214
  parent_id=self.plan_id,
213
- parent_module="securityplans",
215
+ parent_module="components" if self.is_component else "securityplans",
214
216
  notes="Generated for missing Qualys data",
215
217
  )
216
218
 
@@ -244,7 +246,7 @@ class QualysTotalCloudJSONLIntegration(JSONLScannerIntegration):
244
246
  vlan_id=host_info["network_id"],
245
247
  notes=f"Qualys Asset ID: {host_info['host_id']}",
246
248
  parent_id=self.plan_id,
247
- parent_module="securityplans",
249
+ parent_module="components" if self.is_component else "securityplans",
248
250
  )
249
251
 
250
252
  def _extract_host_from_structure(self, host_data):
@@ -1044,7 +1046,7 @@ class QualysTotalCloudJSONLIntegration(JSONLScannerIntegration):
1044
1046
  mac_address=None,
1045
1047
  notes=f"Qualys Container ID: {container_id}. Image ID: {image_id}. SHA: {sha}",
1046
1048
  parent_id=self.plan_id,
1047
- parent_module="securityplans",
1049
+ parent_module="components" if self.is_component else "securityplans",
1048
1050
  is_virtual=True,
1049
1051
  )
1050
1052
 
@@ -21,10 +21,12 @@ def snyk():
21
21
  message="File path to the folder containing Snyk .xlsx or .json files to process to RegScale.",
22
22
  prompt="File path for Snyk files",
23
23
  import_name="snyk",
24
+ support_component=True,
24
25
  )
25
26
  def import_snyk(
26
27
  folder_path: PathLike[str],
27
28
  regscale_ssp_id: int,
29
+ component_id: int,
28
30
  scan_date: datetime,
29
31
  mappings_path: PathLike[str],
30
32
  disable_mapping: bool,
@@ -36,9 +38,14 @@ def import_snyk(
36
38
  """
37
39
  Import scans, vulnerabilities and assets to RegScale from Snyk export files
38
40
  """
41
+
42
+ if not regscale_ssp_id and not component_id:
43
+ raise click.UsageError("You must provide either a --regscale_ssp_id or a --component_id to import Snyk scans.")
44
+
39
45
  import_synk_files(
40
46
  folder_path=folder_path,
41
- regscale_ssp_id=regscale_ssp_id,
47
+ object_id=component_id if component_id else regscale_ssp_id,
48
+ is_component=bool(component_id),
42
49
  scan_date=scan_date,
43
50
  mappings_path=mappings_path,
44
51
  disable_mapping=disable_mapping,
@@ -51,7 +58,8 @@ def import_snyk(
51
58
 
52
59
  def import_synk_files(
53
60
  folder_path: PathLike[str],
54
- regscale_ssp_id: int,
61
+ object_id: int,
62
+ is_component: bool,
55
63
  scan_date: datetime,
56
64
  mappings_path: PathLike[str],
57
65
  disable_mapping: bool,
@@ -64,7 +72,7 @@ def import_synk_files(
64
72
  Import Snyk scans, vulnerabilities and assets to RegScale from Snyk files
65
73
 
66
74
  :param PathLike[str] folder_path: File path to the folder containing Snyk .xlsx files to process to RegScale
67
- :param int regscale_ssp_id: The RegScale SSP ID
75
+ :param int object_id: The RegScale SSP ID or Component ID
68
76
  :param datetime scan_date: The date of the scan
69
77
  :param PathLike[str] mappings_path: The path to the mappings file
70
78
  :param bool disable_mapping: Whether to disable custom mappings
@@ -72,6 +80,7 @@ def import_synk_files(
72
80
  :param str s3_prefix: The S3 prefix to download the files from
73
81
  :param str aws_profile: The AWS profile to use for S3 access
74
82
  :param Optional[bool] upload_file: Whether to upload the file to RegScale after processing, defaults to True
83
+ :param bool is_component: Whether the object is a component
75
84
  :rtype: None
76
85
  """
77
86
  FlatFileImporter.import_files(
@@ -79,7 +88,7 @@ def import_synk_files(
79
88
  import_name="Snyk",
80
89
  file_types=[".xlsx", ".json"],
81
90
  folder_path=folder_path,
82
- regscale_ssp_id=regscale_ssp_id,
91
+ object_id=object_id,
83
92
  scan_date=scan_date,
84
93
  mappings_path=mappings_path,
85
94
  disable_mapping=disable_mapping,
@@ -87,4 +96,5 @@ def import_synk_files(
87
96
  s3_prefix=s3_prefix,
88
97
  aws_profile=aws_profile,
89
98
  upload_file=upload_file,
99
+ is_component=is_component,
90
100
  )