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
@@ -12,8 +12,9 @@ from typing import Any, Dict, List, Optional, Tuple, Union
12
12
  from regscale.core.app.utils.app_utils import check_file_path, error_and_exit, get_current_datetime
13
13
  from regscale.core.utils import get_base_protocol_from_port
14
14
  from regscale.core.utils.date import format_to_regscale_iso
15
- from regscale.integrations.commercial.wizv2.async_client import run_async_queries
16
- from regscale.integrations.commercial.wizv2.constants import (
15
+ from regscale.integrations.commercial.wizv2.core.client import run_async_queries
16
+ from regscale.integrations.commercial.wizv2.core.file_operations import FileOperations
17
+ from regscale.integrations.commercial.wizv2.core.constants import (
17
18
  END_OF_LIFE_FILE_PATH,
18
19
  EXTERNAL_ATTACK_SURFACE_FILE_PATH,
19
20
  INVENTORY_FILE_PATH,
@@ -44,7 +45,7 @@ from regscale.integrations.commercial.wizv2.utils import (
44
45
  )
45
46
  from regscale.integrations.commercial.wizv2.variables import WizVariables
46
47
  from regscale.integrations.variables import ScannerVariables
47
- from regscale.integrations.commercial.wizv2.wiz_auth import wiz_authenticate
48
+ from regscale.integrations.commercial.wizv2.core.auth import wiz_authenticate
48
49
  from regscale.integrations.scanner_integration import (
49
50
  IntegrationAsset,
50
51
  IntegrationFinding,
@@ -442,30 +443,13 @@ class WizVulnerabilityIntegration(ScannerIntegration):
442
443
  :return: Results in the same format as async queries
443
444
  :rtype: List[Tuple[str, List[Dict[str, Any]], Optional[Exception]]]
444
445
  """
445
-
446
- results = []
447
446
  cache_task = self.finding_progress.add_task("[green]Loading cached Wiz data...", total=len(query_configs))
448
447
 
449
- for config in query_configs:
450
- query_type = config["type"].value
451
- file_path = config.get("file_path")
452
-
453
- try:
454
- if file_path and os.path.exists(file_path):
455
- with open(file_path, encoding="utf-8") as file:
456
- nodes = json.load(file)
457
-
458
- logger.info(f"Loaded {len(nodes)} cached {query_type} findings from {file_path}")
459
- results.append((query_type, nodes, None))
460
- else:
461
- logger.warning(f"No cached data found for {query_type}")
462
- results.append((query_type, [], None))
448
+ def progress_callback(query_type: str, status: str):
449
+ if status == "loaded":
450
+ self.finding_progress.advance(cache_task, 1)
463
451
 
464
- except Exception as e:
465
- logger.error(f"Error loading cached data for {query_type}: {e}")
466
- results.append((query_type, [], e))
467
-
468
- self.finding_progress.advance(cache_task, 1)
452
+ results = FileOperations.load_cached_findings(query_configs, progress_callback)
469
453
 
470
454
  self.finding_progress.update(
471
455
  cache_task, description=f"[green]✓ Loaded cached data for {len(query_configs)} query types"
@@ -484,21 +468,10 @@ class WizVulnerabilityIntegration(ScannerIntegration):
484
468
  if not file_path:
485
469
  return
486
470
 
487
- try:
488
- from regscale.core.app.utils.app_utils import check_file_path
489
-
490
- # Ensure directory exists
491
- check_file_path(os.path.dirname(file_path))
492
-
493
- # Save data to file
494
- with open(file_path, "w", encoding="utf-8") as file:
495
- json.dump(nodes, file)
496
-
471
+ success = FileOperations.save_json_file(nodes, file_path, create_dir=True)
472
+ if success:
497
473
  logger.debug(f"Saved {len(nodes)} nodes to cache file: {file_path}")
498
474
 
499
- except Exception as e:
500
- logger.warning(f"Failed to save data to cache file {file_path}: {e}")
501
-
502
475
  def fetch_findings_sync(self, **kwargs) -> Iterator[IntegrationFinding]:
503
476
  """
504
477
  Original synchronous method for fetching findings (renamed for fallback)
@@ -760,7 +733,8 @@ class WizVulnerabilityIntegration(ScannerIntegration):
760
733
  else:
761
734
  filtered_out_count += 1
762
735
  logger.info(
763
- f"🚫 FILTERED BY SEVERITY: Vulnerability {wiz_id} with severity '{wiz_severity}' filtered due to minimumSeverity configuration"
736
+ f"🚫 FILTERED BY SEVERITY: Vulnerability {wiz_id} with severity '{wiz_severity}' "
737
+ f"filtered due to minimumSeverity configuration"
764
738
  )
765
739
 
766
740
  logger.info(
@@ -870,6 +844,9 @@ class WizVulnerabilityIntegration(ScannerIntegration):
870
844
  if asset_obj := node.get(field):
871
845
  if provider_id := asset_obj.get("providerId"):
872
846
  return provider_id
847
+ # For vulnerability nodes, use asset ID if providerId is not available
848
+ if field == "vulnerableAsset" and (asset_id := asset_obj.get("id")):
849
+ return asset_id
873
850
 
874
851
  return ""
875
852
 
@@ -993,7 +970,8 @@ class WizVulnerabilityIntegration(ScannerIntegration):
993
970
 
994
971
  if comments := comments_dict.get("comments", {}).get("edges", []):
995
972
  formatted_comments = [
996
- f"{edge.get('node', {}).get('author', {}).get('name', 'Unknown')}: {edge.get('node', {}).get('body', 'No comment')}"
973
+ f"{edge.get('node', {}).get('author', {}).get('name', 'Unknown')}: "
974
+ f"{edge.get('node', {}).get('body', 'No comment')}"
997
975
  for edge in comments
998
976
  ]
999
977
  # Join with newlines
@@ -1925,37 +1903,27 @@ class WizVulnerabilityIntegration(ScannerIntegration):
1925
1903
  :return: List of nodes as dictionaries
1926
1904
  :rtype: List[Dict]
1927
1905
  """
1928
- fetch_interval = datetime.timedelta(hours=WizVariables.wizFullPullLimitHours or 8) # Interval to fetch new data
1929
- current_time = datetime.datetime.now()
1930
- check_file_path(os.path.dirname(file_path))
1906
+ max_age_hours = WizVariables.wizFullPullLimitHours or 8
1931
1907
 
1932
- if os.path.exists(file_path):
1933
- file_mod_time = datetime.datetime.fromtimestamp(os.path.getmtime(file_path))
1934
- if current_time - file_mod_time < fetch_interval:
1935
- logger.info("File %s is newer than %s hours. Using cached data...", file_path, fetch_interval)
1936
- with open(file_path, encoding="utf-8") as file:
1937
- return json.load(file)
1938
- else:
1939
- logger.info("File %s is older than %s hours. Fetching new data...", file_path, fetch_interval)
1940
- else:
1941
- logger.info("File %s does not exist. Fetching new data...", file_path)
1908
+ def fetch_fresh_data():
1909
+ # Ensure we have a valid token (should already be set by caller)
1910
+ if not self.wiz_token:
1911
+ error_and_exit("Wiz token is not set. Please authenticate before calling fetch_wiz_data_if_needed.")
1942
1912
 
1943
- self.authenticate(WizVariables.wizClientId, WizVariables.wizClientSecret)
1913
+ return fetch_wiz_data(
1914
+ query=query,
1915
+ variables=variables,
1916
+ api_endpoint_url=WizVariables.wizUrl,
1917
+ token=self.wiz_token,
1918
+ topic_key=topic_key,
1919
+ )
1944
1920
 
1945
- if not self.wiz_token:
1946
- error_and_exit("Wiz token is not set. Please authenticate first.")
1947
-
1948
- nodes = fetch_wiz_data(
1949
- query=query,
1950
- variables=variables,
1951
- api_endpoint_url=WizVariables.wizUrl,
1952
- token=self.wiz_token,
1953
- topic_key=topic_key,
1921
+ return FileOperations.load_cache_or_fetch(
1922
+ file_path=file_path,
1923
+ fetch_fn=fetch_fresh_data,
1924
+ max_age_hours=max_age_hours,
1925
+ save_cache=True,
1954
1926
  )
1955
- with open(file_path, "w", encoding="utf-8") as file:
1956
- json.dump(nodes, file)
1957
-
1958
- return nodes
1959
1927
 
1960
1928
  def get_asset_by_identifier(self, identifier: str) -> Optional[regscale_models.Asset]:
1961
1929
  """
@@ -2023,39 +1991,11 @@ class WizVulnerabilityIntegration(ScannerIntegration):
2023
1991
  :return: Tuple of (asset_info, source_file) or (None, None)
2024
1992
  :rtype: tuple[Optional[Dict], Optional[str]]
2025
1993
  """
2026
- for file_path in file_paths:
2027
- if not os.path.exists(file_path):
2028
- continue
2029
-
2030
- try:
2031
- asset_info = self._search_single_file(identifier, file_path)
2032
- if asset_info:
2033
- return asset_info, file_path
2034
- except (OSError, json.JSONDecodeError) as e:
2035
- logger.debug("Error reading %s: %s", file_path, e)
2036
- continue
2037
-
2038
- return None, None
2039
-
2040
- def _search_single_file(self, identifier: str, file_path: str) -> Optional[Dict]:
2041
- """
2042
- Search for asset in a single JSON file
2043
-
2044
- :param str identifier: Asset identifier to search for
2045
- :param str file_path: Path to JSON file
2046
- :return: Asset data if found, None otherwise
2047
- :rtype: Optional[Dict]
2048
- """
2049
- logger.debug("Searching for asset %s in %s", identifier, file_path)
2050
-
2051
- with open(file_path, encoding="utf-8") as f:
2052
- data = json.load(f)
2053
-
2054
- if not isinstance(data, list):
2055
- return None
2056
-
2057
- # Use generator expression for memory efficiency
2058
- return next((item for item in data if self._find_asset_in_node(item, identifier)), None)
1994
+ return FileOperations.search_json_files(
1995
+ identifier=identifier,
1996
+ file_paths=list(file_paths),
1997
+ match_fn=self._find_asset_in_node,
1998
+ )
2059
1999
 
2060
2000
  def _log_found_asset_details(self, identifier: str, asset_info: Dict, source_file: str) -> None:
2061
2001
  """
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Utils module for Wiz integration - re-exports from main.py for clean imports."""
4
+
5
+ # Import all util functions from the main utils module
6
+ from regscale.integrations.commercial.wizv2.utils.main import (
7
+ create_asset_type,
8
+ get_notes_from_wiz_props,
9
+ handle_management_type,
10
+ map_category,
11
+ is_report_expired,
12
+ convert_first_seen_to_days,
13
+ fetch_report_by_id,
14
+ download_file,
15
+ fetch_sbom_report,
16
+ compliance_job_progress,
17
+ get_report_url_and_status,
18
+ get_or_create_report_id,
19
+ create_compliance_report,
20
+ download_report,
21
+ rerun_expired_report,
22
+ )
23
+
24
+ # Import constants needed by tests and other modules
25
+ from regscale.integrations.commercial.wizv2.core.constants import (
26
+ CHECK_INTERVAL_FOR_DOWNLOAD_REPORT,
27
+ MAX_RETRIES,
28
+ )
29
+
30
+ __all__ = [
31
+ "create_asset_type",
32
+ "get_notes_from_wiz_props",
33
+ "handle_management_type",
34
+ "map_category",
35
+ "is_report_expired",
36
+ "convert_first_seen_to_days",
37
+ "fetch_report_by_id",
38
+ "download_file",
39
+ "fetch_sbom_report",
40
+ "compliance_job_progress",
41
+ "get_report_url_and_status",
42
+ "get_or_create_report_id",
43
+ "create_compliance_report",
44
+ "download_report",
45
+ "rerun_expired_report",
46
+ "CHECK_INTERVAL_FOR_DOWNLOAD_REPORT",
47
+ "MAX_RETRIES",
48
+ ]
@@ -28,7 +28,7 @@ from regscale.core.app.utils.app_utils import (
28
28
  )
29
29
  from regscale.core.utils.date import datetime_obj
30
30
  from regscale.integrations.commercial.cpe import extract_product_name_and_version
31
- from regscale.integrations.commercial.wizv2.constants import (
31
+ from regscale.integrations.commercial.wizv2.core.constants import (
32
32
  BEARER,
33
33
  CHECK_INTERVAL_FOR_DOWNLOAD_REPORT,
34
34
  CONTENT_TYPE,
@@ -42,7 +42,7 @@ from regscale.integrations.commercial.wizv2.constants import (
42
42
  )
43
43
  from regscale.models.integration_models.wizv2 import ComplianceReport, ComplianceCheckStatus
44
44
  from regscale.integrations.commercial.wizv2.variables import WizVariables
45
- from regscale.integrations.commercial.wizv2.wiz_auth import wiz_authenticate
45
+ from regscale.integrations.commercial.wizv2.core.auth import wiz_authenticate
46
46
  from regscale.models import (
47
47
  File,
48
48
  Sbom,
@@ -167,34 +167,57 @@ def map_category(node: dict[str, Any]) -> regscale_models.AssetCategory:
167
167
  :return: RegScale AssetCategory
168
168
  :rtype: regscale_models.AssetCategory
169
169
  """
170
-
171
170
  # First check if there is a CPE which can tell us the category directly.
171
+ if category := _get_category_from_cpe(node):
172
+ return category
173
+
174
+ # Then try mapping by the configured Wiz hardware asset and technology deployment model types.
175
+ asset_type = node.get("type", "")
176
+ if category := _get_category_from_hardware_types(node, asset_type):
177
+ return category
178
+
179
+ # Finally try matching the asset type directly by name.
180
+ if category := _get_category_from_asset_type(asset_type, node):
181
+ return category
182
+
183
+ # If all else fails, default to software.
184
+ return regscale_models.AssetCategory.Software
185
+
186
+
187
+ def _get_category_from_cpe(node: dict[str, Any]) -> Optional[regscale_models.AssetCategory]:
188
+ """Get asset category from CPE information."""
172
189
  cpe = node.get("graphEntity", {}).get("properties", {}).get("cpe", "")
173
190
  cpe_part = extract_product_name_and_version(cpe).get("part", "")
174
191
  if cpe_part and cpe_part.lower() in CPE_PART_TO_CATEGORY_MAPPING:
175
192
  return CPE_PART_TO_CATEGORY_MAPPING[cpe_part]
193
+ return None
176
194
 
177
- # Then try mapping by the configured Wiz hardware asset and technology deployment model types.
178
- asset_type = node.get("type", "")
179
- if WizVariables.useWizHardwareAssetTypes:
180
- if asset_type in WizVariables.wizHardwareAssetTypes:
181
- return regscale_models.AssetCategory.Hardware
182
- if (graph_entity := node.get("graphEntity", {})) and (techs := graph_entity.get("technologies", [])):
183
- for tech in techs:
184
- # We double check just in case we get an explicit None for the technologies.
185
- if tech and tech.get("deploymentModel", None) in WizVariables.wizHardwareAssetTypes:
186
- return regscale_models.AssetCategory.Hardware
187
- else:
188
- logger.debug("No graphEntity set for node %r, default to Software.", node)
189
195
 
190
- # Finally try matching the asset type directly by name.
196
+ def _get_category_from_hardware_types(node: dict[str, Any], asset_type: str) -> Optional[regscale_models.AssetCategory]:
197
+ """Get asset category from configured hardware types."""
198
+ if not WizVariables.useWizHardwareAssetTypes:
199
+ return None
200
+
201
+ if asset_type in WizVariables.wizHardwareAssetTypes:
202
+ return regscale_models.AssetCategory.Hardware
203
+
204
+ if (graph_entity := node.get("graphEntity", {})) and (techs := graph_entity.get("technologies", [])):
205
+ for tech in techs:
206
+ if tech and tech.get("deploymentModel", None) in WizVariables.wizHardwareAssetTypes:
207
+ return regscale_models.AssetCategory.Hardware
208
+ else:
209
+ logger.debug("No graphEntity set for node %r, default to Software.", node)
210
+
211
+ return None
212
+
213
+
214
+ def _get_category_from_asset_type(asset_type: str, node: dict[str, Any]) -> Optional[regscale_models.AssetCategory]:
215
+ """Get asset category from asset type name."""
191
216
  if hasattr(regscale_models.AssetCategory, asset_type):
192
217
  if asset_category := getattr(regscale_models.AssetCategory, asset_type):
193
218
  return asset_category
194
219
  logger.debug("Unknown AssetType %r for node %r. Defaulting to Software.", asset_type, node)
195
-
196
- # If all else fails, default to software.
197
- return regscale_models.AssetCategory.Software
220
+ return None
198
221
 
199
222
 
200
223
  def convert_first_seen_to_days(first_seen: str) -> int:
@@ -753,27 +776,41 @@ def get_report_url_and_status(report_id: str) -> str:
753
776
  raise requests.RequestException("Failed to download report")
754
777
 
755
778
  response_json = response.json()
756
- if errors := response_json.get("errors"):
757
- message = errors[0]["message"]
758
- if RATE_LIMIT_MSG in message:
759
- rate = errors[0]["extensions"]["retryAfter"]
760
- logger.warning("Sleeping %i seconds due to rate limit", rate)
761
- time.sleep(rate)
762
- continue
763
-
764
- logger.error(errors)
765
- else:
766
- status = response_json.get("data", {}).get("report", {}).get("lastRun", {}).get("status")
767
- if status == "COMPLETED":
768
- return response_json["data"]["report"]["lastRun"]["url"]
769
- elif status == "EXPIRED":
770
- logger.warning("Report %s is expired, rerunning report...", report_id)
771
- rerun_expired_report({"reportId": report_id})
772
- return get_report_url_and_status(report_id)
779
+ if url := _handle_report_response(response_json, report_id):
780
+ return url
773
781
 
774
782
  raise requests.RequestException("Download failed, exceeding the maximum number of retries")
775
783
 
776
784
 
785
+ def _handle_report_response(response_json: dict, report_id: str) -> Optional[str]:
786
+ """Handle report response and return URL if ready."""
787
+ if errors := response_json.get("errors"):
788
+ if _handle_rate_limit_error(errors):
789
+ return None
790
+ logger.error(errors)
791
+ return None
792
+
793
+ status = response_json.get("data", {}).get("report", {}).get("lastRun", {}).get("status")
794
+ if status == "COMPLETED":
795
+ return response_json["data"]["report"]["lastRun"]["url"]
796
+ if status == "EXPIRED":
797
+ logger.warning("Report %s is expired, rerunning report...", report_id)
798
+ rerun_expired_report({"reportId": report_id})
799
+ return get_report_url_and_status(report_id)
800
+ return None
801
+
802
+
803
+ def _handle_rate_limit_error(errors: list) -> bool:
804
+ """Handle rate limit error and return True if rate limited."""
805
+ message = errors[0]["message"]
806
+ if RATE_LIMIT_MSG in message:
807
+ rate = errors[0]["extensions"]["retryAfter"]
808
+ logger.warning("Sleeping %i seconds due to rate limit", rate)
809
+ time.sleep(rate)
810
+ return True
811
+ return False
812
+
813
+
777
814
  def download_report(variables: Dict) -> requests.Response:
778
815
  """
779
816
  Return a download URL for a provided Wiz report id
@@ -1264,35 +1301,53 @@ def report_result_to_implementation_status(result: str) -> str:
1264
1301
  compliance_settings = get_wiz_compliance_settings()
1265
1302
 
1266
1303
  if compliance_settings:
1267
- try:
1268
- # Get implementation status labels from compliance settings
1269
- status_labels = compliance_settings.get_field_labels("implementationStatus")
1270
-
1271
- # Map compliance check result to implementation status
1272
- result_lower = result.lower()
1273
- for label in status_labels:
1274
- label_lower = label.lower()
1275
- if result_lower == ComplianceCheckStatus.PASS.value.lower():
1276
- if label_lower in ["implemented", "complete", "compliant"]:
1277
- return label
1278
- elif result_lower == ComplianceCheckStatus.FAIL.value.lower():
1279
- if label_lower in ["inremediation", "in remediation", "remediation", "failed", "non-compliant"]:
1280
- return label
1281
- else: # Not implemented or other status
1282
- if label_lower in ["notimplemented", "not implemented", "pending", "planned"]:
1283
- return label
1284
-
1285
- logger.debug(f"No matching compliance setting found for result: {result}")
1286
- except Exception as e:
1287
- logger.debug(f"Error using compliance settings for implementation status mapping: {e}")
1304
+ if status := _try_get_status_from_settings(compliance_settings, result):
1305
+ return status
1288
1306
 
1289
1307
  # Fallback to default mapping
1308
+ return _get_default_status_mapping(result)
1309
+
1310
+
1311
+ def _try_get_status_from_settings(compliance_settings, result: str) -> Optional[str]:
1312
+ """Try to get status from compliance settings."""
1313
+ try:
1314
+ status_labels = compliance_settings.get_field_labels("implementationStatus")
1315
+ result_lower = result.lower()
1316
+
1317
+ for label in status_labels:
1318
+ if status := _match_label_to_result(label, result_lower):
1319
+ return status
1320
+
1321
+ logger.debug(f"No matching compliance setting found for result: {result}")
1322
+ except Exception as e:
1323
+ logger.debug(f"Error using compliance settings for implementation status mapping: {e}")
1324
+ return None
1325
+
1326
+
1327
+ def _match_label_to_result(label: str, result_lower: str) -> Optional[str]:
1328
+ """Match a label to a result status."""
1329
+ label_lower = label.lower()
1330
+
1331
+ if result_lower == ComplianceCheckStatus.PASS.value.lower():
1332
+ if label_lower in ["implemented", "complete", "compliant"]:
1333
+ return label
1334
+ elif result_lower == ComplianceCheckStatus.FAIL.value.lower():
1335
+ if label_lower in ["inremediation", "in remediation", "remediation", "failed", "non-compliant"]:
1336
+ return label
1337
+ else: # Not implemented or other status
1338
+ if label_lower in ["notimplemented", "not implemented", "pending", "planned"]:
1339
+ return label
1340
+
1341
+ return None
1342
+
1343
+
1344
+ def _get_default_status_mapping(result: str) -> str:
1345
+ """Get default status mapping for result."""
1290
1346
  if result == ComplianceCheckStatus.PASS.value:
1291
1347
  return ControlImplementationStatus.Implemented.value
1292
- elif result == ComplianceCheckStatus.FAIL.value:
1348
+ if result == ComplianceCheckStatus.FAIL.value:
1293
1349
  return ControlImplementationStatus.InRemediation.value
1294
- else:
1295
- return ControlImplementationStatus.NotImplemented.value
1350
+ return ControlImplementationStatus.NotImplemented.value
1296
1351
 
1297
1352
 
1298
1353
  def create_vulnerabilities_from_wiz_findings(
@@ -1385,7 +1440,7 @@ def create_single_vulnerability_from_wiz_data(
1385
1440
  """
1386
1441
  # Import here to avoid circular imports
1387
1442
  from regscale.integrations.commercial.wizv2.scanner import WizVulnerabilityIntegration
1388
- from regscale.integrations.commercial.wizv2.constants import WizVulnerabilityType
1443
+ from regscale.integrations.commercial.wizv2.core.constants import WizVulnerabilityType
1389
1444
 
1390
1445
  try:
1391
1446
  # Create integration instance
@@ -3,7 +3,93 @@
3
3
  """Wiz Variables"""
4
4
 
5
5
  from regscale.core.app.utils.variables import RsVariableType, RsVariablesMeta
6
- from regscale.integrations.commercial.wizv2.constants import RECOMMENDED_WIZ_INVENTORY_TYPES, DEFAULT_WIZ_HARDWARE_TYPES
6
+
7
+ # Define constants locally to avoid circular import with core.constants
8
+ _RECOMMENDED_WIZ_INVENTORY_TYPES = [
9
+ # Compute resources
10
+ "CONTAINER",
11
+ "CONTAINER_GROUP",
12
+ "CONTAINER_IMAGE",
13
+ "POD",
14
+ "SERVERLESS",
15
+ "SERVERLESS_PACKAGE",
16
+ "VIRTUAL_DESKTOP",
17
+ "VIRTUAL_MACHINE",
18
+ "VIRTUAL_MACHINE_IMAGE",
19
+ # Network and exposure
20
+ "API_GATEWAY",
21
+ "CDN",
22
+ "CERTIFICATE",
23
+ "DNS_RECORD",
24
+ "ENDPOINT",
25
+ "FIREWALL",
26
+ "GATEWAY",
27
+ "LOAD_BALANCER",
28
+ "MANAGED_CERTIFICATE",
29
+ "NETWORK_ADDRESS",
30
+ "NETWORK_INTERFACE",
31
+ "PRIVATE_ENDPOINT",
32
+ "PRIVATE_LINK",
33
+ "PROXY",
34
+ "WEB_SERVICE",
35
+ # Storage and data
36
+ "BACKUP_SERVICE",
37
+ "BUCKET",
38
+ "DATABASE",
39
+ "DATA_WORKLOAD",
40
+ "DB_SERVER",
41
+ "FILE_SYSTEM_SERVICE",
42
+ "SECRET",
43
+ "SECRET_CONTAINER",
44
+ "STORAGE_ACCOUNT",
45
+ "VOLUME",
46
+ # Identity and access management
47
+ "ACCESS_ROLE",
48
+ "AUTHENTICATION_CONFIGURATION",
49
+ "IAM_BINDING",
50
+ "RAW_ACCESS_POLICY",
51
+ "SERVICE_ACCOUNT",
52
+ # Development and CI/CD
53
+ "APPLICATION",
54
+ "CICD_SERVICE",
55
+ "CONFIG_MAP",
56
+ "CONTAINER_REGISTRY",
57
+ "CONTAINER_SERVICE",
58
+ # Kubernetes resources
59
+ "CONTROLLER_REVISION",
60
+ "KUBERNETES_CLUSTER",
61
+ "KUBERNETES_INGRESS",
62
+ "KUBERNETES_NODE",
63
+ "KUBERNETES_SERVICE",
64
+ "NAMESPACE",
65
+ # Infrastructure and management
66
+ "CLOUD_LOG_CONFIGURATION",
67
+ "CLOUD_ORGANIZATION",
68
+ "DOMAIN",
69
+ "EMAIL_SERVICE",
70
+ "ENCRYPTION_KEY",
71
+ "MANAGEMENT_SERVICE",
72
+ "MESSAGING_SERVICE",
73
+ "REGISTERED_DOMAIN",
74
+ "RESOURCE_GROUP",
75
+ "SERVICE_CONFIGURATION",
76
+ "SUBNET",
77
+ "SUBSCRIPTION",
78
+ "VIRTUAL_NETWORK",
79
+ ]
80
+
81
+ _DEFAULT_WIZ_HARDWARE_TYPES = [
82
+ # CloudResource types
83
+ "VIRTUAL_MACHINE",
84
+ "VIRTUAL_MACHINE_IMAGE",
85
+ "CONTAINER",
86
+ "CONTAINER_IMAGE",
87
+ "DB_SERVER",
88
+ # technology deploymentModels
89
+ "SERVER_APPLICATION",
90
+ "CLIENT_APPLICATION",
91
+ "VIRTUAL_APPLIANCE",
92
+ ]
7
93
 
8
94
 
9
95
  class WizVariables(metaclass=RsVariablesMeta):
@@ -23,7 +109,7 @@ class WizVariables(metaclass=RsVariablesMeta):
23
109
  wizInventoryFilterBy: RsVariableType(
24
110
  str,
25
111
  '{"projectId": ["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"], "type": ["API_GATEWAY"]}',
26
- default="""{"type": ["%s"] }""" % '","'.join(RECOMMENDED_WIZ_INVENTORY_TYPES), # type: ignore
112
+ default="""{"type": ["%s"] }""" % '","'.join(_RECOMMENDED_WIZ_INVENTORY_TYPES), # type: ignore
27
113
  ) # type: ignore
28
114
  wizAccessToken: RsVariableType(str, "", sensitive=True, required=False) # type: ignore
29
115
  wizClientId: RsVariableType(str, "", sensitive=True) # type: ignore
@@ -34,7 +120,7 @@ class WizVariables(metaclass=RsVariablesMeta):
34
120
  list,
35
121
  '["CONTAINER", "CONTAINER_IMAGE", "VIRTUAL_MACHINE", "VIRTUAL_MACHINE_IMAGE", "DB_SERVER", '
36
122
  '"CLIENT_APPLICATION", "SERVER_APPLICATION", "VIRTUAL_APPLIANCE"]',
37
- default=DEFAULT_WIZ_HARDWARE_TYPES,
123
+ default=_DEFAULT_WIZ_HARDWARE_TYPES,
38
124
  required=False,
39
125
  ) # type: ignore
40
126
  wizReportAge: RsVariableType(int, "14", default=14, required=False) # type: ignore