regscale-cli 6.25.1.0__py3-none-any.whl → 6.27.0.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 (146) hide show
  1. regscale/_version.py +1 -1
  2. regscale/airflow/hierarchy.py +2 -2
  3. regscale/core/app/application.py +19 -4
  4. regscale/core/app/internal/evidence.py +419 -2
  5. regscale/core/app/internal/login.py +0 -1
  6. regscale/core/app/utils/catalog_utils/common.py +1 -1
  7. regscale/dev/code_gen.py +24 -20
  8. regscale/integrations/commercial/jira.py +367 -126
  9. regscale/integrations/commercial/qualys/__init__.py +7 -8
  10. regscale/integrations/commercial/qualys/scanner.py +8 -3
  11. regscale/integrations/commercial/sicura/api.py +14 -13
  12. regscale/integrations/commercial/sicura/commands.py +8 -2
  13. regscale/integrations/commercial/sicura/scanner.py +49 -39
  14. regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
  15. regscale/integrations/commercial/synqly/assets.py +17 -0
  16. regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
  17. regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
  18. regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
  19. regscale/integrations/commercial/tenablev2/commands.py +142 -1
  20. regscale/integrations/commercial/tenablev2/scanner.py +0 -1
  21. regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
  22. regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
  23. regscale/integrations/commercial/wizv2/click.py +64 -79
  24. regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
  25. regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
  26. regscale/integrations/commercial/wizv2/compliance_report.py +161 -165
  27. regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
  28. regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
  29. regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
  30. regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
  31. regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
  32. regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
  33. regscale/integrations/commercial/wizv2/issue.py +1 -1
  34. regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
  35. regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
  36. regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
  37. regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
  38. regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
  39. regscale/integrations/commercial/wizv2/reports.py +1 -1
  40. regscale/integrations/commercial/wizv2/sbom.py +1 -1
  41. regscale/integrations/commercial/wizv2/scanner.py +39 -99
  42. regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
  43. regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
  44. regscale/integrations/commercial/wizv2/variables.py +89 -3
  45. regscale/integrations/compliance_integration.py +60 -41
  46. regscale/integrations/control_matcher.py +377 -0
  47. regscale/integrations/due_date_handler.py +14 -8
  48. regscale/integrations/milestone_manager.py +291 -0
  49. regscale/integrations/public/__init__.py +1 -0
  50. regscale/integrations/public/cci_importer.py +37 -38
  51. regscale/integrations/public/fedramp/click.py +60 -2
  52. regscale/integrations/public/fedramp/docx_parser.py +10 -1
  53. regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
  54. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  55. regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
  56. regscale/integrations/scanner_integration.py +277 -153
  57. regscale/models/integration_models/cisa_kev_data.json +282 -9
  58. regscale/models/integration_models/nexpose.py +36 -10
  59. regscale/models/integration_models/qualys.py +3 -4
  60. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  61. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
  62. regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
  63. regscale/models/locking.py +12 -8
  64. regscale/models/platform.py +1 -2
  65. regscale/models/regscale_models/control_implementation.py +47 -22
  66. regscale/models/regscale_models/issue.py +256 -95
  67. regscale/models/regscale_models/milestone.py +1 -1
  68. regscale/models/regscale_models/regscale_model.py +6 -1
  69. regscale/templates/__init__.py +0 -0
  70. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/METADATA +1 -17
  71. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/RECORD +145 -65
  72. tests/regscale/integrations/commercial/__init__.py +0 -0
  73. tests/regscale/integrations/commercial/conftest.py +28 -0
  74. tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
  75. tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
  76. tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
  77. tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
  78. tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
  79. tests/regscale/integrations/commercial/test_aws.py +3731 -0
  80. tests/regscale/integrations/commercial/test_burp.py +48 -0
  81. tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
  82. tests/regscale/integrations/commercial/test_dependabot.py +341 -0
  83. tests/regscale/integrations/commercial/test_gcp.py +1543 -0
  84. tests/regscale/integrations/commercial/test_gitlab.py +549 -0
  85. tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
  86. tests/regscale/integrations/commercial/test_jira.py +2204 -0
  87. tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
  88. tests/regscale/integrations/commercial/test_okta.py +1228 -0
  89. tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
  90. tests/regscale/integrations/commercial/test_sicura.py +350 -0
  91. tests/regscale/integrations/commercial/test_snow.py +423 -0
  92. tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
  93. tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
  94. tests/regscale/integrations/commercial/test_stig.py +33 -0
  95. tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
  96. tests/regscale/integrations/commercial/test_stigv2.py +406 -0
  97. tests/regscale/integrations/commercial/test_wiz.py +1365 -0
  98. tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
  99. tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
  100. tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
  101. tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
  102. tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
  103. tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
  104. tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
  105. tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
  106. tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
  107. tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
  108. tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
  109. tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
  110. tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
  111. tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
  112. tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
  113. tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
  114. tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
  115. tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
  116. tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
  117. tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
  118. tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
  119. tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
  120. tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
  121. tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
  122. tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
  123. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
  124. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1394 -0
  125. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
  126. tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
  127. tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
  128. tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
  129. tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
  130. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +1132 -0
  131. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +519 -0
  132. tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
  133. tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
  134. tests/regscale/integrations/public/fedramp/__init__.py +1 -0
  135. tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
  136. tests/regscale/integrations/public/test_fedramp.py +301 -0
  137. tests/regscale/integrations/test_control_matcher.py +1397 -0
  138. tests/regscale/integrations/test_control_matching.py +155 -0
  139. tests/regscale/integrations/test_milestone_manager.py +408 -0
  140. tests/regscale/models/test_issue.py +378 -1
  141. regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
  142. /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
  143. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/LICENSE +0 -0
  144. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/WHEEL +0 -0
  145. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/entry_points.txt +0 -0
  146. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/top_level.txt +0 -0
