regscale-cli 6.26.0.0__py3-none-any.whl → 6.27.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of regscale-cli might be problematic. Click here for more details.
- regscale/_version.py +1 -1
- regscale/core/app/application.py +1 -1
- regscale/core/app/internal/evidence.py +419 -2
- regscale/dev/code_gen.py +24 -20
- regscale/integrations/commercial/__init__.py +0 -1
- regscale/integrations/commercial/jira.py +367 -126
- regscale/integrations/commercial/qualys/__init__.py +7 -8
- regscale/integrations/commercial/qualys/scanner.py +8 -3
- regscale/integrations/commercial/synqly/assets.py +17 -0
- regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
- regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
- regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
- regscale/integrations/commercial/tenablev2/commands.py +142 -1
- regscale/integrations/commercial/tenablev2/scanner.py +0 -1
- regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
- regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
- regscale/integrations/commercial/wizv2/click.py +44 -59
- regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
- regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
- regscale/integrations/commercial/wizv2/compliance_report.py +10 -9
- regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
- regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
- regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
- regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
- regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
- regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
- regscale/integrations/commercial/wizv2/issue.py +1 -1
- regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
- regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
- regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
- regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
- regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
- regscale/integrations/commercial/wizv2/reports.py +1 -1
- regscale/integrations/commercial/wizv2/sbom.py +1 -1
- regscale/integrations/commercial/wizv2/scanner.py +40 -100
- regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
- regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
- regscale/integrations/commercial/wizv2/variables.py +89 -3
- regscale/integrations/compliance_integration.py +0 -46
- regscale/integrations/control_matcher.py +22 -3
- regscale/integrations/due_date_handler.py +14 -8
- regscale/integrations/public/fedramp/docx_parser.py +10 -1
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
- regscale/integrations/public/fedramp/fedramp_five.py +1 -1
- regscale/integrations/scanner_integration.py +127 -57
- regscale/models/integration_models/cisa_kev_data.json +132 -9
- regscale/models/integration_models/qualys.py +3 -4
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
- regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
- regscale/models/regscale_models/control_implementation.py +1 -1
- regscale/models/regscale_models/issue.py +0 -1
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/METADATA +1 -17
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/RECORD +94 -61
- tests/regscale/integrations/commercial/test_jira.py +481 -91
- tests/regscale/integrations/commercial/test_wiz.py +96 -200
- tests/regscale/integrations/commercial/wizv2/__init__.py +1 -1
- tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
- tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
- tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
- tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
- tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
- tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
- tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
- tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
- tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
- tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
- tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
- tests/regscale/integrations/commercial/wizv2/test_issue.py +1 -1
- tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
- tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
- tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
- tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +1 -1
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +72 -29
- tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +946 -78
- tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +97 -202
- tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
- tests/regscale/integrations/public/test_fedramp.py +301 -0
- tests/regscale/integrations/test_control_matcher.py +83 -0
- regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
- tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +0 -750
- /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/LICENSE +0 -0
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/WHEEL +0 -0
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.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.
|
|
16
|
-
from regscale.integrations.commercial.wizv2.
|
|
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.
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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)
|
|
448
|
+
def progress_callback(query_type: str, status: str):
|
|
449
|
+
if status == "loaded":
|
|
450
|
+
self.finding_progress.advance(cache_task, 1)
|
|
457
451
|
|
|
458
|
-
|
|
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))
|
|
463
|
-
|
|
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
|
-
|
|
488
|
-
|
|
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}'
|
|
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')}:
|
|
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
|
-
|
|
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
|
-
|
|
1933
|
-
|
|
1934
|
-
if
|
|
1935
|
-
|
|
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)
|
|
1942
|
-
|
|
1943
|
-
# Ensure we have a valid token (should already be set by caller)
|
|
1944
|
-
if not self.wiz_token:
|
|
1945
|
-
error_and_exit("Wiz token is not set. Please authenticate before calling fetch_wiz_data_if_needed.")
|
|
1946
|
-
|
|
1947
|
-
nodes = fetch_wiz_data(
|
|
1948
|
-
query=query,
|
|
1949
|
-
variables=variables,
|
|
1950
|
-
api_endpoint_url=WizVariables.wizUrl,
|
|
1951
|
-
token=self.wiz_token,
|
|
1952
|
-
topic_key=topic_key,
|
|
1953
|
-
)
|
|
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.")
|
|
1954
1912
|
|
|
1955
|
-
|
|
1956
|
-
|
|
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
|
+
)
|
|
1957
1920
|
|
|
1958
|
-
return
|
|
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,
|
|
1926
|
+
)
|
|
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
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
757
|
-
|
|
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
|
-
|
|
1268
|
-
|
|
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
|
-
|
|
1348
|
+
if result == ComplianceCheckStatus.FAIL.value:
|
|
1293
1349
|
return ControlImplementationStatus.InRemediation.value
|
|
1294
|
-
|
|
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
|
-
|
|
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(
|
|
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=
|
|
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
|
|
@@ -1234,7 +1234,6 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1234
1234
|
processed_impl_today.add(impl.id)
|
|
1235
1235
|
|
|
1236
1236
|
self._record_control_mapping(control_id, impl.id)
|
|
1237
|
-
self._map_assets_to_control_component(sec_control, items)
|
|
1238
1237
|
return 1
|
|
1239
1238
|
except Exception as e: # noqa: BLE001
|
|
1240
1239
|
logger.error(f"Error processing control assessment for '{control_id}': {e}")
|
|
@@ -1370,51 +1369,6 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1370
1369
|
except Exception:
|
|
1371
1370
|
pass
|
|
1372
1371
|
|
|
1373
|
-
def _map_assets_to_control_component(self, sec_control: SecurityControl, items: List[ComplianceItem]) -> None:
|
|
1374
|
-
"""
|
|
1375
|
-
Map assets to control-specific components for organization.
|
|
1376
|
-
|
|
1377
|
-
:param SecurityControl sec_control: Security control to create component for
|
|
1378
|
-
:param List[ComplianceItem] items: Compliance items with asset references
|
|
1379
|
-
:return: None
|
|
1380
|
-
:rtype: None
|
|
1381
|
-
"""
|
|
1382
|
-
try:
|
|
1383
|
-
component_title = f"Control {sec_control.controlId}"
|
|
1384
|
-
component = self.components_by_title.get(component_title) if hasattr(self, "components_by_title") else None
|
|
1385
|
-
if not component:
|
|
1386
|
-
# Get complianceSettingsId from the security plan
|
|
1387
|
-
security_plan = self._get_security_plan()
|
|
1388
|
-
compliance_settings_id = getattr(security_plan, "complianceSettingsId", None) if security_plan else None
|
|
1389
|
-
if not compliance_settings_id:
|
|
1390
|
-
compliance_setting = self._get_compliance_settings()
|
|
1391
|
-
compliance_settings_id = getattr(compliance_setting, "id", None) if compliance_setting else None
|
|
1392
|
-
component = regscale_models.Component(
|
|
1393
|
-
title=component_title,
|
|
1394
|
-
componentType=regscale_models.ComponentType.Hardware,
|
|
1395
|
-
securityPlansId=self.plan_id,
|
|
1396
|
-
description=component_title,
|
|
1397
|
-
componentOwnerId=self.get_assessor_id(),
|
|
1398
|
-
complianceSettingsId=compliance_settings_id,
|
|
1399
|
-
).get_or_create()
|
|
1400
|
-
regscale_models.ComponentMapping(
|
|
1401
|
-
componentId=component.id,
|
|
1402
|
-
securityPlanId=self.plan_id,
|
|
1403
|
-
).get_or_create()
|
|
1404
|
-
if hasattr(self, "components_by_title"):
|
|
1405
|
-
self.components_by_title[component_title] = component
|
|
1406
|
-
|
|
1407
|
-
for item in items:
|
|
1408
|
-
asset = self.get_asset_by_identifier(getattr(item, "resource_id", ""))
|
|
1409
|
-
if not asset:
|
|
1410
|
-
continue
|
|
1411
|
-
regscale_models.AssetMapping(
|
|
1412
|
-
assetId=asset.id,
|
|
1413
|
-
componentId=component.id,
|
|
1414
|
-
).get_or_create_with_status()
|
|
1415
|
-
except Exception as map_exc: # noqa: BLE001
|
|
1416
|
-
logger.debug(f"Control-to-asset mapping skipped due to: {map_exc}")
|
|
1417
|
-
|
|
1418
1372
|
@staticmethod
|
|
1419
1373
|
def _parse_control_id(control_id: str) -> tuple[str, Optional[str]]:
|
|
1420
1374
|
"""
|