regscale-cli 6.23.0.1__py3-none-any.whl → 6.24.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 +2 -0
- regscale/integrations/commercial/__init__.py +1 -0
- regscale/integrations/commercial/jira.py +95 -22
- regscale/integrations/commercial/sarif/sarif_converter.py +1 -1
- regscale/integrations/commercial/wizv2/click.py +132 -2
- regscale/integrations/commercial/wizv2/compliance_report.py +1574 -0
- regscale/integrations/commercial/wizv2/constants.py +72 -2
- regscale/integrations/commercial/wizv2/data_fetcher.py +61 -0
- regscale/integrations/commercial/wizv2/file_cleanup.py +104 -0
- regscale/integrations/commercial/wizv2/issue.py +775 -27
- regscale/integrations/commercial/wizv2/policy_compliance.py +599 -181
- regscale/integrations/commercial/wizv2/reports.py +243 -0
- regscale/integrations/commercial/wizv2/scanner.py +668 -245
- regscale/integrations/compliance_integration.py +534 -56
- regscale/integrations/due_date_handler.py +210 -0
- regscale/integrations/public/cci_importer.py +444 -0
- regscale/integrations/scanner_integration.py +718 -153
- regscale/models/integration_models/CCI_List.xml +1 -0
- regscale/models/integration_models/cisa_kev_data.json +18 -3
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/regscale_models/control_implementation.py +13 -3
- regscale/models/regscale_models/form_field_value.py +1 -1
- regscale/models/regscale_models/milestone.py +1 -0
- regscale/models/regscale_models/regscale_model.py +225 -60
- regscale/models/regscale_models/security_plan.py +3 -2
- regscale/regscale.py +7 -0
- {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.1.dist-info}/METADATA +17 -17
- {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.1.dist-info}/RECORD +45 -28
- tests/fixtures/test_fixture.py +13 -8
- tests/regscale/integrations/public/__init__.py +0 -0
- tests/regscale/integrations/public/test_alienvault.py +220 -0
- tests/regscale/integrations/public/test_cci.py +458 -0
- tests/regscale/integrations/public/test_cisa.py +1021 -0
- tests/regscale/integrations/public/test_emass.py +518 -0
- tests/regscale/integrations/public/test_fedramp.py +851 -0
- tests/regscale/integrations/public/test_fedramp_cis_crm.py +3661 -0
- tests/regscale/integrations/public/test_file_uploads.py +506 -0
- tests/regscale/integrations/public/test_oscal.py +453 -0
- tests/regscale/models/test_form_field_value_integration.py +304 -0
- tests/regscale/models/test_module_integration.py +582 -0
- {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.1.dist-info}/LICENSE +0 -0
- {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.1.dist-info}/WHEEL +0 -0
- {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.1.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.1.dist-info}/top_level.txt +0 -0
|
@@ -6,6 +6,7 @@ import logging
|
|
|
6
6
|
import os
|
|
7
7
|
import re
|
|
8
8
|
from collections.abc import Iterator
|
|
9
|
+
from functools import lru_cache
|
|
9
10
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
10
11
|
|
|
11
12
|
from regscale.core.app.utils.app_utils import check_file_path, error_and_exit, get_current_datetime
|
|
@@ -42,15 +43,16 @@ from regscale.integrations.commercial.wizv2.utils import (
|
|
|
42
43
|
map_category,
|
|
43
44
|
)
|
|
44
45
|
from regscale.integrations.commercial.wizv2.variables import WizVariables
|
|
46
|
+
from regscale.integrations.variables import ScannerVariables
|
|
45
47
|
from regscale.integrations.commercial.wizv2.wiz_auth import wiz_authenticate
|
|
46
48
|
from regscale.integrations.scanner_integration import (
|
|
47
49
|
IntegrationAsset,
|
|
48
50
|
IntegrationFinding,
|
|
49
51
|
ScannerIntegration,
|
|
50
52
|
)
|
|
51
|
-
from regscale.integrations.variables import ScannerVariables
|
|
52
53
|
from regscale.models import IssueStatus, regscale_models
|
|
53
54
|
from regscale.models.regscale_models.compliance_settings import ComplianceSettings
|
|
55
|
+
from regscale.models.regscale_models.regscale_model import RegScaleModel
|
|
54
56
|
|
|
55
57
|
logger = logging.getLogger("regscale")
|
|
56
58
|
|
|
@@ -66,11 +68,19 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
66
68
|
"High": regscale_models.IssueSeverity.High,
|
|
67
69
|
"Medium": regscale_models.IssueSeverity.Moderate,
|
|
68
70
|
"Low": regscale_models.IssueSeverity.Low,
|
|
71
|
+
"INFORMATIONAL": regscale_models.IssueSeverity.NotAssigned,
|
|
69
72
|
}
|
|
70
73
|
asset_lookup = "vulnerableAsset"
|
|
71
74
|
wiz_token = None
|
|
72
75
|
_compliance_settings = None
|
|
73
76
|
|
|
77
|
+
def __init__(self, *args, **kwargs):
|
|
78
|
+
super().__init__(*args, **kwargs)
|
|
79
|
+
# Suppress generic asset not found errors but use enhanced diagnostics instead
|
|
80
|
+
self.suppress_asset_not_found_errors = True
|
|
81
|
+
# Track unique missing asset types for summary reporting
|
|
82
|
+
self._missing_asset_types = set()
|
|
83
|
+
|
|
74
84
|
@staticmethod
|
|
75
85
|
def get_variables() -> Dict[str, Any]:
|
|
76
86
|
"""
|
|
@@ -84,6 +94,31 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
84
94
|
"filterBy": {},
|
|
85
95
|
}
|
|
86
96
|
|
|
97
|
+
def get_finding_identifier(self, finding) -> str:
|
|
98
|
+
"""
|
|
99
|
+
Gets the finding identifier for Wiz findings.
|
|
100
|
+
For Wiz integrations, prioritize external_id since plugin_id can be non-unique.
|
|
101
|
+
|
|
102
|
+
:param finding: The finding
|
|
103
|
+
:return: The finding identifier
|
|
104
|
+
:rtype: str
|
|
105
|
+
"""
|
|
106
|
+
# We could have a string truncation error platform side on IntegrationFindingId nvarchar(450)
|
|
107
|
+
prefix = f"{self.plan_id}:"
|
|
108
|
+
|
|
109
|
+
# For Wiz, prioritize external_id since plugin_id can be non-unique
|
|
110
|
+
if finding.external_id:
|
|
111
|
+
prefix += self.hash_string(finding.external_id).__str__()
|
|
112
|
+
else:
|
|
113
|
+
prefix += (
|
|
114
|
+
finding.cve or finding.plugin_id or finding.rule_id or self.hash_string(finding.external_id).__str__()
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if ScannerVariables.issueCreation.lower() == "perasset":
|
|
118
|
+
res = f"{prefix}:{finding.asset_identifier}"
|
|
119
|
+
return res[:450]
|
|
120
|
+
return prefix[:450]
|
|
121
|
+
|
|
87
122
|
def authenticate(self, client_id: Optional[str] = None, client_secret: Optional[str] = None) -> None:
|
|
88
123
|
"""
|
|
89
124
|
Authenticates to Wiz using the client ID and client secret
|
|
@@ -97,14 +132,16 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
97
132
|
logger.info("Authenticating to Wiz...")
|
|
98
133
|
self.wiz_token = wiz_authenticate(client_id, client_secret)
|
|
99
134
|
|
|
100
|
-
def get_query_types(self, project_id: str) -> List[Dict[str, Any]]:
|
|
135
|
+
def get_query_types(self, project_id: str, filter_by: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
|
|
101
136
|
"""Get the query types for vulnerability scanning.
|
|
102
137
|
|
|
103
138
|
:param str project_id: The project ID to get queries for
|
|
139
|
+
:param Optional[Dict[str, Any]] filter_by: Optional filter criteria (used by subclasses)
|
|
104
140
|
:return: List of query types
|
|
105
141
|
:rtype: List[Dict[str, Any]]
|
|
106
142
|
"""
|
|
107
|
-
|
|
143
|
+
# Base class ignores filter_by, subclasses can override to use it
|
|
144
|
+
return get_wiz_vulnerability_queries(project_id=project_id, filter_by=filter_by)
|
|
108
145
|
|
|
109
146
|
def fetch_findings(self, *args, **kwargs) -> Iterator[IntegrationFinding]:
|
|
110
147
|
"""
|
|
@@ -313,7 +350,9 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
313
350
|
# Step 2: Initialize progress tracking
|
|
314
351
|
logger.info("Fetching Wiz findings using async concurrent queries...")
|
|
315
352
|
self.num_findings_to_process = 0
|
|
316
|
-
|
|
353
|
+
# Pass filter_by_override if provided
|
|
354
|
+
filter_by = kwargs.get("filter_by_override")
|
|
355
|
+
query_configs = self.get_query_types(project_id=project_id, filter_by=filter_by)
|
|
317
356
|
|
|
318
357
|
main_task = self.finding_progress.add_task(
|
|
319
358
|
"[cyan]Running concurrent GraphQL queries...", total=len(query_configs)
|
|
@@ -337,8 +376,13 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
337
376
|
# Step 6: Process results
|
|
338
377
|
yield from self._process_query_results(results, query_configs, project_id, should_fetch_fresh)
|
|
339
378
|
|
|
340
|
-
# Step 7: Complete main task
|
|
341
|
-
self.finding_progress.
|
|
379
|
+
# Step 7: Complete main task - ensure it's marked as 100% complete
|
|
380
|
+
self.finding_progress.update(
|
|
381
|
+
main_task,
|
|
382
|
+
description="[green]✓ Completed processing all Wiz findings",
|
|
383
|
+
completed=len(query_configs),
|
|
384
|
+
total=len(query_configs),
|
|
385
|
+
)
|
|
342
386
|
|
|
343
387
|
except Exception as e:
|
|
344
388
|
logger.error(f"Error in async findings fetch: {e!s}", exc_info=True)
|
|
@@ -350,6 +394,14 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
350
394
|
logger.info("Falling back to synchronous query method...")
|
|
351
395
|
yield from self.fetch_findings_sync(**kwargs)
|
|
352
396
|
|
|
397
|
+
# Log summary of missing asset types if any were found
|
|
398
|
+
if hasattr(self, "_missing_asset_types") and self._missing_asset_types:
|
|
399
|
+
logger.warning(
|
|
400
|
+
"Summary: Found references to missing asset types: %s. "
|
|
401
|
+
"Consider adding these to RECOMMENDED_WIZ_INVENTORY_TYPES in constants.py",
|
|
402
|
+
", ".join(sorted(self._missing_asset_types)),
|
|
403
|
+
)
|
|
404
|
+
|
|
353
405
|
logger.info(
|
|
354
406
|
"Finished async fetching Wiz findings. Total findings to process: %d", self.num_findings_to_process or 0
|
|
355
407
|
)
|
|
@@ -459,7 +511,9 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
459
511
|
|
|
460
512
|
logger.info("Fetching Wiz findings using synchronous queries...")
|
|
461
513
|
self.num_findings_to_process = 0
|
|
462
|
-
|
|
514
|
+
# Pass filter_by_override if provided
|
|
515
|
+
filter_by = kwargs.get("filter_by_override")
|
|
516
|
+
query_types = self.get_query_types(project_id=project_id, filter_by=filter_by)
|
|
463
517
|
|
|
464
518
|
# Create detailed progress tracking for each query type
|
|
465
519
|
main_task = self.finding_progress.add_task(
|
|
@@ -530,6 +584,14 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
530
584
|
description=f"[green]✓ Completed fetching all Wiz findings ({self.num_findings_to_process or 0} total)",
|
|
531
585
|
)
|
|
532
586
|
|
|
587
|
+
# Log summary of missing asset types if any were found
|
|
588
|
+
if hasattr(self, "_missing_asset_types") and self._missing_asset_types:
|
|
589
|
+
logger.warning(
|
|
590
|
+
"Summary: Found references to missing asset types: %s. "
|
|
591
|
+
"Consider adding these to RECOMMENDED_WIZ_INVENTORY_TYPES in constants.py",
|
|
592
|
+
", ".join(sorted(self._missing_asset_types)),
|
|
593
|
+
)
|
|
594
|
+
|
|
533
595
|
logger.info(
|
|
534
596
|
"Finished synchronous fetching Wiz findings. Total findings to process: %d",
|
|
535
597
|
self.num_findings_to_process or 0,
|
|
@@ -664,16 +726,218 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
664
726
|
) -> Iterator[IntegrationFinding]:
|
|
665
727
|
"""
|
|
666
728
|
Parses a list of Wiz finding nodes into IntegrationFinding objects.
|
|
667
|
-
|
|
668
|
-
This is a compatibility wrapper that calls the progress-aware version.
|
|
729
|
+
Groups findings by rule and scope for consolidation when appropriate.
|
|
669
730
|
|
|
670
731
|
:param List[Dict[str, Any]] nodes: List of Wiz finding nodes
|
|
671
732
|
:param WizVulnerabilityType vulnerability_type: The type of vulnerability
|
|
672
733
|
:yield: IntegrationFinding objects
|
|
673
734
|
:rtype: Iterator[IntegrationFinding]
|
|
674
735
|
"""
|
|
675
|
-
|
|
676
|
-
|
|
736
|
+
logger.info(
|
|
737
|
+
f"🔍 VULNERABILITY PROCESSING ANALYSIS: Received {len(nodes)} raw Wiz vulnerabilities for processing"
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
# Count issues by severity for analysis
|
|
741
|
+
severity_counts = {}
|
|
742
|
+
status_counts = {}
|
|
743
|
+
for node in nodes:
|
|
744
|
+
severity = node.get("severity", "Low")
|
|
745
|
+
status = node.get("status", "OPEN")
|
|
746
|
+
severity_counts[severity] = severity_counts.get(severity, 0) + 1
|
|
747
|
+
status_counts[status] = status_counts.get(status, 0) + 1
|
|
748
|
+
|
|
749
|
+
logger.debug(f"Raw vulnerability breakdown by severity: {severity_counts}")
|
|
750
|
+
logger.debug(f"Raw vulnerability breakdown by status: {status_counts}")
|
|
751
|
+
|
|
752
|
+
# Filter nodes by minimum severity configuration
|
|
753
|
+
filtered_nodes = []
|
|
754
|
+
filtered_out_count = 0
|
|
755
|
+
for node in nodes:
|
|
756
|
+
wiz_severity = node.get("severity", "Low")
|
|
757
|
+
wiz_id = node.get("id", "unknown")
|
|
758
|
+
if self.should_process_finding_by_severity(wiz_severity):
|
|
759
|
+
filtered_nodes.append(node)
|
|
760
|
+
else:
|
|
761
|
+
filtered_out_count += 1
|
|
762
|
+
logger.info(
|
|
763
|
+
f"🚫 FILTERED BY SEVERITY: Vulnerability {wiz_id} with severity '{wiz_severity}' filtered due to minimumSeverity configuration"
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
logger.info(
|
|
767
|
+
f"✅ After severity filtering: {len(filtered_nodes)} vulnerabilities kept, {filtered_out_count} filtered out"
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
if not filtered_nodes:
|
|
771
|
+
logger.warning(
|
|
772
|
+
"⚠️ All vulnerabilities filtered out by severity configuration - check your minimumSeverity setting"
|
|
773
|
+
)
|
|
774
|
+
return
|
|
775
|
+
|
|
776
|
+
# Apply consolidation logic for findings that support it
|
|
777
|
+
if self._should_apply_consolidation(vulnerability_type):
|
|
778
|
+
yield from self._parse_findings_with_consolidation(filtered_nodes, vulnerability_type)
|
|
779
|
+
else:
|
|
780
|
+
# Use original parsing for vulnerability types that shouldn't be consolidated
|
|
781
|
+
yield from self.parse_findings_with_progress(filtered_nodes, vulnerability_type, task_id=None)
|
|
782
|
+
|
|
783
|
+
def _should_apply_consolidation(self, vulnerability_type: WizVulnerabilityType) -> bool:
|
|
784
|
+
"""
|
|
785
|
+
Determine if consolidation should be applied for this vulnerability type.
|
|
786
|
+
|
|
787
|
+
:param WizVulnerabilityType vulnerability_type: The vulnerability type
|
|
788
|
+
:return: True if consolidation should be applied
|
|
789
|
+
:rtype: bool
|
|
790
|
+
"""
|
|
791
|
+
# Apply consolidation to finding types that commonly affect multiple assets
|
|
792
|
+
consolidation_types = {
|
|
793
|
+
WizVulnerabilityType.HOST_FINDING,
|
|
794
|
+
WizVulnerabilityType.DATA_FINDING,
|
|
795
|
+
WizVulnerabilityType.VULNERABILITY,
|
|
796
|
+
}
|
|
797
|
+
return vulnerability_type in consolidation_types
|
|
798
|
+
|
|
799
|
+
def _parse_findings_with_consolidation(
|
|
800
|
+
self, nodes: List[Dict[str, Any]], vulnerability_type: WizVulnerabilityType
|
|
801
|
+
) -> Iterator[IntegrationFinding]:
|
|
802
|
+
"""
|
|
803
|
+
Parse findings with consolidation logic applied.
|
|
804
|
+
|
|
805
|
+
:param List[Dict[str, Any]] nodes: List of Wiz finding nodes
|
|
806
|
+
:param WizVulnerabilityType vulnerability_type: The vulnerability type
|
|
807
|
+
:yield: Consolidated IntegrationFinding objects
|
|
808
|
+
:rtype: Iterator[IntegrationFinding]
|
|
809
|
+
"""
|
|
810
|
+
# Group nodes for potential consolidation
|
|
811
|
+
grouped_nodes = self._group_findings_for_consolidation(nodes)
|
|
812
|
+
|
|
813
|
+
# Process each group
|
|
814
|
+
for group_key, group_nodes in grouped_nodes.items():
|
|
815
|
+
if len(group_nodes) > 1:
|
|
816
|
+
# Multiple nodes with same rule - attempt consolidation
|
|
817
|
+
if consolidated_finding := self._create_consolidated_scanner_finding(group_nodes, vulnerability_type):
|
|
818
|
+
yield consolidated_finding
|
|
819
|
+
else:
|
|
820
|
+
# Single node - process normally
|
|
821
|
+
if finding := self.parse_finding(group_nodes[0], vulnerability_type):
|
|
822
|
+
yield finding
|
|
823
|
+
|
|
824
|
+
def _group_findings_for_consolidation(self, nodes: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
|
|
825
|
+
"""
|
|
826
|
+
Group findings by rule and appropriate scope for consolidation.
|
|
827
|
+
- Database findings: group by server
|
|
828
|
+
- App Configuration findings: group by resource group
|
|
829
|
+
- Other findings: group by full resource path
|
|
830
|
+
|
|
831
|
+
:param List[Dict[str, Any]] nodes: List of Wiz finding nodes
|
|
832
|
+
:return: Dictionary mapping group keys to lists of nodes
|
|
833
|
+
:rtype: Dict[str, List[Dict[str, Any]]]
|
|
834
|
+
"""
|
|
835
|
+
groups = {}
|
|
836
|
+
|
|
837
|
+
for node in nodes:
|
|
838
|
+
# Create a grouping key based on rule and appropriate scope
|
|
839
|
+
rule_name = self._get_rule_name_from_node(node)
|
|
840
|
+
provider_id = self._get_provider_id_from_node(node)
|
|
841
|
+
|
|
842
|
+
# Determine the appropriate grouping scope based on resource type
|
|
843
|
+
grouping_scope = self._determine_grouping_scope(provider_id, rule_name)
|
|
844
|
+
|
|
845
|
+
# Group key combines rule name and scope
|
|
846
|
+
group_key = f"{rule_name}|{grouping_scope}"
|
|
847
|
+
|
|
848
|
+
if group_key not in groups:
|
|
849
|
+
groups[group_key] = []
|
|
850
|
+
groups[group_key].append(node)
|
|
851
|
+
|
|
852
|
+
return groups
|
|
853
|
+
|
|
854
|
+
def _get_rule_name_from_node(self, node: Dict[str, Any]) -> str:
|
|
855
|
+
"""Get rule name from various node structures."""
|
|
856
|
+
# Try different ways to get rule name
|
|
857
|
+
if source_rule := node.get("sourceRule"):
|
|
858
|
+
return source_rule.get("name", "")
|
|
859
|
+
return node.get("name", node.get("title", ""))
|
|
860
|
+
|
|
861
|
+
def _get_provider_id_from_node(self, node: Dict[str, Any]) -> str:
|
|
862
|
+
"""Get provider ID from various node structures."""
|
|
863
|
+
# Try different ways to get provider ID
|
|
864
|
+
if entity_snapshot := node.get("entitySnapshot"):
|
|
865
|
+
return entity_snapshot.get("providerId", "")
|
|
866
|
+
|
|
867
|
+
# Try other asset lookup patterns
|
|
868
|
+
asset_fields = ["vulnerableAsset", "entity", "resource", "relatedEntity", "sourceEntity", "target"]
|
|
869
|
+
for field in asset_fields:
|
|
870
|
+
if asset_obj := node.get(field):
|
|
871
|
+
if provider_id := asset_obj.get("providerId"):
|
|
872
|
+
return provider_id
|
|
873
|
+
|
|
874
|
+
return ""
|
|
875
|
+
|
|
876
|
+
def _determine_grouping_scope(self, provider_id: str, rule_name: str) -> str:
|
|
877
|
+
"""
|
|
878
|
+
Determine the appropriate grouping scope for consolidation.
|
|
879
|
+
|
|
880
|
+
:param str provider_id: The provider ID
|
|
881
|
+
:param str rule_name: The rule name
|
|
882
|
+
:return: The grouping scope (server, resource group, or full path)
|
|
883
|
+
:rtype: str
|
|
884
|
+
"""
|
|
885
|
+
# For database issues, group by server
|
|
886
|
+
if "/databases/" in provider_id:
|
|
887
|
+
return provider_id.split("/databases/")[0]
|
|
888
|
+
|
|
889
|
+
# For App Configuration issues, group by resource group to consolidate multiple stores
|
|
890
|
+
if (
|
|
891
|
+
"app configuration" in rule_name.lower()
|
|
892
|
+
and "/microsoft.appconfiguration/configurationstores/" in provider_id
|
|
893
|
+
):
|
|
894
|
+
# Extract resource group path: /subscriptions/.../resourcegroups/rg_name
|
|
895
|
+
parts = provider_id.split("/resourcegroups/")
|
|
896
|
+
if len(parts) >= 2:
|
|
897
|
+
rg_part = parts[1].split("/")[0] # Get just the resource group name
|
|
898
|
+
return f"{parts[0]}/resourcegroups/{rg_part}"
|
|
899
|
+
|
|
900
|
+
# For other resources, use the full provider path (no consolidation)
|
|
901
|
+
return provider_id
|
|
902
|
+
|
|
903
|
+
def _create_consolidated_scanner_finding(
|
|
904
|
+
self, nodes: List[Dict[str, Any]], vulnerability_type: WizVulnerabilityType
|
|
905
|
+
) -> Optional[IntegrationFinding]:
|
|
906
|
+
"""
|
|
907
|
+
Create a consolidated finding from multiple nodes with the same rule.
|
|
908
|
+
|
|
909
|
+
:param List[Dict[str, Any]] nodes: List of nodes to consolidate
|
|
910
|
+
:param WizVulnerabilityType vulnerability_type: The vulnerability type
|
|
911
|
+
:return: Consolidated IntegrationFinding or None
|
|
912
|
+
:rtype: Optional[IntegrationFinding]
|
|
913
|
+
"""
|
|
914
|
+
# Use the first node as the base
|
|
915
|
+
base_node = nodes[0]
|
|
916
|
+
|
|
917
|
+
# Collect all asset identifiers and provider IDs
|
|
918
|
+
asset_ids = []
|
|
919
|
+
provider_ids = []
|
|
920
|
+
|
|
921
|
+
for node in nodes:
|
|
922
|
+
if asset_id := self.get_asset_id_from_node(node, vulnerability_type):
|
|
923
|
+
asset_ids.append(asset_id)
|
|
924
|
+
if provider_id := self.get_provider_unique_id_from_node(node, vulnerability_type):
|
|
925
|
+
provider_ids.append(provider_id)
|
|
926
|
+
|
|
927
|
+
# If we couldn't extract asset info, fall back to normal parsing
|
|
928
|
+
if not asset_ids:
|
|
929
|
+
return self.parse_finding(base_node, vulnerability_type)
|
|
930
|
+
|
|
931
|
+
# Create the finding using normal parsing, then override asset identifiers
|
|
932
|
+
base_finding = self.parse_finding(base_node, vulnerability_type)
|
|
933
|
+
if not base_finding:
|
|
934
|
+
return None
|
|
935
|
+
|
|
936
|
+
# Override with consolidated asset information
|
|
937
|
+
base_finding.asset_identifier = asset_ids[0] # Use first asset as primary
|
|
938
|
+
base_finding.issue_asset_identifier_value = "\n".join(provider_ids) if provider_ids else None
|
|
939
|
+
|
|
940
|
+
return base_finding
|
|
677
941
|
|
|
678
942
|
@classmethod
|
|
679
943
|
def get_issue_severity(cls, severity: str) -> regscale_models.IssueSeverity:
|
|
@@ -686,6 +950,37 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
686
950
|
"""
|
|
687
951
|
return cls.finding_severity_map.get(severity.capitalize(), regscale_models.IssueSeverity.Low)
|
|
688
952
|
|
|
953
|
+
def should_process_finding_by_severity(self, wiz_severity: str) -> bool:
|
|
954
|
+
"""
|
|
955
|
+
Check if finding should be processed based on minimum severity configuration.
|
|
956
|
+
|
|
957
|
+
:param str wiz_severity: The Wiz severity level (e.g., "INFORMATIONAL", "Low", "Medium", etc.)
|
|
958
|
+
:return: True if finding should be processed, False if it should be filtered out
|
|
959
|
+
:rtype: bool
|
|
960
|
+
"""
|
|
961
|
+
# Get minimum severity from configuration, default to "low"
|
|
962
|
+
min_severity = self.app.config.get("scanners", {}).get("wiz", {}).get("minimumSeverity", "low").lower()
|
|
963
|
+
|
|
964
|
+
# Log the configuration being used (only once to avoid spam)
|
|
965
|
+
if not hasattr(self, "_severity_config_logged"):
|
|
966
|
+
logger.debug(f"SEVERITY FILTER CONFIG: minimumSeverity = '{min_severity}'")
|
|
967
|
+
self._severity_config_logged = True
|
|
968
|
+
|
|
969
|
+
# Define severity hierarchy (lower index = higher severity)
|
|
970
|
+
severity_hierarchy = ["critical", "high", "medium", "low", "informational"]
|
|
971
|
+
|
|
972
|
+
try:
|
|
973
|
+
wiz_severity_lower = wiz_severity.lower()
|
|
974
|
+
min_severity_index = severity_hierarchy.index(min_severity)
|
|
975
|
+
finding_severity_index = severity_hierarchy.index(wiz_severity_lower)
|
|
976
|
+
|
|
977
|
+
# Process if finding severity is equal or higher (lower index) than minimum
|
|
978
|
+
return finding_severity_index <= min_severity_index
|
|
979
|
+
except ValueError:
|
|
980
|
+
# If severity not found in hierarchy, default to processing it
|
|
981
|
+
logger.warning(f"Unknown severity level: {wiz_severity}, processing anyway")
|
|
982
|
+
return True
|
|
983
|
+
|
|
689
984
|
def process_comments(self, comments_dict: Dict) -> Optional[str]:
|
|
690
985
|
"""
|
|
691
986
|
Processes comments from Wiz findings to match RegScale's comment format.
|
|
@@ -716,10 +1011,10 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
716
1011
|
"""
|
|
717
1012
|
# Define asset lookup patterns for different vulnerability types
|
|
718
1013
|
asset_lookup_patterns = {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
1014
|
+
WizVulnerabilityType.VULNERABILITY: "vulnerableAsset",
|
|
1015
|
+
WizVulnerabilityType.CONFIGURATION: "resource",
|
|
1016
|
+
WizVulnerabilityType.HOST_FINDING: "resource",
|
|
1017
|
+
WizVulnerabilityType.DATA_FINDING: "resource",
|
|
723
1018
|
WizVulnerabilityType.SECRET_FINDING: "resource",
|
|
724
1019
|
WizVulnerabilityType.NETWORK_EXPOSURE_FINDING: "exposedEntity",
|
|
725
1020
|
WizVulnerabilityType.END_OF_LIFE_FINDING: "vulnerableAsset",
|
|
@@ -738,7 +1033,76 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
738
1033
|
|
|
739
1034
|
# Standard case - direct id access
|
|
740
1035
|
asset_container = node.get(asset_lookup_key, {})
|
|
741
|
-
|
|
1036
|
+
asset_id = asset_container.get("id")
|
|
1037
|
+
|
|
1038
|
+
# Add debug logging to help diagnose missing assets
|
|
1039
|
+
if not asset_id:
|
|
1040
|
+
logger.debug(
|
|
1041
|
+
f"No asset ID found for {vulnerability_type.value} using key '{asset_lookup_key}'. "
|
|
1042
|
+
f"Available keys in node: {list(node.keys())}"
|
|
1043
|
+
)
|
|
1044
|
+
# Try alternative lookup patterns as fallback
|
|
1045
|
+
fallback_keys = ["vulnerableAsset", "resource", "exposedEntity", "entitySnapshot"]
|
|
1046
|
+
for fallback_key in fallback_keys:
|
|
1047
|
+
if fallback_key != asset_lookup_key and fallback_key in node:
|
|
1048
|
+
fallback_asset = node.get(fallback_key, {})
|
|
1049
|
+
if fallback_id := fallback_asset.get("id"):
|
|
1050
|
+
logger.debug(
|
|
1051
|
+
f"Found asset ID using fallback key '{fallback_key}' for {vulnerability_type.value}"
|
|
1052
|
+
)
|
|
1053
|
+
return fallback_id
|
|
1054
|
+
|
|
1055
|
+
return asset_id
|
|
1056
|
+
|
|
1057
|
+
def get_provider_unique_id_from_node(
|
|
1058
|
+
self, node: Dict[str, Any], vulnerability_type: WizVulnerabilityType
|
|
1059
|
+
) -> Optional[str]:
|
|
1060
|
+
"""
|
|
1061
|
+
Get the providerUniqueId from a node based on the vulnerability type.
|
|
1062
|
+
This provides more meaningful asset identification for eMASS exports.
|
|
1063
|
+
|
|
1064
|
+
:param Dict[str, Any] node: The Wiz finding node
|
|
1065
|
+
:param WizVulnerabilityType vulnerability_type: The type of vulnerability
|
|
1066
|
+
:return: The providerUniqueId or fallback to asset name/ID
|
|
1067
|
+
:rtype: Optional[str]
|
|
1068
|
+
"""
|
|
1069
|
+
# Define asset lookup patterns for different vulnerability types - aligned with get_asset_id_from_node
|
|
1070
|
+
asset_lookup_patterns = {
|
|
1071
|
+
WizVulnerabilityType.VULNERABILITY: "vulnerableAsset",
|
|
1072
|
+
WizVulnerabilityType.CONFIGURATION: "resource",
|
|
1073
|
+
WizVulnerabilityType.HOST_FINDING: "resource",
|
|
1074
|
+
WizVulnerabilityType.DATA_FINDING: "resource",
|
|
1075
|
+
WizVulnerabilityType.SECRET_FINDING: "resource",
|
|
1076
|
+
WizVulnerabilityType.NETWORK_EXPOSURE_FINDING: "exposedEntity",
|
|
1077
|
+
WizVulnerabilityType.END_OF_LIFE_FINDING: "vulnerableAsset",
|
|
1078
|
+
WizVulnerabilityType.EXTERNAL_ATTACH_SURFACE: "exposedEntity",
|
|
1079
|
+
WizVulnerabilityType.EXCESSIVE_ACCESS_FINDING: "scope",
|
|
1080
|
+
WizVulnerabilityType.ISSUE: "entitySnapshot",
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
asset_lookup_key = asset_lookup_patterns.get(vulnerability_type, "entitySnapshot")
|
|
1084
|
+
|
|
1085
|
+
if asset_lookup_key == "scope":
|
|
1086
|
+
# Handle special case for excessive access findings where ID is nested
|
|
1087
|
+
scope = node.get("scope", {})
|
|
1088
|
+
graph_entity = scope.get("graphEntity", {})
|
|
1089
|
+
# Try providerUniqueId first, fallback to name, then id
|
|
1090
|
+
return graph_entity.get("providerUniqueId") or graph_entity.get("name") or graph_entity.get("id")
|
|
1091
|
+
|
|
1092
|
+
# Standard case - get asset container and extract provider identifier
|
|
1093
|
+
asset_container = node.get(asset_lookup_key, {})
|
|
1094
|
+
|
|
1095
|
+
# For Issue queries, the field is called 'providerId' instead of 'providerUniqueId'
|
|
1096
|
+
if vulnerability_type == WizVulnerabilityType.ISSUE:
|
|
1097
|
+
return (
|
|
1098
|
+
asset_container.get("providerId")
|
|
1099
|
+
or asset_container.get("providerUniqueId")
|
|
1100
|
+
or asset_container.get("name")
|
|
1101
|
+
or asset_container.get("id")
|
|
1102
|
+
)
|
|
1103
|
+
|
|
1104
|
+
# For other queries, try providerUniqueId first
|
|
1105
|
+
return asset_container.get("providerUniqueId") or asset_container.get("name") or asset_container.get("id")
|
|
742
1106
|
|
|
743
1107
|
def parse_finding(
|
|
744
1108
|
self, node: Dict[str, Any], vulnerability_type: WizVulnerabilityType
|
|
@@ -769,28 +1133,16 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
769
1133
|
logger.error("Error parsing Wiz finding: %s", str(e), exc_info=True)
|
|
770
1134
|
return None
|
|
771
1135
|
|
|
772
|
-
def
|
|
1136
|
+
def _get_secret_finding_data(self, node: Dict[str, Any]) -> Dict[str, Any]:
|
|
773
1137
|
"""
|
|
774
|
-
|
|
1138
|
+
Extract data specific to secret findings.
|
|
775
1139
|
|
|
776
1140
|
:param Dict[str, Any] node: The Wiz finding node to parse
|
|
777
|
-
:return:
|
|
778
|
-
:rtype:
|
|
1141
|
+
:return: Dictionary containing secret-specific data
|
|
1142
|
+
:rtype: Dict[str, Any]
|
|
779
1143
|
"""
|
|
780
|
-
asset_id = node.get("resource", {}).get("id")
|
|
781
|
-
if not asset_id:
|
|
782
|
-
return None
|
|
783
|
-
|
|
784
|
-
first_seen = node.get("firstSeenAt") or get_current_datetime()
|
|
785
|
-
first_seen = format_to_regscale_iso(first_seen)
|
|
786
|
-
severity = self.get_issue_severity(node.get("severity", "Low"))
|
|
787
|
-
due_date = regscale_models.Issue.get_due_date(severity, self.app.config, "wiz", first_seen)
|
|
788
|
-
|
|
789
|
-
# Create meaningful title for secret findings
|
|
790
1144
|
secret_type = node.get("type", "Unknown Secret")
|
|
791
1145
|
resource_name = node.get("resource", {}).get("name", "Unknown Resource")
|
|
792
|
-
title = f"Secret Detected: {secret_type} in {resource_name}"
|
|
793
|
-
|
|
794
1146
|
# Build description with secret details
|
|
795
1147
|
description_parts = [
|
|
796
1148
|
f"Secret type: {secret_type}",
|
|
@@ -802,52 +1154,152 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
802
1154
|
if rule := node.get("rule", {}):
|
|
803
1155
|
description_parts.append(f"Detection rule: {rule.get('name', 'Unknown')}")
|
|
804
1156
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
status=self.map_status_to_issue_status(node.get("status", "Open")),
|
|
814
|
-
asset_identifier=asset_id,
|
|
815
|
-
external_id=node.get("id"),
|
|
816
|
-
first_seen=first_seen,
|
|
817
|
-
date_created=first_seen,
|
|
818
|
-
last_seen=format_to_regscale_iso(node.get("lastSeenAt") or get_current_datetime()),
|
|
819
|
-
remediation=f"Remove or properly secure the {secret_type} secret found in {resource_name}",
|
|
820
|
-
plugin_name=f"Wiz Secret Detection - {secret_type}",
|
|
821
|
-
vulnerability_type=WizVulnerabilityType.SECRET_FINDING.value,
|
|
822
|
-
due_date=due_date,
|
|
823
|
-
date_last_updated=format_to_regscale_iso(get_current_datetime()),
|
|
824
|
-
identification="Secret Scanning",
|
|
825
|
-
)
|
|
1157
|
+
return {
|
|
1158
|
+
"category": "Wiz Secret Detection",
|
|
1159
|
+
"title": f"Secret Detected: {secret_type} in {resource_name}",
|
|
1160
|
+
"description": "\n".join(description_parts),
|
|
1161
|
+
"remediation": f"Remove or properly secure the {secret_type} secret found in {resource_name}",
|
|
1162
|
+
"plugin_name": f"Wiz Secret Detection - {secret_type}",
|
|
1163
|
+
"identification": "Secret Scanning",
|
|
1164
|
+
}
|
|
826
1165
|
|
|
827
|
-
def
|
|
828
|
-
"""
|
|
1166
|
+
def _parse_secret_finding(self, node: Dict[str, Any]) -> Optional[IntegrationFinding]:
|
|
1167
|
+
"""
|
|
1168
|
+
Parse secret finding from Wiz.
|
|
829
1169
|
|
|
830
1170
|
:param Dict[str, Any] node: The Wiz finding node to parse
|
|
831
1171
|
:return: The parsed IntegrationFinding or None if parsing fails
|
|
832
1172
|
:rtype: Optional[IntegrationFinding]
|
|
833
1173
|
"""
|
|
834
|
-
|
|
1174
|
+
finding_data = self._get_secret_finding_data(node)
|
|
1175
|
+
return self._create_integration_finding(node, WizVulnerabilityType.SECRET_FINDING, finding_data)
|
|
1176
|
+
|
|
1177
|
+
def _create_integration_finding(
|
|
1178
|
+
self, node: Dict[str, Any], vulnerability_type: WizVulnerabilityType, finding_data: Dict[str, Any]
|
|
1179
|
+
) -> Optional[IntegrationFinding]:
|
|
1180
|
+
"""
|
|
1181
|
+
Unified method to create IntegrationFinding objects from Wiz data.
|
|
1182
|
+
|
|
1183
|
+
:param Dict[str, Any] node: The Wiz finding node to parse
|
|
1184
|
+
:param WizVulnerabilityType vulnerability_type: The type of vulnerability
|
|
1185
|
+
:param Dict[str, Any] finding_data: Finding-specific data (title, description, etc.)
|
|
1186
|
+
:return: The parsed IntegrationFinding or None if parsing fails
|
|
1187
|
+
:rtype: Optional[IntegrationFinding]
|
|
1188
|
+
"""
|
|
1189
|
+
# Get asset identifier
|
|
1190
|
+
asset_id = self.get_asset_id_from_node(node, vulnerability_type)
|
|
835
1191
|
if not asset_id:
|
|
1192
|
+
logger.warning(
|
|
1193
|
+
f"Skipping {vulnerability_type.value} finding '{node.get('name', 'Unknown')}' "
|
|
1194
|
+
f"(ID: {node.get('id', 'Unknown')}) - no asset identifier found"
|
|
1195
|
+
)
|
|
836
1196
|
return None
|
|
837
1197
|
|
|
838
|
-
|
|
839
|
-
|
|
1198
|
+
# Get meaningful asset identifier for eMASS exports
|
|
1199
|
+
provider_unique_id = self.get_provider_unique_id_from_node(node, vulnerability_type)
|
|
840
1200
|
|
|
841
|
-
#
|
|
842
|
-
|
|
1201
|
+
# Parse dates
|
|
1202
|
+
first_seen = self._get_first_seen_date(node)
|
|
1203
|
+
last_seen = self._get_last_seen_date(node, first_seen)
|
|
1204
|
+
# Get severity and calculate due date
|
|
1205
|
+
severity = self.get_issue_severity(finding_data.get("severity") or node.get("severity", "Low"))
|
|
843
1206
|
due_date = regscale_models.Issue.get_due_date(severity, self.app.config, "wiz", first_seen)
|
|
844
1207
|
|
|
845
|
-
#
|
|
1208
|
+
# Get status with diagnostic logging
|
|
1209
|
+
wiz_status = node.get("status", "Open")
|
|
1210
|
+
logger.debug(f"Processing Wiz finding {node.get('id', 'Unknown')}: raw status from node = '{wiz_status}'")
|
|
1211
|
+
status = self.map_status_to_issue_status(wiz_status)
|
|
1212
|
+
|
|
1213
|
+
# Add diagnostic logging for unexpected issue closure
|
|
1214
|
+
if status == regscale_models.IssueStatus.Closed and wiz_status.upper() not in ["RESOLVED", "REJECTED"]:
|
|
1215
|
+
logger.warning(
|
|
1216
|
+
f"Unexpected issue closure: Wiz status '{wiz_status}' mapped to Closed status "
|
|
1217
|
+
f"for finding {node.get('id', 'Unknown')} - '{finding_data.get('title', 'Unknown')}'. "
|
|
1218
|
+
f"This may indicate a mapping configuration issue."
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
# Process comments if available
|
|
1222
|
+
comments_dict = node.get("commentThread", {})
|
|
1223
|
+
formatted_comments = self.process_comments(comments_dict) if comments_dict else None
|
|
1224
|
+
|
|
1225
|
+
# Build IntegrationFinding with unified data structure
|
|
1226
|
+
integration_finding_data = {
|
|
1227
|
+
"control_labels": [],
|
|
1228
|
+
"category": finding_data.get("category", "Wiz Vulnerability"),
|
|
1229
|
+
"title": finding_data.get("title", node.get("name", "Unknown vulnerability")),
|
|
1230
|
+
"description": finding_data.get("description", node.get("description", "")),
|
|
1231
|
+
"severity": severity,
|
|
1232
|
+
"status": status,
|
|
1233
|
+
"asset_identifier": asset_id,
|
|
1234
|
+
"issue_asset_identifier_value": provider_unique_id,
|
|
1235
|
+
"external_id": finding_data.get("external_id", node.get("id")),
|
|
1236
|
+
"first_seen": first_seen,
|
|
1237
|
+
"date_created": first_seen,
|
|
1238
|
+
"last_seen": last_seen,
|
|
1239
|
+
"remediation": finding_data.get("remediation", node.get("description", "")),
|
|
1240
|
+
"plugin_name": finding_data.get("plugin_name", node.get("name", "Unknown")),
|
|
1241
|
+
"vulnerability_type": vulnerability_type.value,
|
|
1242
|
+
"due_date": due_date,
|
|
1243
|
+
"date_last_updated": format_to_regscale_iso(get_current_datetime()),
|
|
1244
|
+
"identification": finding_data.get("identification", "Vulnerability Assessment"),
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
# Add optional fields if present
|
|
1248
|
+
if formatted_comments:
|
|
1249
|
+
integration_finding_data["comments"] = formatted_comments
|
|
1250
|
+
integration_finding_data["poam_comments"] = formatted_comments
|
|
1251
|
+
|
|
1252
|
+
# Add CVE-specific fields for generic findings
|
|
1253
|
+
if finding_data.get("cve"):
|
|
1254
|
+
integration_finding_data["cve"] = finding_data["cve"]
|
|
1255
|
+
if finding_data.get("cvss_score"):
|
|
1256
|
+
integration_finding_data["cvss_score"] = finding_data["cvss_score"]
|
|
1257
|
+
integration_finding_data["cvss_v3_base_score"] = finding_data["cvss_score"]
|
|
1258
|
+
if finding_data.get("source_rule_id"):
|
|
1259
|
+
integration_finding_data["source_rule_id"] = finding_data["source_rule_id"]
|
|
1260
|
+
|
|
1261
|
+
return IntegrationFinding(**integration_finding_data)
|
|
1262
|
+
|
|
1263
|
+
def _get_first_seen_date(self, node: Dict[str, Any]) -> str:
|
|
1264
|
+
"""
|
|
1265
|
+
Get the first seen date from a Wiz node, with fallbacks.
|
|
1266
|
+
|
|
1267
|
+
:param Dict[str, Any] node: The Wiz finding node
|
|
1268
|
+
:return: ISO formatted first seen date
|
|
1269
|
+
:rtype: str
|
|
1270
|
+
"""
|
|
1271
|
+
first_seen = node.get("firstSeenAt") or node.get("firstDetectedAt") or get_current_datetime()
|
|
1272
|
+
return format_to_regscale_iso(first_seen)
|
|
1273
|
+
|
|
1274
|
+
def _get_last_seen_date(self, node: Dict[str, Any], first_seen_fallback: str) -> str:
|
|
1275
|
+
"""
|
|
1276
|
+
Get the last seen date from a Wiz node, with fallbacks.
|
|
1277
|
+
|
|
1278
|
+
:param Dict[str, Any] node: The Wiz finding node
|
|
1279
|
+
:param str first_seen_fallback: Fallback date if no last seen available
|
|
1280
|
+
:return: ISO formatted last seen date
|
|
1281
|
+
:rtype: str
|
|
1282
|
+
"""
|
|
1283
|
+
last_seen = (
|
|
1284
|
+
node.get("lastSeenAt")
|
|
1285
|
+
or node.get("lastDetectedAt")
|
|
1286
|
+
or node.get("analyzedAt")
|
|
1287
|
+
or first_seen_fallback
|
|
1288
|
+
or get_current_datetime()
|
|
1289
|
+
)
|
|
1290
|
+
return format_to_regscale_iso(last_seen)
|
|
1291
|
+
|
|
1292
|
+
def _get_network_exposure_finding_data(self, node: Dict[str, Any]) -> Dict[str, Any]:
|
|
1293
|
+
"""
|
|
1294
|
+
Extract data specific to network exposure findings.
|
|
1295
|
+
|
|
1296
|
+
:param Dict[str, Any] node: The Wiz finding node to parse
|
|
1297
|
+
:return: Dictionary containing network exposure-specific data
|
|
1298
|
+
:rtype: Dict[str, Any]
|
|
1299
|
+
"""
|
|
846
1300
|
exposed_entity = node.get("exposedEntity", {})
|
|
847
1301
|
entity_name = exposed_entity.get("name", "Unknown Entity")
|
|
848
1302
|
port_range = node.get("portRange", "Unknown Port")
|
|
849
|
-
title = f"Network Exposure: {entity_name} on {port_range}"
|
|
850
|
-
|
|
851
1303
|
# Build description with network details
|
|
852
1304
|
description_parts = [
|
|
853
1305
|
f"Exposed entity: {entity_name} ({exposed_entity.get('type', 'Unknown Type')})",
|
|
@@ -861,52 +1313,37 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
861
1313
|
if net_protocols := node.get("networkProtocols"):
|
|
862
1314
|
description_parts.append(f"Network protocols: {', '.join(net_protocols)}")
|
|
863
1315
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
asset_identifier=asset_id,
|
|
874
|
-
external_id=node.get("id"),
|
|
875
|
-
first_seen=first_seen,
|
|
876
|
-
date_created=first_seen,
|
|
877
|
-
last_seen=first_seen, # Network exposures may not have lastSeen
|
|
878
|
-
remediation=f"Review and restrict network access to {entity_name} on {port_range}",
|
|
879
|
-
plugin_name=f"Wiz Network Exposure - {port_range}",
|
|
880
|
-
vulnerability_type=WizVulnerabilityType.NETWORK_EXPOSURE_FINDING.value,
|
|
881
|
-
due_date=due_date,
|
|
882
|
-
date_last_updated=format_to_regscale_iso(get_current_datetime()),
|
|
883
|
-
identification="Network Security Assessment",
|
|
884
|
-
)
|
|
1316
|
+
return {
|
|
1317
|
+
"category": "Wiz Network Exposure",
|
|
1318
|
+
"title": f"Network Exposure: {entity_name} on {port_range}",
|
|
1319
|
+
"description": "\n".join(description_parts),
|
|
1320
|
+
"severity": "Medium", # Network exposures typically don't have explicit severity
|
|
1321
|
+
"remediation": f"Review and restrict network access to {entity_name} on {port_range}",
|
|
1322
|
+
"plugin_name": f"Wiz Network Exposure - {port_range}",
|
|
1323
|
+
"identification": "Network Security Assessment",
|
|
1324
|
+
}
|
|
885
1325
|
|
|
886
|
-
def
|
|
887
|
-
"""Parse
|
|
1326
|
+
def _parse_network_exposure_finding(self, node: Dict[str, Any]) -> Optional[IntegrationFinding]:
|
|
1327
|
+
"""Parse network exposure finding from Wiz.
|
|
888
1328
|
|
|
889
1329
|
:param Dict[str, Any] node: The Wiz finding node to parse
|
|
890
1330
|
:return: The parsed IntegrationFinding or None if parsing fails
|
|
891
1331
|
:rtype: Optional[IntegrationFinding]
|
|
892
1332
|
"""
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
return None
|
|
1333
|
+
finding_data = self._get_network_exposure_finding_data(node)
|
|
1334
|
+
return self._create_integration_finding(node, WizVulnerabilityType.NETWORK_EXPOSURE_FINDING, finding_data)
|
|
896
1335
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
# External attack surface findings are typically high severity
|
|
901
|
-
severity = regscale_models.IssueSeverity.High
|
|
902
|
-
due_date = regscale_models.Issue.get_due_date(severity, self.app.config, "wiz", first_seen)
|
|
1336
|
+
def _get_external_attack_surface_finding_data(self, node: Dict[str, Any]) -> Dict[str, Any]:
|
|
1337
|
+
"""
|
|
1338
|
+
Extract data specific to external attack surface findings.
|
|
903
1339
|
|
|
904
|
-
|
|
1340
|
+
:param Dict[str, Any] node: The Wiz finding node to parse
|
|
1341
|
+
:return: Dictionary containing external attack surface-specific data
|
|
1342
|
+
:rtype: Dict[str, Any]
|
|
1343
|
+
"""
|
|
905
1344
|
exposed_entity = node.get("exposedEntity", {})
|
|
906
1345
|
entity_name = exposed_entity.get("name", "Unknown Entity")
|
|
907
1346
|
port_range = node.get("portRange", "Unknown Port")
|
|
908
|
-
title = f"External Attack Surface: {entity_name} exposed on {port_range}"
|
|
909
|
-
|
|
910
1347
|
# Build description with attack surface details
|
|
911
1348
|
description_parts = [
|
|
912
1349
|
f"Externally exposed entity: {entity_name} ({exposed_entity.get('type', 'Unknown Type')})",
|
|
@@ -920,49 +1357,34 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
920
1357
|
endpoint_names = [ep.get("name", "Unknown") for ep in endpoints[:3]] # Limit to first 3
|
|
921
1358
|
description_parts.append(f"Application endpoints: {', '.join(endpoint_names)}")
|
|
922
1359
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
asset_identifier=asset_id,
|
|
933
|
-
external_id=node.get("id"),
|
|
934
|
-
first_seen=first_seen,
|
|
935
|
-
date_created=first_seen,
|
|
936
|
-
last_seen=first_seen,
|
|
937
|
-
remediation=f"Review external exposure of {entity_name} and implement proper access controls",
|
|
938
|
-
plugin_name=f"Wiz External Attack Surface - {port_range}",
|
|
939
|
-
vulnerability_type=WizVulnerabilityType.EXTERNAL_ATTACH_SURFACE.value,
|
|
940
|
-
due_date=due_date,
|
|
941
|
-
date_last_updated=format_to_regscale_iso(get_current_datetime()),
|
|
942
|
-
identification="External Attack Surface Assessment",
|
|
943
|
-
)
|
|
1360
|
+
return {
|
|
1361
|
+
"category": "Wiz External Attack Surface",
|
|
1362
|
+
"title": f"External Attack Surface: {entity_name} exposed on {port_range}",
|
|
1363
|
+
"description": "\n".join(description_parts),
|
|
1364
|
+
"severity": "High", # External attack surface findings are typically high severity
|
|
1365
|
+
"remediation": f"Review external exposure of {entity_name} and implement proper access controls",
|
|
1366
|
+
"plugin_name": f"Wiz External Attack Surface - {port_range}",
|
|
1367
|
+
"identification": "External Attack Surface Assessment",
|
|
1368
|
+
}
|
|
944
1369
|
|
|
945
|
-
def
|
|
946
|
-
"""Parse
|
|
1370
|
+
def _parse_external_attack_surface_finding(self, node: Dict[str, Any]) -> Optional[IntegrationFinding]:
|
|
1371
|
+
"""Parse external attack surface finding from Wiz.
|
|
947
1372
|
|
|
948
1373
|
:param Dict[str, Any] node: The Wiz finding node to parse
|
|
949
1374
|
:return: The parsed IntegrationFinding or None if parsing fails
|
|
950
1375
|
:rtype: Optional[IntegrationFinding]
|
|
951
1376
|
"""
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
if not asset_id:
|
|
955
|
-
return None
|
|
1377
|
+
finding_data = self._get_external_attack_surface_finding_data(node)
|
|
1378
|
+
return self._create_integration_finding(node, WizVulnerabilityType.EXTERNAL_ATTACH_SURFACE, finding_data)
|
|
956
1379
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
due_date = regscale_models.Issue.get_due_date(severity, self.app.config, "wiz", first_seen)
|
|
961
|
-
|
|
962
|
-
# Use the finding name directly as it's descriptive
|
|
963
|
-
title = node.get("name", "Excessive Access Detected")
|
|
964
|
-
description = node.get("description", "")
|
|
1380
|
+
def _get_excessive_access_finding_data(self, node: Dict[str, Any]) -> Dict[str, Any]:
|
|
1381
|
+
"""
|
|
1382
|
+
Extract data specific to excessive access findings.
|
|
965
1383
|
|
|
1384
|
+
:param Dict[str, Any] node: The Wiz finding node to parse
|
|
1385
|
+
:return: Dictionary containing excessive access-specific data
|
|
1386
|
+
:rtype: Dict[str, Any]
|
|
1387
|
+
"""
|
|
966
1388
|
# Add remediation details
|
|
967
1389
|
remediation_parts = [node.get("description", "")]
|
|
968
1390
|
if remediation_instructions := node.get("remediationInstructions"):
|
|
@@ -970,42 +1392,34 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
970
1392
|
if policy_name := node.get("builtInPolicyRemediationName"):
|
|
971
1393
|
remediation_parts.append(f"Built-in policy: {policy_name}")
|
|
972
1394
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
status=self.map_status_to_issue_status(node.get("status", "Open")),
|
|
982
|
-
asset_identifier=asset_id,
|
|
983
|
-
external_id=node.get("id"),
|
|
984
|
-
first_seen=first_seen,
|
|
985
|
-
date_created=first_seen,
|
|
986
|
-
last_seen=first_seen,
|
|
987
|
-
remediation=remediation,
|
|
988
|
-
plugin_name=f"Wiz Excessive Access - {node.get('remediationType', 'Unknown')}",
|
|
989
|
-
vulnerability_type=WizVulnerabilityType.EXCESSIVE_ACCESS_FINDING.value,
|
|
990
|
-
due_date=due_date,
|
|
991
|
-
date_last_updated=format_to_regscale_iso(get_current_datetime()),
|
|
992
|
-
identification="Access Control Assessment",
|
|
993
|
-
)
|
|
1395
|
+
return {
|
|
1396
|
+
"category": "Wiz Excessive Access",
|
|
1397
|
+
"title": node.get("name", "Excessive Access Detected"),
|
|
1398
|
+
"description": node.get("description", ""),
|
|
1399
|
+
"remediation": "\n".join(filter(None, remediation_parts)),
|
|
1400
|
+
"plugin_name": f"Wiz Excessive Access - {node.get('remediationType', 'Unknown')}",
|
|
1401
|
+
"identification": "Access Control Assessment",
|
|
1402
|
+
}
|
|
994
1403
|
|
|
995
|
-
def
|
|
996
|
-
"""Parse
|
|
997
|
-
asset_id = node.get("vulnerableAsset", {}).get("id")
|
|
998
|
-
if not asset_id:
|
|
999
|
-
return None
|
|
1404
|
+
def _parse_excessive_access_finding(self, node: Dict[str, Any]) -> Optional[IntegrationFinding]:
|
|
1405
|
+
"""Parse excessive access finding from Wiz.
|
|
1000
1406
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1407
|
+
:param Dict[str, Any] node: The Wiz finding node to parse
|
|
1408
|
+
:return: The parsed IntegrationFinding or None if parsing fails
|
|
1409
|
+
:rtype: Optional[IntegrationFinding]
|
|
1410
|
+
"""
|
|
1411
|
+
finding_data = self._get_excessive_access_finding_data(node)
|
|
1412
|
+
return self._create_integration_finding(node, WizVulnerabilityType.EXCESSIVE_ACCESS_FINDING, finding_data)
|
|
1413
|
+
|
|
1414
|
+
def _get_end_of_life_finding_data(self, node: Dict[str, Any]) -> Dict[str, Any]:
|
|
1415
|
+
"""
|
|
1416
|
+
Extract data specific to end of life findings.
|
|
1005
1417
|
|
|
1006
|
-
|
|
1418
|
+
:param Dict[str, Any] node: The Wiz finding node to parse
|
|
1419
|
+
:return: Dictionary containing end of life-specific data
|
|
1420
|
+
:rtype: Dict[str, Any]
|
|
1421
|
+
"""
|
|
1007
1422
|
name = node.get("name", "Unknown Technology")
|
|
1008
|
-
title = f"End of Life: {name}"
|
|
1009
1423
|
|
|
1010
1424
|
# Build description with EOL details
|
|
1011
1425
|
description_parts = [node.get("description", "")]
|
|
@@ -1014,42 +1428,30 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1014
1428
|
if recommended_version := node.get("recommendedVersion"):
|
|
1015
1429
|
description_parts.append(f"Recommended version: {recommended_version}")
|
|
1016
1430
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
asset_identifier=asset_id,
|
|
1027
|
-
external_id=node.get("id"),
|
|
1028
|
-
first_seen=first_seen,
|
|
1029
|
-
date_created=first_seen,
|
|
1030
|
-
last_seen=format_to_regscale_iso(node.get("lastDetectedAt") or get_current_datetime()),
|
|
1031
|
-
remediation=f"Upgrade {name} to a supported version",
|
|
1032
|
-
plugin_name=f"Wiz End of Life - {name}",
|
|
1033
|
-
vulnerability_type=WizVulnerabilityType.END_OF_LIFE_FINDING.value,
|
|
1034
|
-
due_date=due_date,
|
|
1035
|
-
date_last_updated=format_to_regscale_iso(get_current_datetime()),
|
|
1036
|
-
identification="Technology Lifecycle Assessment",
|
|
1037
|
-
)
|
|
1431
|
+
return {
|
|
1432
|
+
"category": "Wiz End of Life",
|
|
1433
|
+
"title": f"End of Life: {name}",
|
|
1434
|
+
"description": "\n".join(filter(None, description_parts)),
|
|
1435
|
+
"severity": "High", # End of life findings are typically high severity
|
|
1436
|
+
"remediation": f"Upgrade {name} to a supported version",
|
|
1437
|
+
"plugin_name": f"Wiz End of Life - {name}",
|
|
1438
|
+
"identification": "Technology Lifecycle Assessment",
|
|
1439
|
+
}
|
|
1038
1440
|
|
|
1039
|
-
def
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
asset_id = self.get_asset_id_from_node(node, vulnerability_type)
|
|
1044
|
-
if not asset_id:
|
|
1045
|
-
return None
|
|
1441
|
+
def _parse_end_of_life_finding(self, node: Dict[str, Any]) -> Optional[IntegrationFinding]:
|
|
1442
|
+
"""Parse end of life finding from Wiz."""
|
|
1443
|
+
finding_data = self._get_end_of_life_finding_data(node)
|
|
1444
|
+
return self._create_integration_finding(node, WizVulnerabilityType.END_OF_LIFE_FINDING, finding_data)
|
|
1046
1445
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
due_date = regscale_models.Issue.get_due_date(severity, self.app.config, "wiz", first_seen)
|
|
1446
|
+
def _get_generic_finding_data(self, node: Dict[str, Any]) -> Dict[str, Any]:
|
|
1447
|
+
"""
|
|
1448
|
+
Extract data specific to generic findings.
|
|
1051
1449
|
|
|
1052
|
-
|
|
1450
|
+
:param Dict[str, Any] node: The Wiz finding node to parse
|
|
1451
|
+
:param WizVulnerabilityType vulnerability_type: The type of vulnerability
|
|
1452
|
+
:return: Dictionary containing generic finding-specific data
|
|
1453
|
+
:rtype: Dict[str, Any]
|
|
1454
|
+
"""
|
|
1053
1455
|
name: str = node.get("name", "")
|
|
1054
1456
|
cve = (
|
|
1055
1457
|
name
|
|
@@ -1057,36 +1459,25 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1057
1459
|
else node.get("cve", name)
|
|
1058
1460
|
)
|
|
1059
1461
|
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
cve=cve,
|
|
1080
|
-
plugin_name=cve,
|
|
1081
|
-
cvss_v3_base_score=node.get("score"),
|
|
1082
|
-
source_rule_id=node.get("sourceRule", {}).get("id"),
|
|
1083
|
-
vulnerability_type=vulnerability_type.value,
|
|
1084
|
-
due_date=due_date,
|
|
1085
|
-
date_last_updated=format_to_regscale_iso(get_current_datetime()),
|
|
1086
|
-
identification="Vulnerability Assessment",
|
|
1087
|
-
comments=formatted_comments,
|
|
1088
|
-
poam_comments=formatted_comments,
|
|
1089
|
-
)
|
|
1462
|
+
return {
|
|
1463
|
+
"category": "Wiz Vulnerability",
|
|
1464
|
+
"title": node.get("name", "Unknown vulnerability"),
|
|
1465
|
+
"description": node.get("description", ""),
|
|
1466
|
+
"external_id": f"{node.get('sourceRule', {'id': cve}).get('id')}",
|
|
1467
|
+
"remediation": node.get("description", ""),
|
|
1468
|
+
"plugin_name": cve,
|
|
1469
|
+
"identification": "Vulnerability Assessment",
|
|
1470
|
+
"cve": cve,
|
|
1471
|
+
"cvss_score": node.get("score"),
|
|
1472
|
+
"source_rule_id": node.get("sourceRule", {}).get("id"),
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
def _parse_generic_finding(
|
|
1476
|
+
self, node: Dict[str, Any], vulnerability_type: WizVulnerabilityType
|
|
1477
|
+
) -> Optional[IntegrationFinding]:
|
|
1478
|
+
"""Generic parsing method for fallback cases."""
|
|
1479
|
+
finding_data = self._get_generic_finding_data(node)
|
|
1480
|
+
return self._create_integration_finding(node, vulnerability_type, finding_data)
|
|
1090
1481
|
|
|
1091
1482
|
def get_compliance_settings(self):
|
|
1092
1483
|
"""
|
|
@@ -1164,12 +1555,22 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1164
1555
|
"""
|
|
1165
1556
|
label_lower = label.lower()
|
|
1166
1557
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1558
|
+
logger.debug(f"Checking compliance label matching: status='{status_lower}', label='{label_lower}'")
|
|
1559
|
+
|
|
1560
|
+
# Check for open status mappings (including IN_PROGRESS)
|
|
1561
|
+
if status_lower in ["open", "in_progress"] and label_lower in [
|
|
1562
|
+
"open",
|
|
1563
|
+
"active",
|
|
1564
|
+
"new",
|
|
1565
|
+
"in progress",
|
|
1566
|
+
"in_progress",
|
|
1567
|
+
]:
|
|
1568
|
+
logger.debug(f"Matched status '{status_lower}' with label '{label_lower}' -> IssueStatus.Open")
|
|
1169
1569
|
return IssueStatus.Open
|
|
1170
1570
|
|
|
1171
1571
|
# Check for closed status mappings
|
|
1172
1572
|
if status_lower in ["resolved", "rejected"] and label_lower in ["closed", "resolved", "rejected", "completed"]:
|
|
1573
|
+
logger.debug(f"Matched status '{status_lower}' with label '{label_lower}' -> IssueStatus.Closed")
|
|
1173
1574
|
return IssueStatus.Closed
|
|
1174
1575
|
|
|
1175
1576
|
return None
|
|
@@ -1184,10 +1585,19 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1184
1585
|
"""
|
|
1185
1586
|
status_lower = status.lower()
|
|
1186
1587
|
|
|
1187
|
-
|
|
1588
|
+
# Add debug logging to trace status mapping
|
|
1589
|
+
logger.debug(f"Mapping Wiz status: original='{status}', lowercase='{status_lower}'")
|
|
1590
|
+
|
|
1591
|
+
# Map open and in-progress statuses to Open
|
|
1592
|
+
if status_lower in ["open", "in_progress"]:
|
|
1593
|
+
logger.debug(f"Wiz status '{status}' mapped to IssueStatus.Open")
|
|
1188
1594
|
return IssueStatus.Open
|
|
1595
|
+
# Map resolved and rejected statuses to Closed
|
|
1189
1596
|
if status_lower in ["resolved", "rejected"]:
|
|
1597
|
+
logger.debug(f"Wiz status '{status}' mapped to IssueStatus.Closed")
|
|
1190
1598
|
return IssueStatus.Closed
|
|
1599
|
+
# Default to Open for any unknown status
|
|
1600
|
+
logger.debug(f"Unknown Wiz status '{status}' encountered, defaulting to Open")
|
|
1191
1601
|
return IssueStatus.Open
|
|
1192
1602
|
|
|
1193
1603
|
def fetch_assets(self, *args, **kwargs) -> Iterator[IntegrationAsset]:
|
|
@@ -1298,6 +1708,13 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1298
1708
|
|
|
1299
1709
|
return software_version, software_vendor, software_name
|
|
1300
1710
|
|
|
1711
|
+
@lru_cache()
|
|
1712
|
+
def get_user_id(self) -> str:
|
|
1713
|
+
"""Function to return the default user ID
|
|
1714
|
+
:return: The default user ID as a string
|
|
1715
|
+
"""
|
|
1716
|
+
return RegScaleModel.get_user_id()
|
|
1717
|
+
|
|
1301
1718
|
def parse_asset(self, node: Dict[str, Any]) -> Optional[IntegrationAsset]:
|
|
1302
1719
|
"""
|
|
1303
1720
|
Parses Wiz assets
|
|
@@ -1306,8 +1723,9 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1306
1723
|
:return: The parsed IntegrationAsset
|
|
1307
1724
|
:rtype: Optional[IntegrationAsset]
|
|
1308
1725
|
"""
|
|
1309
|
-
|
|
1726
|
+
|
|
1310
1727
|
wiz_entity = node.get("graphEntity", {})
|
|
1728
|
+
name = wiz_entity.get("providerUniqueId") or node.get("name", "")
|
|
1311
1729
|
if not wiz_entity:
|
|
1312
1730
|
logger.warning("No graph entity found for asset %s", name)
|
|
1313
1731
|
return None
|
|
@@ -1336,7 +1754,7 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1336
1754
|
other_tracking_number=node.get("id", ""),
|
|
1337
1755
|
identifier=node.get("id", ""),
|
|
1338
1756
|
asset_type=create_asset_type(node.get("type", "")),
|
|
1339
|
-
asset_owner_id=
|
|
1757
|
+
asset_owner_id=self.get_user_id(),
|
|
1340
1758
|
parent_id=self.plan_id,
|
|
1341
1759
|
parent_module=regscale_models.SecurityPlan.get_module_slug(),
|
|
1342
1760
|
asset_category=map_category(node),
|
|
@@ -1572,7 +1990,11 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1572
1990
|
:param str identifier: The missing asset identifier
|
|
1573
1991
|
:rtype: None
|
|
1574
1992
|
"""
|
|
1575
|
-
|
|
1993
|
+
# Only log detailed diagnostics for the first occurrence of each asset
|
|
1994
|
+
if identifier in self.alerted_assets:
|
|
1995
|
+
return
|
|
1996
|
+
|
|
1997
|
+
logger.debug("Analyzing missing asset: %s", identifier)
|
|
1576
1998
|
|
|
1577
1999
|
# Define inventory files to search (constant moved up for clarity)
|
|
1578
2000
|
inventory_files = (
|
|
@@ -1647,10 +2069,13 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1647
2069
|
asset_type = self._extract_asset_type_from_node(asset_info)
|
|
1648
2070
|
asset_name = self._extract_asset_name_from_node(asset_info)
|
|
1649
2071
|
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
2072
|
+
# Track missing asset types for summary reporting
|
|
2073
|
+
self._missing_asset_types.add(asset_type)
|
|
2074
|
+
|
|
2075
|
+
logger.info(
|
|
2076
|
+
"Missing asset found in cached data - ID: %s, Type: %s, Name: '%s', Source: %s\n"
|
|
2077
|
+
" Action: Consider adding '%s' to RECOMMENDED_WIZ_INVENTORY_TYPES in constants.py\n"
|
|
2078
|
+
" Then re-run: regscale wiz inventory --wiz_project_id <project_id> -id <plan_id>",
|
|
1654
2079
|
identifier,
|
|
1655
2080
|
asset_type,
|
|
1656
2081
|
asset_name,
|
|
@@ -1665,13 +2090,11 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1665
2090
|
:param str identifier: Asset identifier
|
|
1666
2091
|
:rtype: None
|
|
1667
2092
|
"""
|
|
1668
|
-
logger.
|
|
1669
|
-
"
|
|
1670
|
-
"
|
|
1671
|
-
"
|
|
1672
|
-
"
|
|
1673
|
-
" - Asset type not included in current queries\n"
|
|
1674
|
-
" - Asset deleted from Wiz but finding still exists",
|
|
2093
|
+
logger.debug(
|
|
2094
|
+
"Asset not found in cached data - ID: %s. Possible reasons: "
|
|
2095
|
+
"(1) Asset from different Wiz project, "
|
|
2096
|
+
"(2) Asset type not included in queries, "
|
|
2097
|
+
"(3) Asset deleted from Wiz",
|
|
1675
2098
|
identifier,
|
|
1676
2099
|
)
|
|
1677
2100
|
|