@@ -34,9 +34,8 @@ from regscale.integrations.commercial.qualys.scanner import QualysTotalCloudJSON
34
34
  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
- from regscale.models import Asset, Issue, Search, regscale_models
38
- from regscale.models.app_models.click import NotRequiredIf, regscale_ssp_id, save_output_to, ssp_or_component_id
39
- from regscale.models.integration_models.ecr_models.data import Finding
37
+ from regscale.models import Asset, Issue, Search, regscale_models, IssueStatus, IssueSeverity
38
+ from regscale.models.app_models.click import NotRequiredIf, save_output_to, ssp_or_component_id
40
39
  from regscale.models.integration_models.flat_file_importer import FlatFileImporter
41
40
  from regscale.models.integration_models.qualys import (
42
41
  Qualys,
@@ -2116,7 +2115,7 @@ def lookup_asset(asset_list: list, asset_id: str = None) -> list[Asset]:
2116
2115
  return list(set(asset_list)) or []
2117
2116
 
2118
2117
 
2119
- def map_qualys_severity_to_regscale(severity: int) -> tuple[str, str]:
2118
+ def map_qualys_severity_to_regscale(severity: int) -> tuple[IssueSeverity, str]:
2120
2119
  """
2121
2120
  Map Qualys vulnerability severity to RegScale Issue severity
2122
2121
 
@@ -2125,12 +2124,12 @@ def map_qualys_severity_to_regscale(severity: int) -> tuple[str, str]:
2125
2124
  :rtype: tuple[str, str]
2126
2125
  """
2127
2126
  if severity <= 2:
2128
- return "III - Low - Other Weakness", "low"
2127
+ return IssueSeverity.Low, "low"
2129
2128
  if severity == 3:
2130
- return "II - Moderate - Reportable Condition", "moderate"
2129
+ return IssueSeverity.Moderate, "moderate"
2131
2130
  if severity > 3:
2132
- return "I - High - Significant Deficiency", "high"
2133
- return "IV - Not Assigned", "low"
2131
+ return IssueSeverity.High, "high"
2132
+ return IssueSeverity.NotAssigned, "low"
2134
2133
 
2135
2134
 
2136
2135
  def create_regscale_issue_from_vuln(
@@ -4,6 +4,7 @@ Qualys Total Cloud scanner integration class using JSONLScannerIntegration.
4
4
 
5
5
  import logging
6
6
  import os
7
+ import threading
7
8
  import time
8
9
  import traceback
9
10
  import xml.etree.ElementTree as ET
@@ -1554,17 +1555,21 @@ class QualysTotalCloudJSONLIntegration(JSONLScannerIntegration):
1554
1555
 
1555
1556
  return vulnerability_id
1556
1557
 
1557
- def set_severity_count_for_scan(self, severity: str, scan_history: regscale_models.ScanHistory) -> None:
1558
+ def set_severity_count_for_scan(
1559
+ self, severity: str, scan_history: regscale_models.ScanHistory, lock: Optional[threading.RLock] = None
1560
+ ) -> None:
1558
1561
  """
1559
1562
  Override parent method to ensure Qualys scan history severity counts are properly updated.
1560
1563
  This ensures that the vulnerability counts are accurately reflected in the scan history.
1561
1564
 
1562
1565
  :param str severity: Severity of the vulnerability
1563
1566
  :param regscale_models.ScanHistory scan_history: Scan history object
1567
+ :param Optional[threading.RLock] lock: Thread lock for synchronization
1564
1568
  :rtype: None
1565
1569
  """
1566
- # Use parent method to update severity counts
1567
- super().set_severity_count_for_scan(severity, scan_history)
1570
+ # Use parent method to update severity counts with thread-safe locking
1571
+ # Pass lock if provided, otherwise use our instance lock
1572
+ super().set_severity_count_for_scan(severity, scan_history, lock or self.scan_history_lock)
1568
1573
 
1569
1574
  def create_scan_history(self) -> regscale_models.ScanHistory:
1570
1575
  """
@@ -356,7 +356,7 @@ class SicuraAPI:
356
356
  try:
357
357
  response = self._make_request(
358
358
  "GET",
359
- "/backend/api/jaeger/v1/nodes",
359
+ "/api/jaeger/v1/nodes",
360
360
  params={
361
361
  "verbose": "true",
362
362
  "attributes": "platforms,scannable_profiles,most_recent_scan",
@@ -425,7 +425,7 @@ class SicuraAPI:
425
425
  "scanAttributes": {"platform": platform, "profile": profile},
426
426
  }
427
427
 
428
- result = self._make_request("POST", "/backend/api/jaeger/v1/tasks/", data=payload)
428
+ result = self._make_request("POST", "/api/jaeger/v1/tasks/", data=payload)
429
429
 
430
430
  if result:
431
431
  logger.info(f"Successfully created scan task with ID: {result}")
@@ -448,7 +448,7 @@ class SicuraAPI:
448
448
  """
449
449
  try:
450
450
  response = self._make_request(
451
- "GET", "/backend/api/jaeger/v1/jobs", params={"verbose": "true", self.FILTER_TASK_ID: task_id}
451
+ "GET", "/api/jaeger/v1/jobs", params={"verbose": "true", self.FILTER_TASK_ID: task_id}
452
452
  )
453
453
 
454
454
  # Handle 404 or empty response
@@ -503,7 +503,7 @@ class SicuraAPI:
503
503
  if profile:
504
504
  params["profile"] = profile
505
505
 
506
- response = self._make_request("GET", "/backend/api/jaeger/v1/nodes", params=params)
506
+ response = self._make_request("GET", "/api/jaeger/v1/nodes", params=params)
507
507
 
508
508
  # Handle 404 or empty response
509
509
  if not response or (isinstance(response, list) and not response):
@@ -524,16 +524,17 @@ class SicuraAPI:
524
524
  return None # Return None if no scans available
525
525
 
526
526
  # Calculate summary stats
527
- pass_count = sum(1 for scan in device.get("scans", []) if scan.get("result") == "pass")
528
- fail_count = sum(1 for scan in device.get("scans", []) if scan.get("result") == "fail")
529
- total_count = len(device.get("scans", []))
527
+ scan_results = device.get("scans", {}).get("results", [])
528
+ pass_count = sum(1 for scan in scan_results if scan.get("result") == "pass")
529
+ fail_count = sum(1 for scan in scan_results if scan.get("result") == "fail")
530
+ total_count = len(scan_results)
530
531
 
531
532
  # Create the raw result data
532
533
  result_data = {
533
534
  "device_id": device.get("id"),
534
535
  "fqdn": device.get("fqdn"),
535
536
  "ip_address": device.get("ip_address"),
536
- "scans": device.get("scans", []),
537
+ "scans": scan_results,
537
538
  "summary": {
538
539
  "total": total_count,
539
540
  "pass": pass_count,
@@ -638,7 +639,7 @@ class SicuraAPI:
638
639
  if ip_address:
639
640
  params[self.FILTER_IP_ADDRESS] = ip_address
640
641
 
641
- response = self._make_request("GET", "/backend/api/jaeger/v1/node_templates/", params=params)
642
+ response = self._make_request("GET", "/api/jaeger/v1/node_templates/", params=params)
642
643
 
643
644
  # Handle 404 or empty response
644
645
  if not response or (isinstance(response, dict) and "detail" in response):
@@ -670,7 +671,7 @@ class SicuraAPI:
670
671
  # Use PUT method with the correct endpoint and payload
671
672
  result = self._make_request(
672
673
  "PUT",
673
- f"/backend/api/jaeger/v1/node_templates/{device_id}",
674
+ f"/api/jaeger/v1/node_templates/{device_id}",
674
675
  params={"verbose": "true", "include_controls": "true", "action": "promote"},
675
676
  )
676
677
 
@@ -697,7 +698,7 @@ class SicuraAPI:
697
698
  # Use PUT method with the correct endpoint and payload
698
699
  result = self._make_request(
699
700
  "PUT",
700
- f"/backend/api/jaeger/v1/node_templates/{device_id}",
701
+ f"/api/jaeger/v1/node_templates/{device_id}",
701
702
  params={"verbose": "true", "include_controls": "true", "action": "reject"},
702
703
  )
703
704
 
@@ -721,7 +722,7 @@ class SicuraAPI:
721
722
  :rtype: bool
722
723
  """
723
724
  try:
724
- result = self._make_request("DELETE", f"/backend/api/jaeger/v1/nodes/{device_id}")
725
+ result = self._make_request("DELETE", f"/api/jaeger/v1/nodes/{device_id}")
725
726
 
726
727
  # For DELETE operations, an empty result typically indicates success
727
728
  if result is None or result == "" or (isinstance(result, dict) and not result):
@@ -854,7 +855,7 @@ class SicuraAPI:
854
855
  }
855
856
 
856
857
  logger.info(f"Creating enforcement task for device {device_id} with {len(ce_names)} CE names")
857
- result = self._make_request("POST", "/backend/api/jaeger/v1/tasks/", data=payload)
858
+ result = self._make_request("POST", "/api/jaeger/v1/tasks/", data=payload)
858
859
 
859
860
  if result:
860
861
  logger.info(f"Successfully created enforcement task with ID: {result}")
@@ -46,7 +46,13 @@ def sync_assets(regscale_id: int):
46
46
 
47
47
  @sicura.command(name="sync_findings")
48
48
  @regscale_id(help="RegScale will create and update findings as children of this record.")
49
- def sync_findings(regscale_id: int):
49
+ @click.option(
50
+ "--trigger_scan",
51
+ "-s",
52
+ is_flag=True,
53
+ help="Trigger a new scan on Sicura assets before syncing.",
54
+ )
55
+ def sync_findings(regscale_id: int, trigger_scan: bool):
50
56
  """
51
57
  Sync Sicura findings to RegScale.
52
58
 
@@ -60,7 +66,7 @@ def sync_findings(regscale_id: int):
60
66
  )
61
67
 
62
68
  # Using import_findings method which handles the synchronization
63
- integration.sync_findings(plan_id=regscale_id)
69
+ integration.sync_findings(plan_id=regscale_id, trigger_scan=trigger_scan)
64
70
 
65
71
  logger.info("[bold green]Finding synchronization complete.")
66
72
 
@@ -74,6 +74,10 @@ class SicuraIntegration(ScannerIntegration):
74
74
  logger.warning("No devices found in Sicura")
75
75
  return
76
76
 
77
+ if kwargs.pop("trigger_scan", False):
78
+ logger.info(f"Triggering scans on Sicura {len(devices)} devices...")
79
+ self.trigger_scans(devices)
80
+
77
81
  self.num_findings_to_process = 0
78
82
  findings_count = 0
79
83
 
@@ -206,7 +210,8 @@ class SicuraIntegration(ScannerIntegration):
206
210
  mitigation=mitigation,
207
211
  )
208
212
 
209
- def _extract_scan_data(self, scan: Any) -> dict:
213
+ @staticmethod
214
+ def _extract_scan_data(scan: Any) -> dict:
210
215
  """
211
216
  Extract scan data from the scan object
212
217
 
@@ -235,7 +240,8 @@ class SicuraIntegration(ScannerIntegration):
235
240
  "controls": scan.controls if hasattr(scan, "controls") else {},
236
241
  }
237
242
 
238
- def _extract_control_refs(self, controls: dict) -> tuple:
243
+ @staticmethod
244
+ def _extract_control_refs(controls: dict) -> tuple:
239
245
  """
240
246
  Extract CCI and SRG references from controls
241
247
 
@@ -353,8 +359,9 @@ class SicuraIntegration(ScannerIntegration):
353
359
  baseline=platform,
354
360
  )
355
361
 
362
+ @staticmethod
356
363
  def _create_results_text(
357
- self, title, ce_name, result, description, state, state_reason, cci_ref, srg_refs, is_cci_finding
364
+ title, ce_name, result, description, state, state_reason, cci_ref, srg_refs, is_cci_finding
358
365
  ) -> str:
359
366
  """
360
367
  Create the results text for a finding
@@ -401,42 +408,6 @@ class SicuraIntegration(ScannerIntegration):
401
408
  :rtype: Iterator[IntegrationAsset]
402
409
  """
403
410
  logger.info("Fetching assets from Sicura...")
404
-
405
- # Get all devices
406
- pending_devices = self.api.get_pending_devices()
407
- for pending_device in pending_devices:
408
- print(f"Pending device: {pending_device}")
409
- self.api.accept_pending_device(pending_device.id)
410
- task_id = self.api.create_scan_task(
411
- device_id=pending_device.id,
412
- platform=pending_device.platform,
413
- profile=SicuraProfile.I_MISSION_CRITICAL_CLASSIFIED,
414
- )
415
- if task_id:
416
- self.api.wait_for_scan_results(
417
- task_id=task_id,
418
- fqdn=pending_device.fqdn,
419
- platform=pending_device.platform,
420
- profile=SicuraProfile.I_MISSION_CRITICAL_CLASSIFIED,
421
- )
422
- else:
423
- logger.warning(f"Failed to create scan task for device {pending_device.fqdn}")
424
- continue # Continue to next device if creating scan task failed
425
-
426
- task_id = self.api.create_scan_task(
427
- device_id=pending_device.id, platform=pending_device.platform, profile=SicuraProfile.LEVEL_1_SERVER
428
- )
429
- if task_id:
430
- self.api.wait_for_scan_results(
431
- task_id=task_id,
432
- fqdn=pending_device.fqdn,
433
- platform=pending_device.platform,
434
- profile=SicuraProfile.LEVEL_1_SERVER,
435
- )
436
- else:
437
- logger.warning(f"Failed to create scan task for device {pending_device.fqdn}")
438
- # No continue needed here as we're at the end of the loop iteration
439
-
440
411
  devices = self.api.get_devices()
441
412
 
442
413
  if not devices:
@@ -479,3 +450,42 @@ class SicuraIntegration(ScannerIntegration):
479
450
  )
480
451
 
481
452
  self.asset_progress.update(loading_devices, advance=1)
453
+
454
+ def trigger_and_wait_for_scan(self, device: Device) -> None:
455
+ """
456
+ Trigger a scan and wait for the results
457
+
458
+ :param Device device: The device to trigger a scan for
459
+ :return: None
460
+ """
461
+ task_id = self.api.create_scan_task(
462
+ device_id=device.id,
463
+ platform=device.platforms,
464
+ profile=SicuraProfile.I_MISSION_CRITICAL_CLASSIFIED,
465
+ )
466
+ if task_id:
467
+ self.api.wait_for_scan_results(
468
+ task_id=task_id,
469
+ fqdn=device.fqdn,
470
+ platform=device.platforms,
471
+ profile=SicuraProfile.I_MISSION_CRITICAL_CLASSIFIED,
472
+ )
473
+ else:
474
+ logger.warning(f"Failed to create scan task for device {device.fqdn}")
475
+
476
+ def trigger_scans(self, devices: list[Device]) -> None:
477
+ """
478
+ Trigger scans for a list of devices
479
+
480
+ :param list[Device] devices: The devices to trigger scans for
481
+ :return: None
482
+ """
483
+ if len(devices) > 1:
484
+ from regscale.utils.threading import ThreadManager
485
+
486
+ # use multithreading to trigger scans for multiple devices
487
+ thread_manager = ThreadManager(max_workers=10)
488
+ thread_manager.submit_tasks_from_list(self.trigger_and_wait_for_scan, devices)
489
+ thread_manager.execute_and_verify()
490
+ else:
491
+ self.trigger_and_wait_for_scan(devices[0])
@@ -82,15 +82,15 @@ class STIGInfo(BaseModel):
82
82
  """Data model for STIG Information with optional and required attributes."""
83
83
 
84
84
  version: str
85
- classification: str
85
+ classification: Optional[str] = None
86
86
  customname: Optional[str] = None
87
87
  stigid: str
88
88
  description: Optional[str] = None
89
89
  filename: Optional[str] = None
90
90
  releaseinfo: str
91
91
  title: str
92
- uuid: str
93
- notice: str
92
+ uuid: Optional[str] = None
93
+ notice: Optional[str] = None
94
94
  source: Optional[str] = None
95
95
 
96
96
 
@@ -115,10 +115,10 @@ class Vuln(BaseModel):
115
115
  check_content: Optional[str] = None
116
116
  fix_text: str
117
117
  check_content_ref: Optional[str] = None
118
- weight: str
118
+ weight: Optional[str] = None
119
119
  stigref: Optional[str] = None
120
120
  targetkey: Optional[str] = None
121
- stig_uuid: str
121
+ stig_uuid: Optional[str] = None
122
122
  vuln_discuss: Optional[str] = None
123
123
  ia_controls: Optional[str] = None
124
124
  class_: Optional[str] = None
@@ -74,6 +74,23 @@ def sync_axonius(regscale_ssp_id: int, filter: str) -> None:
74
74
  assets_axonius.run_sync(regscale_ssp_id=regscale_ssp_id, filter=filter.split(";") if filter else [])
75
75
 
76
76
 
77
+ @assets.command(name="sync_claroty_xdome")
78
+ @regscale_ssp_id()
79
+ @click.option(
80
+ "--filter",
81
+ help='STRING: Apply filters to the query. Can be a single filter "field[operator]value" or semicolon-separated filters "field1[op]value1;field2[op]value2"',
82
+ required=False,
83
+ type=str,
84
+ default=None,
85
+ )
86
+ def sync_claroty_xdome(regscale_ssp_id: int, filter: str) -> None:
87
+ """Sync Assets from Claroty Xdome to RegScale."""
88
+ from regscale.models.integration_models.synqly_models.connectors import Assets
89
+
90
+ assets_claroty_xdome = Assets("claroty_xdome")
91
+ assets_claroty_xdome.run_sync(regscale_ssp_id=regscale_ssp_id, filter=filter.split(";") if filter else [])
92
+
93
+
77
94
  @assets.command(name="sync_crowdstrike")
78
95
  @regscale_ssp_id()
79
96
  @click.option(
@@ -44,8 +44,9 @@ def build_query(provider, validate, list_fields):
44
44
  @vulnerabilities.command(name="sync_crowdstrike")
45
45
  @regscale_ssp_id()
46
46
  @click.option(
47
- "--vuln_filter",
48
- help="Filter the vulnerabilities for the selected severity. (Options: critical, high, medium, low, info)",
47
+ "--minimum_severity_filter",
48
+ "-s",
49
+ help="Minimum severity of the vulnerabilities to sync. (Options: critical, high, medium, low, info), e.g. providing high will sync all vulnerabilities with a severity of high and critical.",
49
50
  required=False,
50
51
  type=click.Choice(["critical", "high", "medium", "low", "info"]),
51
52
  default=None,
@@ -78,7 +79,12 @@ def build_query(provider, validate, list_fields):
78
79
  required=False,
79
80
  )
80
81
  def sync_crowdstrike(
81
- regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, all_scans: bool, asset_filter: str, url: str
82
+ regscale_ssp_id: int,
83
+ minimum_severity_filter: str,
84
+ scan_date: datetime,
85
+ all_scans: bool,
86
+ asset_filter: str,
87
+ url: str,
82
88
  ) -> None:
83
89
  """Sync Vulnerabilities from Crowdstrike to RegScale."""
84
90
  from regscale.models.integration_models.synqly_models.connectors import Vulnerabilities
@@ -86,7 +92,7 @@ def sync_crowdstrike(
86
92
  vulnerabilities_crowdstrike = Vulnerabilities("crowdstrike")
87
93
  vulnerabilities_crowdstrike.run_sync(
88
94
  regscale_ssp_id=regscale_ssp_id,
89
- vuln_filter=vuln_filter,
95
+ minimum_severity_filter=minimum_severity_filter,
90
96
  scan_date=scan_date,
91
97
  all_scans=all_scans,
92
98
  filter=asset_filter.split(";") if asset_filter else [],
@@ -97,8 +103,9 @@ def sync_crowdstrike(
97
103
  @vulnerabilities.command(name="sync_nucleus")
98
104
  @regscale_ssp_id()
99
105
  @click.option(
100
- "--vuln_filter",
101
- help="Filter the vulnerabilities for the selected severity. (Options: critical, high, medium, low, info)",
106
+ "--minimum_severity_filter",
107
+ "-s",
108
+ help="Minimum severity of the vulnerabilities to sync. (Options: critical, high, medium, low, info), e.g. providing high will sync all vulnerabilities with a severity of high and critical.",
102
109
  required=False,
103
110
  type=click.Choice(["critical", "high", "medium", "low", "info"]),
104
111
  default=None,
@@ -121,7 +128,7 @@ def sync_crowdstrike(
121
128
  default=None,
122
129
  )
123
130
  def sync_nucleus(
124
- regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, all_scans: bool, asset_filter: str
131
+ regscale_ssp_id: int, minimum_severity_filter: str, scan_date: datetime, all_scans: bool, asset_filter: str
125
132
  ) -> None:
126
133
  """Sync Vulnerabilities from Nucleus to RegScale."""
127
134
  from regscale.models.integration_models.synqly_models.connectors import Vulnerabilities
@@ -129,7 +136,7 @@ def sync_nucleus(
129
136
  vulnerabilities_nucleus = Vulnerabilities("nucleus")
130
137
  vulnerabilities_nucleus.run_sync(
131
138
  regscale_ssp_id=regscale_ssp_id,
132
- vuln_filter=vuln_filter,
139
+ minimum_severity_filter=minimum_severity_filter,
133
140
  scan_date=scan_date,
134
141
  all_scans=all_scans,
135
142
  filter=asset_filter.split(";") if asset_filter else [],
@@ -139,8 +146,9 @@ def sync_nucleus(
139
146
  @vulnerabilities.command(name="sync_qualys_cloud")
140
147
  @regscale_ssp_id()
141
148
  @click.option(
142
- "--vuln_filter",
143
- help="Filter the vulnerabilities for the selected severity. (Options: critical, high, medium, low, info)",
149
+ "--minimum_severity_filter",
150
+ "-s",
151
+ help="Minimum severity of the vulnerabilities to sync. (Options: critical, high, medium, low, info), e.g. providing high will sync all vulnerabilities with a severity of high and critical.",
144
152
  required=False,
145
153
  type=click.Choice(["critical", "high", "medium", "low", "info"]),
146
154
  default=None,
@@ -167,7 +175,7 @@ def sync_nucleus(
167
175
  default=None,
168
176
  )
169
177
  def sync_qualys_cloud(
170
- regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, all_scans: bool, asset_filter: str
178
+ regscale_ssp_id: int, minimum_severity_filter: str, scan_date: datetime, all_scans: bool, asset_filter: str
171
179
  ) -> None:
172
180
  """Sync Vulnerabilities from Qualys Cloud to RegScale."""
173
181
  from regscale.models.integration_models.synqly_models.connectors import Vulnerabilities
@@ -175,7 +183,7 @@ def sync_qualys_cloud(
175
183
  vulnerabilities_qualys_cloud = Vulnerabilities("qualys_cloud")
176
184
  vulnerabilities_qualys_cloud.run_sync(
177
185
  regscale_ssp_id=regscale_ssp_id,
178
- vuln_filter=vuln_filter,
186
+ minimum_severity_filter=minimum_severity_filter,
179
187
  scan_date=scan_date,
180
188
  all_scans=all_scans,
181
189
  filter=asset_filter.split(";") if asset_filter else [],
@@ -185,8 +193,9 @@ def sync_qualys_cloud(
185
193
  @vulnerabilities.command(name="sync_rapid7_insight_cloud")
186
194
  @regscale_ssp_id()
187
195
  @click.option(
188
- "--vuln_filter",
189
- help="Filter the vulnerabilities for the selected severity. (Options: critical, high, medium, low, info)",
196
+ "--minimum_severity_filter",
197
+ "-s",
198
+ help="Minimum severity of the vulnerabilities to sync. (Options: critical, high, medium, low, info), e.g. providing high will sync all vulnerabilities with a severity of high and critical.",
190
199
  required=False,
191
200
  type=click.Choice(["critical", "high", "medium", "low", "info"]),
192
201
  default=None,
@@ -213,7 +222,7 @@ def sync_qualys_cloud(
213
222
  default=None,
214
223
  )
215
224
  def sync_rapid7_insight_cloud(
216
- regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, all_scans: bool, asset_filter: str
225
+ regscale_ssp_id: int, minimum_severity_filter: str, scan_date: datetime, all_scans: bool, asset_filter: str
217
226
  ) -> None:
218
227
  """Sync Vulnerabilities from Rapid7 Insight Cloud to RegScale."""
219
228
  from regscale.models.integration_models.synqly_models.connectors import Vulnerabilities
@@ -221,7 +230,7 @@ def sync_rapid7_insight_cloud(
221
230
  vulnerabilities_rapid7_insight_cloud = Vulnerabilities("rapid7_insight_cloud")
222
231
  vulnerabilities_rapid7_insight_cloud.run_sync(
223
232
  regscale_ssp_id=regscale_ssp_id,
224
- vuln_filter=vuln_filter,
233
+ minimum_severity_filter=minimum_severity_filter,
225
234
  scan_date=scan_date,
226
235
  all_scans=all_scans,
227
236
  filter=asset_filter.split(";") if asset_filter else [],
@@ -231,8 +240,9 @@ def sync_rapid7_insight_cloud(
231
240
  @vulnerabilities.command(name="sync_servicenow_vr")
232
241
  @regscale_ssp_id()
233
242
  @click.option(
234
- "--vuln_filter",
235
- help="Filter the vulnerabilities for the selected severity. (Options: critical, high, medium, low, info)",
243
+ "--minimum_severity_filter",
244
+ "-s",
245
+ help="Minimum severity of the vulnerabilities to sync. (Options: critical, high, medium, low, info), e.g. providing high will sync all vulnerabilities with a severity of high and critical.",
236
246
  required=False,
237
247
  type=click.Choice(["critical", "high", "medium", "low", "info"]),
238
248
  default=None,
@@ -259,7 +269,7 @@ def sync_rapid7_insight_cloud(
259
269
  default=None,
260
270
  )
261
271
  def sync_servicenow_vr(
262
- regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, all_scans: bool, asset_filter: str
272
+ regscale_ssp_id: int, minimum_severity_filter: str, scan_date: datetime, all_scans: bool, asset_filter: str
263
273
  ) -> None:
264
274
  """Sync Vulnerabilities from Servicenow Vr to RegScale."""
265
275
  from regscale.models.integration_models.synqly_models.connectors import Vulnerabilities
@@ -267,7 +277,7 @@ def sync_servicenow_vr(
267
277
  vulnerabilities_servicenow_vr = Vulnerabilities("servicenow_vr")
268
278
  vulnerabilities_servicenow_vr.run_sync(
269
279
  regscale_ssp_id=regscale_ssp_id,
270
- vuln_filter=vuln_filter,
280
+ minimum_severity_filter=minimum_severity_filter,
271
281
  scan_date=scan_date,
272
282
  all_scans=all_scans,
273
283
  filter=asset_filter.split(";") if asset_filter else [],
@@ -277,8 +287,9 @@ def sync_servicenow_vr(
277
287
  @vulnerabilities.command(name="sync_tanium_cloud")
278
288
  @regscale_ssp_id()
279
289
  @click.option(
280
- "--vuln_filter",
281
- help="Filter the vulnerabilities for the selected severity. (Options: critical, high, medium, low, info)",
290
+ "--minimum_severity_filter",
291
+ "-s",
292
+ help="Minimum severity of the vulnerabilities to sync. (Options: critical, high, medium, low, info), e.g. providing high will sync all vulnerabilities with a severity of high and critical.",
282
293
  required=False,
283
294
  type=click.Choice(["critical", "high", "medium", "low", "info"]),
284
295
  default=None,
@@ -305,7 +316,7 @@ def sync_servicenow_vr(
305
316
  default=None,
306
317
  )
307
318
  def sync_tanium_cloud(
308
- regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, all_scans: bool, asset_filter: str
319
+ regscale_ssp_id: int, minimum_severity_filter: str, scan_date: datetime, all_scans: bool, asset_filter: str
309
320
  ) -> None:
310
321
  """Sync Vulnerabilities from Tanium Cloud to RegScale."""
311
322
  from regscale.models.integration_models.synqly_models.connectors import Vulnerabilities
@@ -313,7 +324,7 @@ def sync_tanium_cloud(
313
324
  vulnerabilities_tanium_cloud = Vulnerabilities("tanium_cloud")
314
325
  vulnerabilities_tanium_cloud.run_sync(
315
326
  regscale_ssp_id=regscale_ssp_id,
316
- vuln_filter=vuln_filter,
327
+ minimum_severity_filter=minimum_severity_filter,
317
328
  scan_date=scan_date,
318
329
  all_scans=all_scans,
319
330
  filter=asset_filter.split(";") if asset_filter else [],
@@ -323,8 +334,9 @@ def sync_tanium_cloud(
323
334
  @vulnerabilities.command(name="sync_tenable_cloud")
324
335
  @regscale_ssp_id()
325
336
  @click.option(
326
- "--vuln_filter",
327
- help="Filter the vulnerabilities for the selected severity. (Options: critical, high, medium, low, info)",
337
+ "--minimum_severity_filter",
338
+ "-s",
339
+ help="Minimum severity of the vulnerabilities to sync. (Options: critical, high, medium, low, info), e.g. providing high will sync all vulnerabilities with a severity of high and critical.",
328
340
  required=False,
329
341
  type=click.Choice(["critical", "high", "medium", "low", "info"]),
330
342
  default=None,
@@ -357,7 +369,12 @@ def sync_tanium_cloud(
357
369
  required=False,
358
370
  )
359
371
  def sync_tenable_cloud(
360
- regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, all_scans: bool, asset_filter: str, url: str
372
+ regscale_ssp_id: int,
373
+ minimum_severity_filter: str,
374
+ scan_date: datetime,
375
+ all_scans: bool,
376
+ asset_filter: str,
377
+ url: str,
361
378
  ) -> None:
362
379
  """Sync Vulnerabilities from Tenable Cloud to RegScale."""
363
380
  from regscale.models.integration_models.synqly_models.connectors import Vulnerabilities
@@ -365,7 +382,7 @@ def sync_tenable_cloud(
365
382
  vulnerabilities_tenable_cloud = Vulnerabilities("tenable_cloud")
366
383
  vulnerabilities_tenable_cloud.run_sync(
367
384
  regscale_ssp_id=regscale_ssp_id,
368
- vuln_filter=vuln_filter,
385
+ minimum_severity_filter=minimum_severity_filter,
369
386
  scan_date=scan_date,
370
387
  all_scans=all_scans,
371
388
  filter=asset_filter.split(";") if asset_filter else [],