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
|
@@ -4,10 +4,11 @@ import logging
|
|
|
4
4
|
import re
|
|
5
5
|
from typing import List, Dict, Any, Iterator, Optional
|
|
6
6
|
|
|
7
|
+
from regscale import models as regscale_models
|
|
7
8
|
from regscale.core.app.utils.parser_utils import safe_datetime_str
|
|
8
9
|
from regscale.integrations.scanner_integration import IntegrationFinding
|
|
9
|
-
from regscale.utils.dict_utils import get_value
|
|
10
10
|
from regscale.models import Issue
|
|
11
|
+
from regscale.utils.dict_utils import get_value
|
|
11
12
|
from .constants import (
|
|
12
13
|
get_wiz_issue_queries,
|
|
13
14
|
WizVulnerabilityType,
|
|
@@ -26,29 +27,608 @@ class WizIssue(WizVulnerabilityIntegration):
|
|
|
26
27
|
asset_identifier_field = "wizId"
|
|
27
28
|
issue_identifier_field = "wizId"
|
|
28
29
|
|
|
29
|
-
def get_query_types(self, project_id: str) -> List[Dict[str, Any]]:
|
|
30
|
+
def get_query_types(self, project_id: str, filter_by: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
|
|
30
31
|
"""Get the query types for issue scanning.
|
|
31
32
|
|
|
32
33
|
:param str project_id: The project ID to get queries for
|
|
34
|
+
:param Optional[Dict[str, Any]] filter_by: Optional filter criteria to override defaults
|
|
33
35
|
:return: List of query types
|
|
34
36
|
:rtype: List[Dict[str, Any]]
|
|
35
37
|
"""
|
|
36
|
-
return get_wiz_issue_queries(project_id=project_id)
|
|
38
|
+
return get_wiz_issue_queries(project_id=project_id, filter_by=filter_by)
|
|
37
39
|
|
|
38
40
|
def parse_findings(
|
|
39
41
|
self, nodes: List[Dict[str, Any]], vulnerability_type: WizVulnerabilityType
|
|
40
42
|
) -> Iterator[IntegrationFinding]:
|
|
41
43
|
"""
|
|
42
|
-
Parse the Wiz issues into IntegrationFinding objects
|
|
44
|
+
Parse the Wiz issues into IntegrationFinding objects.
|
|
45
|
+
Groups issues by source rule and server to consolidate multiple database assets.
|
|
43
46
|
:param nodes:
|
|
44
47
|
:param vulnerability_type:
|
|
45
48
|
:return:
|
|
46
49
|
"""
|
|
50
|
+
logger.debug(f"ISSUE PROCESSING ANALYSIS: Received {len(nodes)} raw Wiz issues for processing")
|
|
51
|
+
|
|
52
|
+
# Analyze and log raw issue statistics
|
|
53
|
+
self._log_raw_issue_statistics(nodes)
|
|
54
|
+
|
|
55
|
+
# Filter nodes by minimum severity configuration
|
|
56
|
+
filtered_nodes = self._filter_nodes_by_severity(nodes)
|
|
57
|
+
if not filtered_nodes:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
# Group and process issues for consolidation
|
|
61
|
+
grouped_issues = self._group_issues_for_consolidation(filtered_nodes)
|
|
62
|
+
self._log_consolidation_analysis(grouped_issues)
|
|
63
|
+
|
|
64
|
+
# Generate findings from grouped issues
|
|
65
|
+
yield from self._generate_findings_from_groups(grouped_issues, vulnerability_type, len(nodes))
|
|
66
|
+
|
|
67
|
+
def _log_raw_issue_statistics(self, nodes: List[Dict[str, Any]]) -> None:
|
|
68
|
+
"""
|
|
69
|
+
Count and log raw issue statistics by severity and status.
|
|
70
|
+
|
|
71
|
+
:param List[Dict[str, Any]] nodes: List of raw Wiz issues
|
|
72
|
+
"""
|
|
73
|
+
severity_counts: Dict[str, int] = {}
|
|
74
|
+
status_counts: Dict[str, int] = {}
|
|
75
|
+
|
|
76
|
+
for node in nodes:
|
|
77
|
+
severity = node.get("severity", "Low")
|
|
78
|
+
status = node.get("status", "OPEN")
|
|
79
|
+
severity_counts[severity] = severity_counts.get(severity, 0) + 1
|
|
80
|
+
status_counts[status] = status_counts.get(status, 0) + 1
|
|
81
|
+
|
|
82
|
+
logger.debug(f"Raw issue breakdown by severity: {severity_counts}")
|
|
83
|
+
logger.debug(f"Raw issue breakdown by status: {status_counts}")
|
|
84
|
+
|
|
85
|
+
def _filter_nodes_by_severity(self, nodes: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
86
|
+
"""
|
|
87
|
+
Filter nodes based on minimum severity configuration.
|
|
88
|
+
|
|
89
|
+
:param List[Dict[str, Any]] nodes: List of raw Wiz issues
|
|
90
|
+
:return: Filtered list of issues that meet severity requirements
|
|
91
|
+
:rtype: List[Dict[str, Any]]
|
|
92
|
+
"""
|
|
93
|
+
filtered_nodes = []
|
|
94
|
+
filtered_out_count = 0
|
|
95
|
+
|
|
47
96
|
for node in nodes:
|
|
48
|
-
|
|
97
|
+
wiz_severity = node.get("severity", "Low")
|
|
98
|
+
wiz_id = node.get("id", "unknown")
|
|
99
|
+
|
|
100
|
+
if self.should_process_finding_by_severity(wiz_severity):
|
|
101
|
+
filtered_nodes.append(node)
|
|
102
|
+
else:
|
|
103
|
+
filtered_out_count += 1
|
|
104
|
+
logger.debug(
|
|
105
|
+
f"FILTERED BY SEVERITY: Issue {wiz_id} with severity '{wiz_severity}' filtered due to minimumSeverity configuration"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
logger.debug(f"After severity filtering: {len(filtered_nodes)} issues kept, {filtered_out_count} filtered out")
|
|
109
|
+
|
|
110
|
+
if not filtered_nodes:
|
|
111
|
+
logger.warning("All findings filtered out by severity configuration - check your minimumSeverity setting")
|
|
112
|
+
|
|
113
|
+
return filtered_nodes
|
|
114
|
+
|
|
115
|
+
def _log_consolidation_analysis(self, grouped_issues: Dict[str, List[Dict[str, Any]]]) -> None:
|
|
116
|
+
"""
|
|
117
|
+
Log detailed consolidation analysis statistics.
|
|
118
|
+
|
|
119
|
+
:param Dict[str, List[Dict[str, Any]]] grouped_issues: Issues grouped for consolidation
|
|
120
|
+
"""
|
|
121
|
+
total_groups = len(grouped_issues)
|
|
122
|
+
consolidated_groups = sum(1 for group in grouped_issues.values() if len(group) > 1)
|
|
123
|
+
total_consolidated_issues = sum(len(group) for group in grouped_issues.values() if len(group) > 1)
|
|
124
|
+
single_issue_groups = total_groups - consolidated_groups
|
|
125
|
+
|
|
126
|
+
logger.debug("CONSOLIDATION ANALYSIS:")
|
|
127
|
+
logger.debug(f" • Total groups: {total_groups}")
|
|
128
|
+
logger.debug(f" • Groups with multiple issues (consolidated): {consolidated_groups}")
|
|
129
|
+
logger.debug(f" • Total issues being consolidated: {total_consolidated_issues}")
|
|
130
|
+
logger.debug(f" • Single-issue groups: {single_issue_groups}")
|
|
131
|
+
logger.debug(f" • Expected RegScale issues to create: {total_groups}")
|
|
132
|
+
|
|
133
|
+
def _generate_findings_from_groups(
|
|
134
|
+
self,
|
|
135
|
+
grouped_issues: Dict[str, List[Dict[str, Any]]],
|
|
136
|
+
vulnerability_type: WizVulnerabilityType,
|
|
137
|
+
total_raw_issues: int,
|
|
138
|
+
) -> Iterator[IntegrationFinding]:
|
|
139
|
+
"""
|
|
140
|
+
Generate IntegrationFindings from grouped issues, handling both consolidation and single issues.
|
|
141
|
+
|
|
142
|
+
:param Dict[str, List[Dict[str, Any]]] grouped_issues: Issues grouped for processing
|
|
143
|
+
:param WizVulnerabilityType vulnerability_type: The vulnerability type
|
|
144
|
+
:param int total_raw_issues: Total number of raw issues for logging
|
|
145
|
+
:return: Generator of IntegrationFindings
|
|
146
|
+
:rtype: Iterator[IntegrationFinding]
|
|
147
|
+
"""
|
|
148
|
+
findings_generated = 0
|
|
149
|
+
|
|
150
|
+
for group_key, group_issues in grouped_issues.items():
|
|
151
|
+
if len(group_issues) > 1:
|
|
152
|
+
finding = self._process_consolidated_group(group_key, group_issues, vulnerability_type)
|
|
153
|
+
else:
|
|
154
|
+
finding = self._process_single_issue_group(group_issues[0], vulnerability_type)
|
|
155
|
+
|
|
49
156
|
if finding:
|
|
157
|
+
findings_generated += 1
|
|
50
158
|
yield finding
|
|
51
159
|
|
|
160
|
+
logger.info(f"Generated {findings_generated} RegScale findings from {total_raw_issues} raw Wiz issues")
|
|
161
|
+
|
|
162
|
+
def _process_consolidated_group(
|
|
163
|
+
self, group_key: str, group_issues: List[Dict[str, Any]], vulnerability_type: WizVulnerabilityType
|
|
164
|
+
) -> Optional[IntegrationFinding]:
|
|
165
|
+
"""
|
|
166
|
+
Process a group with multiple issues that need consolidation.
|
|
167
|
+
|
|
168
|
+
:param str group_key: The consolidation group key
|
|
169
|
+
:param List[Dict[str, Any]] group_issues: List of issues to consolidate
|
|
170
|
+
:param WizVulnerabilityType vulnerability_type: The vulnerability type
|
|
171
|
+
:return: Consolidated finding or None if failed
|
|
172
|
+
:rtype: Optional[IntegrationFinding]
|
|
173
|
+
"""
|
|
174
|
+
issue_ids = [issue.get("id", "unknown") for issue in group_issues]
|
|
175
|
+
logger.debug(f"CONSOLIDATING: Group '{group_key}' - merging {len(group_issues)} issues: {issue_ids}")
|
|
176
|
+
|
|
177
|
+
finding = self._create_consolidated_finding(group_issues, vulnerability_type)
|
|
178
|
+
if not finding:
|
|
179
|
+
logger.warning(f"Failed to create consolidated finding for group '{group_key}'")
|
|
180
|
+
|
|
181
|
+
return finding
|
|
182
|
+
|
|
183
|
+
def _process_single_issue_group(
|
|
184
|
+
self, issue: Dict[str, Any], vulnerability_type: WizVulnerabilityType
|
|
185
|
+
) -> Optional[IntegrationFinding]:
|
|
186
|
+
"""
|
|
187
|
+
Process a single issue that doesn't require consolidation.
|
|
188
|
+
|
|
189
|
+
:param Dict[str, Any] issue: The single issue to process
|
|
190
|
+
:param WizVulnerabilityType vulnerability_type: The vulnerability type
|
|
191
|
+
:return: Single issue finding or None if failed
|
|
192
|
+
:rtype: Optional[IntegrationFinding]
|
|
193
|
+
"""
|
|
194
|
+
wiz_id = issue.get("id", "unknown")
|
|
195
|
+
logger.debug(f"📝 SINGLE ISSUE: Processing issue {wiz_id} individually")
|
|
196
|
+
|
|
197
|
+
finding = self.parse_finding(issue, vulnerability_type)
|
|
198
|
+
if not finding:
|
|
199
|
+
logger.warning(f"Failed to create finding for single issue {wiz_id}")
|
|
200
|
+
|
|
201
|
+
return finding
|
|
202
|
+
|
|
203
|
+
def _group_issues_for_consolidation(self, nodes: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
|
|
204
|
+
"""
|
|
205
|
+
Group issues by source rule name (title) for consolidation.
|
|
206
|
+
This consolidates all issues with the same rule/title regardless of the affected asset.
|
|
207
|
+
|
|
208
|
+
:param List[Dict[str, Any]] nodes: List of Wiz issues
|
|
209
|
+
:return: Dictionary mapping rule names to lists of issues
|
|
210
|
+
:rtype: Dict[str, List[Dict[str, Any]]]
|
|
211
|
+
"""
|
|
212
|
+
groups: Dict[str, List[Dict[str, Any]]] = {}
|
|
213
|
+
|
|
214
|
+
for issue in nodes:
|
|
215
|
+
# Group by source rule name only (which becomes the title)
|
|
216
|
+
# This ensures all issues with identical titles are consolidated together
|
|
217
|
+
source_rule = issue.get("sourceRule", {})
|
|
218
|
+
rule_name = source_rule.get("name", "")
|
|
219
|
+
|
|
220
|
+
# Use rule name as the grouping key
|
|
221
|
+
# If no rule name, fall back to issue name to avoid empty keys
|
|
222
|
+
group_key = rule_name or issue.get("name", "") or f"unknown-{issue.get('id', 'no-id')}"
|
|
223
|
+
|
|
224
|
+
if group_key not in groups:
|
|
225
|
+
groups[group_key] = []
|
|
226
|
+
groups[group_key].append(issue)
|
|
227
|
+
|
|
228
|
+
# Log the grouping for analysis
|
|
229
|
+
logger.debug(f"Grouping issue {issue.get('id')} under key '{group_key}'")
|
|
230
|
+
|
|
231
|
+
return groups
|
|
232
|
+
|
|
233
|
+
def _determine_grouping_scope(self, provider_id: str, rule_name: str) -> str:
|
|
234
|
+
"""
|
|
235
|
+
DEPRECATED: This method is no longer used as consolidation is now done by title only.
|
|
236
|
+
Kept for backward compatibility but will be removed in future versions.
|
|
237
|
+
|
|
238
|
+
:param str provider_id: The Azure provider ID
|
|
239
|
+
:param str rule_name: The source rule name
|
|
240
|
+
:return: The grouping scope (always returns provider_id)
|
|
241
|
+
:rtype: str
|
|
242
|
+
"""
|
|
243
|
+
# This method is deprecated - consolidation now happens by title only
|
|
244
|
+
logger.debug("_determine_grouping_scope is deprecated and will be removed in a future version")
|
|
245
|
+
return provider_id
|
|
246
|
+
|
|
247
|
+
def _create_consolidated_finding(
|
|
248
|
+
self, issues: List[Dict[str, Any]], vulnerability_type: WizVulnerabilityType
|
|
249
|
+
) -> IntegrationFinding:
|
|
250
|
+
"""
|
|
251
|
+
Create a consolidated finding from multiple issues with the same rule.
|
|
252
|
+
Implements priority rules for severity, status, due date, and asset consolidation.
|
|
253
|
+
|
|
254
|
+
:param List[Dict[str, Any]] issues: List of issues to consolidate
|
|
255
|
+
:param WizVulnerabilityType vulnerability_type: The vulnerability type
|
|
256
|
+
:return: Consolidated IntegrationFinding
|
|
257
|
+
:rtype: IntegrationFinding
|
|
258
|
+
"""
|
|
259
|
+
# Determine consolidation priorities
|
|
260
|
+
highest_severity = self._determine_highest_severity(issues)
|
|
261
|
+
most_urgent_status = self._determine_most_urgent_status(issues)
|
|
262
|
+
earliest_created = self._find_earliest_creation_date(issues)
|
|
263
|
+
base_issue = self._select_base_issue(issues, highest_severity)
|
|
264
|
+
|
|
265
|
+
# Consolidate asset information
|
|
266
|
+
primary_asset_id, consolidated_provider_ids = self._consolidate_all_assets(issues)
|
|
267
|
+
|
|
268
|
+
# Log consolidation details
|
|
269
|
+
self._log_consolidation_details(
|
|
270
|
+
issues, base_issue, highest_severity, most_urgent_status, primary_asset_id, consolidated_provider_ids
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Create and return the consolidated finding
|
|
274
|
+
return self._build_integration_finding(
|
|
275
|
+
base_issue,
|
|
276
|
+
vulnerability_type,
|
|
277
|
+
highest_severity,
|
|
278
|
+
most_urgent_status,
|
|
279
|
+
earliest_created,
|
|
280
|
+
primary_asset_id,
|
|
281
|
+
consolidated_provider_ids,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
def _determine_highest_severity(self, issues: List[Dict[str, Any]]) -> str:
|
|
285
|
+
"""
|
|
286
|
+
Determine the highest priority severity from a list of issues.
|
|
287
|
+
|
|
288
|
+
:param List[Dict[str, Any]] issues: List of issues to analyze
|
|
289
|
+
:return: The highest severity level
|
|
290
|
+
:rtype: str
|
|
291
|
+
"""
|
|
292
|
+
severity_priority = {"CRITICAL": 4, "HIGH": 3, "MEDIUM": 2, "LOW": 1, "INFORMATIONAL": 0}
|
|
293
|
+
highest_severity = "LOW"
|
|
294
|
+
highest_priority = 0
|
|
295
|
+
|
|
296
|
+
for issue in issues:
|
|
297
|
+
issue_severity = issue.get("severity", "LOW").upper()
|
|
298
|
+
priority = severity_priority.get(issue_severity, 0)
|
|
299
|
+
if priority > highest_priority:
|
|
300
|
+
highest_priority = priority
|
|
301
|
+
highest_severity = issue_severity
|
|
302
|
+
|
|
303
|
+
return highest_severity
|
|
304
|
+
|
|
305
|
+
def _determine_most_urgent_status(self, issues: List[Dict[str, Any]]) -> str:
|
|
306
|
+
"""
|
|
307
|
+
Determine the most urgent status from a list of issues.
|
|
308
|
+
Any open issue means the consolidated issue should be open.
|
|
309
|
+
|
|
310
|
+
:param List[Dict[str, Any]] issues: List of issues to analyze
|
|
311
|
+
:return: The most urgent status
|
|
312
|
+
:rtype: str
|
|
313
|
+
"""
|
|
314
|
+
for issue in issues:
|
|
315
|
+
issue_status = issue.get("status", "OPEN").upper()
|
|
316
|
+
if issue_status in ["OPEN", "IN_PROGRESS", "ACKNOWLEDGE"]:
|
|
317
|
+
return "OPEN"
|
|
318
|
+
return "RESOLVED"
|
|
319
|
+
|
|
320
|
+
def _find_earliest_creation_date(self, issues: List[Dict[str, Any]]) -> Optional[str]:
|
|
321
|
+
"""
|
|
322
|
+
Find the earliest creation date from a list of issues.
|
|
323
|
+
|
|
324
|
+
:param List[Dict[str, Any]] issues: List of issues to analyze
|
|
325
|
+
:return: The earliest creation date or None
|
|
326
|
+
:rtype: Optional[str]
|
|
327
|
+
"""
|
|
328
|
+
earliest_created = None
|
|
329
|
+
for issue in issues:
|
|
330
|
+
created = safe_datetime_str(issue.get("createdAt"))
|
|
331
|
+
if created and (not earliest_created or created < earliest_created):
|
|
332
|
+
earliest_created = created
|
|
333
|
+
return earliest_created
|
|
334
|
+
|
|
335
|
+
def _select_base_issue(self, issues: List[Dict[str, Any]], highest_severity: str) -> Dict[str, Any]:
|
|
336
|
+
"""
|
|
337
|
+
Select the base issue for consolidation based on highest severity.
|
|
338
|
+
|
|
339
|
+
:param List[Dict[str, Any]] issues: List of issues to choose from
|
|
340
|
+
:param str highest_severity: The highest severity level identified
|
|
341
|
+
:return: The selected base issue
|
|
342
|
+
:rtype: Dict[str, Any]
|
|
343
|
+
"""
|
|
344
|
+
for issue in issues:
|
|
345
|
+
if issue.get("severity", "LOW").upper() == highest_severity:
|
|
346
|
+
return issue
|
|
347
|
+
return issues[0] # Fallback to first issue
|
|
348
|
+
|
|
349
|
+
def _consolidate_all_assets(self, issues: List[Dict[str, Any]]) -> tuple[Optional[str], Optional[str]]:
|
|
350
|
+
"""
|
|
351
|
+
Consolidate all asset identifiers and provider IDs from multiple issues.
|
|
352
|
+
|
|
353
|
+
:param List[Dict[str, Any]] issues: List of issues to consolidate assets from
|
|
354
|
+
:return: Tuple of (primary_asset_id, consolidated_provider_ids)
|
|
355
|
+
:rtype: tuple[Optional[str], Optional[str]]
|
|
356
|
+
"""
|
|
357
|
+
asset_ids: List[str] = []
|
|
358
|
+
provider_ids: List[str] = []
|
|
359
|
+
seen_asset_ids: set[str] = set()
|
|
360
|
+
seen_provider_ids: set[str] = set()
|
|
361
|
+
|
|
362
|
+
for issue in issues:
|
|
363
|
+
self._collect_assets_from_issue(issue, asset_ids, provider_ids, seen_asset_ids, seen_provider_ids)
|
|
364
|
+
|
|
365
|
+
primary_asset_id = asset_ids[0] if asset_ids else None
|
|
366
|
+
consolidated_provider_ids = "\n".join(provider_ids) if provider_ids else None
|
|
367
|
+
|
|
368
|
+
return primary_asset_id, consolidated_provider_ids
|
|
369
|
+
|
|
370
|
+
def _collect_assets_from_issue(
|
|
371
|
+
self,
|
|
372
|
+
issue: Dict[str, Any],
|
|
373
|
+
asset_ids: List[str],
|
|
374
|
+
provider_ids: List[str],
|
|
375
|
+
seen_asset_ids: set,
|
|
376
|
+
seen_provider_ids: set,
|
|
377
|
+
) -> None:
|
|
378
|
+
"""
|
|
379
|
+
Collect asset IDs and provider IDs from a single issue.
|
|
380
|
+
|
|
381
|
+
:param Dict[str, Any] issue: The issue to extract assets from
|
|
382
|
+
:param List[str] asset_ids: List to append asset IDs to
|
|
383
|
+
:param List[str] provider_ids: List to append provider IDs to
|
|
384
|
+
:param set seen_asset_ids: Set to track seen asset IDs
|
|
385
|
+
:param set seen_provider_ids: Set to track seen provider IDs
|
|
386
|
+
"""
|
|
387
|
+
self._collect_from_entity_snapshot(issue, asset_ids, provider_ids, seen_asset_ids, seen_provider_ids)
|
|
388
|
+
self._collect_from_related_entities(issue, asset_ids, provider_ids, seen_asset_ids, seen_provider_ids)
|
|
389
|
+
|
|
390
|
+
def _collect_from_entity_snapshot(
|
|
391
|
+
self,
|
|
392
|
+
issue: Dict[str, Any],
|
|
393
|
+
asset_ids: List[str],
|
|
394
|
+
provider_ids: List[str],
|
|
395
|
+
seen_asset_ids: set,
|
|
396
|
+
seen_provider_ids: set,
|
|
397
|
+
) -> None:
|
|
398
|
+
"""
|
|
399
|
+
Collect asset IDs and provider IDs from entitySnapshot.
|
|
400
|
+
|
|
401
|
+
:param Dict[str, Any] issue: The issue to extract assets from
|
|
402
|
+
:param List[str] asset_ids: List to append asset IDs to
|
|
403
|
+
:param List[str] provider_ids: List to append provider IDs to
|
|
404
|
+
:param set seen_asset_ids: Set to track seen asset IDs
|
|
405
|
+
:param set seen_provider_ids: Set to track seen provider IDs
|
|
406
|
+
"""
|
|
407
|
+
entity_snapshot = issue.get("entitySnapshot", {})
|
|
408
|
+
self._add_entity_ids(entity_snapshot, asset_ids, provider_ids, seen_asset_ids, seen_provider_ids)
|
|
409
|
+
|
|
410
|
+
def _collect_from_related_entities(
|
|
411
|
+
self,
|
|
412
|
+
issue: Dict[str, Any],
|
|
413
|
+
asset_ids: List[str],
|
|
414
|
+
provider_ids: List[str],
|
|
415
|
+
seen_asset_ids: set,
|
|
416
|
+
seen_provider_ids: set,
|
|
417
|
+
) -> None:
|
|
418
|
+
"""
|
|
419
|
+
Collect asset IDs and provider IDs from related entities.
|
|
420
|
+
|
|
421
|
+
:param Dict[str, Any] issue: The issue to extract assets from
|
|
422
|
+
:param List[str] asset_ids: List to append asset IDs to
|
|
423
|
+
:param List[str] provider_ids: List to append provider IDs to
|
|
424
|
+
:param set seen_asset_ids: Set to track seen asset IDs
|
|
425
|
+
:param set seen_provider_ids: Set to track seen provider IDs
|
|
426
|
+
"""
|
|
427
|
+
for entity in issue.get("relatedEntities", []):
|
|
428
|
+
if entity and isinstance(entity, dict):
|
|
429
|
+
self._add_entity_ids(entity, asset_ids, provider_ids, seen_asset_ids, seen_provider_ids)
|
|
430
|
+
|
|
431
|
+
def _add_entity_ids(
|
|
432
|
+
self,
|
|
433
|
+
entity: Dict[str, Any],
|
|
434
|
+
asset_ids: List[str],
|
|
435
|
+
provider_ids: List[str],
|
|
436
|
+
seen_asset_ids: set,
|
|
437
|
+
seen_provider_ids: set,
|
|
438
|
+
) -> None:
|
|
439
|
+
"""
|
|
440
|
+
Add entity ID and provider ID from a single entity if not already seen.
|
|
441
|
+
|
|
442
|
+
:param Dict[str, Any] entity: The entity to extract IDs from
|
|
443
|
+
:param List[str] asset_ids: List to append asset IDs to
|
|
444
|
+
:param List[str] provider_ids: List to append provider IDs to
|
|
445
|
+
:param set seen_asset_ids: Set to track seen asset IDs
|
|
446
|
+
:param set seen_provider_ids: Set to track seen provider IDs
|
|
447
|
+
"""
|
|
448
|
+
self._add_unique_id(entity.get("id"), asset_ids, seen_asset_ids)
|
|
449
|
+
self._add_unique_id(entity.get("providerId"), provider_ids, seen_provider_ids)
|
|
450
|
+
|
|
451
|
+
def _add_unique_id(self, id_value: Optional[str], id_list: List[str], seen_ids: set) -> None:
|
|
452
|
+
"""
|
|
453
|
+
Add an ID to the list if it exists and hasn't been seen before.
|
|
454
|
+
|
|
455
|
+
:param Optional[str] id_value: The ID value to add
|
|
456
|
+
:param List[str] id_list: List to append the ID to
|
|
457
|
+
:param set seen_ids: Set to track seen IDs
|
|
458
|
+
"""
|
|
459
|
+
if id_value and id_value not in seen_ids:
|
|
460
|
+
id_list.append(id_value)
|
|
461
|
+
seen_ids.add(id_value)
|
|
462
|
+
|
|
463
|
+
def _log_consolidation_details(
|
|
464
|
+
self,
|
|
465
|
+
issues: List[Dict[str, Any]],
|
|
466
|
+
base_issue: Dict[str, Any],
|
|
467
|
+
highest_severity: str,
|
|
468
|
+
most_urgent_status: str,
|
|
469
|
+
primary_asset_id: Optional[str],
|
|
470
|
+
consolidated_provider_ids: Optional[str],
|
|
471
|
+
) -> None:
|
|
472
|
+
"""
|
|
473
|
+
Log detailed information about the consolidation process.
|
|
474
|
+
|
|
475
|
+
:param List[Dict[str, Any]] issues: List of issues being consolidated
|
|
476
|
+
:param Dict[str, Any] base_issue: The base issue selected for consolidation
|
|
477
|
+
:param str highest_severity: The highest severity determined
|
|
478
|
+
:param str most_urgent_status: The most urgent status determined
|
|
479
|
+
:param Optional[str] primary_asset_id: The primary asset ID
|
|
480
|
+
:param Optional[str] consolidated_provider_ids: The consolidated provider IDs
|
|
481
|
+
"""
|
|
482
|
+
rule_name = base_issue.get("sourceRule", {}).get("name", "Unknown")
|
|
483
|
+
severity_count = len([i for i in issues if i.get("severity", "").upper() == highest_severity])
|
|
484
|
+
asset_count = len(primary_asset_id.split("\n")) if primary_asset_id else 0
|
|
485
|
+
provider_count = len(consolidated_provider_ids.split("\n")) if consolidated_provider_ids else 0
|
|
486
|
+
|
|
487
|
+
logger.debug(f"CONSOLIDATION DETAILS for '{rule_name}':")
|
|
488
|
+
logger.debug(f" • Consolidating {len(issues)} issues into 1")
|
|
489
|
+
logger.debug(f" • Highest severity: {highest_severity} (from {severity_count} issues)")
|
|
490
|
+
logger.debug(f" • Most urgent status: {most_urgent_status}")
|
|
491
|
+
logger.debug(f" • Total unique assets: {asset_count}")
|
|
492
|
+
logger.debug(f" • Total unique provider IDs: {provider_count}")
|
|
493
|
+
|
|
494
|
+
def _build_integration_finding(
|
|
495
|
+
self,
|
|
496
|
+
base_issue: Dict[str, Any],
|
|
497
|
+
vulnerability_type: WizVulnerabilityType,
|
|
498
|
+
highest_severity: str,
|
|
499
|
+
most_urgent_status: str,
|
|
500
|
+
earliest_created: Optional[str],
|
|
501
|
+
primary_asset_id: Optional[str],
|
|
502
|
+
consolidated_provider_ids: Optional[str],
|
|
503
|
+
) -> IntegrationFinding:
|
|
504
|
+
"""
|
|
505
|
+
Build the final IntegrationFinding object from consolidated data.
|
|
506
|
+
|
|
507
|
+
:param Dict[str, Any] base_issue: The base issue to use for field values
|
|
508
|
+
:param WizVulnerabilityType vulnerability_type: The vulnerability type
|
|
509
|
+
:param str highest_severity: The highest severity determined
|
|
510
|
+
:param str most_urgent_status: The most urgent status determined
|
|
511
|
+
:param Optional[str] earliest_created: The earliest creation date
|
|
512
|
+
:param Optional[str] primary_asset_id: The primary asset identifier
|
|
513
|
+
:param Optional[str] consolidated_provider_ids: The consolidated provider IDs
|
|
514
|
+
:return: The built IntegrationFinding
|
|
515
|
+
:rtype: IntegrationFinding
|
|
516
|
+
"""
|
|
517
|
+
wiz_id = base_issue.get("id", "N/A")
|
|
518
|
+
severity = self.get_issue_severity(highest_severity)
|
|
519
|
+
status = self.map_status_to_issue_status(most_urgent_status)
|
|
520
|
+
date_created = earliest_created or safe_datetime_str(base_issue.get("createdAt"))
|
|
521
|
+
name = base_issue.get("name", "")
|
|
522
|
+
|
|
523
|
+
# Handle source rule (Control) specific fields
|
|
524
|
+
source_rule = base_issue.get("sourceRule", {})
|
|
525
|
+
control_name = source_rule.get("name", "")
|
|
526
|
+
control_labels = self._parse_security_subcategories(source_rule)
|
|
527
|
+
description = (
|
|
528
|
+
self._format_control_description(source_rule) if source_rule else base_issue.get("description", "")
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
# Handle CVE if present
|
|
532
|
+
cve = (
|
|
533
|
+
name
|
|
534
|
+
if name and (name.startswith("CVE") or name.startswith("GHSA")) and not base_issue.get("cve")
|
|
535
|
+
else base_issue.get("cve")
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
# Get plugin name and source rule ID
|
|
539
|
+
plugin_name = self._get_plugin_name(base_issue)
|
|
540
|
+
source_rule_id = self._get_source_rule_id(source_rule)
|
|
541
|
+
security_check = f"Wiz {plugin_name}"
|
|
542
|
+
|
|
543
|
+
return IntegrationFinding(
|
|
544
|
+
control_labels=control_labels,
|
|
545
|
+
category="Wiz Control" if source_rule else "Wiz Vulnerability",
|
|
546
|
+
title=control_name or base_issue.get("name") or f"unknown - {wiz_id}",
|
|
547
|
+
security_check=security_check,
|
|
548
|
+
description=description,
|
|
549
|
+
severity=severity,
|
|
550
|
+
status=status,
|
|
551
|
+
asset_identifier=primary_asset_id or f"wiz-issue-{wiz_id}",
|
|
552
|
+
issue_asset_identifier_value=consolidated_provider_ids,
|
|
553
|
+
external_id=wiz_id,
|
|
554
|
+
first_seen=date_created,
|
|
555
|
+
last_seen=safe_datetime_str(base_issue.get("lastDetectedAt")),
|
|
556
|
+
remediation=source_rule.get("resolutionRecommendation")
|
|
557
|
+
or f"Update to version {base_issue.get('fixedVersion')} or higher",
|
|
558
|
+
cve=cve,
|
|
559
|
+
plugin_name=plugin_name,
|
|
560
|
+
source_rule_id=source_rule_id,
|
|
561
|
+
vulnerability_type=vulnerability_type.value,
|
|
562
|
+
date_created=date_created,
|
|
563
|
+
due_date=Issue.get_due_date(severity, self.app.config, "wiz", date_created),
|
|
564
|
+
recommendation_for_mitigation=source_rule.get("resolutionRecommendation")
|
|
565
|
+
or base_issue.get("description", ""),
|
|
566
|
+
poam_comments=None,
|
|
567
|
+
basis_for_adjustment=None,
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
def _get_consolidated_asset_identifiers(self, wiz_issue: Dict[str, Any]) -> tuple[str, Optional[str]]:
|
|
571
|
+
"""
|
|
572
|
+
Get consolidated asset identifiers for an issue.
|
|
573
|
+
For multiple assets, returns the primary asset ID and all provider IDs as newline-separated string.
|
|
574
|
+
|
|
575
|
+
:param Dict[str, Any] wiz_issue: The Wiz issue
|
|
576
|
+
:return: Tuple of (primary_asset_id, consolidated_provider_ids)
|
|
577
|
+
:rtype: tuple[str, Optional[str]]
|
|
578
|
+
"""
|
|
579
|
+
assets = self._collect_all_assets(wiz_issue)
|
|
580
|
+
|
|
581
|
+
if not assets:
|
|
582
|
+
return None, None
|
|
583
|
+
|
|
584
|
+
primary_asset_id = assets[0][0]
|
|
585
|
+
consolidated_provider_ids = self._consolidate_provider_ids(assets)
|
|
586
|
+
|
|
587
|
+
return primary_asset_id, consolidated_provider_ids
|
|
588
|
+
|
|
589
|
+
def _collect_all_assets(self, wiz_issue: Dict[str, Any]) -> List[tuple[str, Optional[str]]]:
|
|
590
|
+
"""Collect all assets from entitySnapshot and relatedEntities."""
|
|
591
|
+
assets = []
|
|
592
|
+
|
|
593
|
+
# Check entitySnapshot first (primary asset)
|
|
594
|
+
assets.extend(self._extract_entity_snapshot_assets(wiz_issue))
|
|
595
|
+
|
|
596
|
+
# Check related entities (additional assets)
|
|
597
|
+
assets.extend(self._extract_related_entity_assets(wiz_issue))
|
|
598
|
+
|
|
599
|
+
# If no assets found yet, try the standard single-asset approach
|
|
600
|
+
if not assets:
|
|
601
|
+
asset_id, provider_id = self._get_asset_identifiers(wiz_issue)
|
|
602
|
+
if asset_id:
|
|
603
|
+
assets.append((asset_id, provider_id))
|
|
604
|
+
|
|
605
|
+
return assets
|
|
606
|
+
|
|
607
|
+
def _extract_entity_snapshot_assets(self, wiz_issue: Dict[str, Any]) -> List[tuple[str, Optional[str]]]:
|
|
608
|
+
"""Extract assets from entitySnapshot."""
|
|
609
|
+
assets = []
|
|
610
|
+
if entity_snapshot := wiz_issue.get("entitySnapshot"):
|
|
611
|
+
if entity_id := entity_snapshot.get("id"):
|
|
612
|
+
provider_id = self._get_provider_id_from_entity(entity_snapshot)
|
|
613
|
+
assets.append((entity_id, provider_id))
|
|
614
|
+
return assets
|
|
615
|
+
|
|
616
|
+
def _extract_related_entity_assets(self, wiz_issue: Dict[str, Any]) -> List[tuple[str, Optional[str]]]:
|
|
617
|
+
"""Extract assets from relatedEntities."""
|
|
618
|
+
assets = []
|
|
619
|
+
entities = wiz_issue.get("relatedEntities", [])
|
|
620
|
+
if entities and isinstance(entities, list):
|
|
621
|
+
for entity in entities:
|
|
622
|
+
if entity and isinstance(entity, dict) and (entity_id := entity.get("id")):
|
|
623
|
+
provider_id = self._get_provider_id_from_entity(entity)
|
|
624
|
+
assets.append((entity_id, provider_id))
|
|
625
|
+
return assets
|
|
626
|
+
|
|
627
|
+
def _consolidate_provider_ids(self, assets: List[tuple[str, Optional[str]]]) -> Optional[str]:
|
|
628
|
+
"""Consolidate provider IDs from assets into newline-separated string."""
|
|
629
|
+
provider_ids = [provider_id for _, provider_id in assets if provider_id]
|
|
630
|
+
return "\n".join(provider_ids) if provider_ids else None
|
|
631
|
+
|
|
52
632
|
def _parse_security_subcategories(self, source_rule: Dict[str, Any]) -> List[str]:
|
|
53
633
|
"""
|
|
54
634
|
Parse security subcategories from a source rule.
|
|
@@ -111,20 +691,58 @@ class WizIssue(WizVulnerabilityIntegration):
|
|
|
111
691
|
:return: The asset identifier
|
|
112
692
|
:rtype: str
|
|
113
693
|
"""
|
|
114
|
-
|
|
694
|
+
return (
|
|
695
|
+
WizIssue._get_id_from_entity_snapshot(wiz_issue)
|
|
696
|
+
or WizIssue._get_id_from_related_entities(wiz_issue)
|
|
697
|
+
or WizIssue._get_id_from_asset_paths(wiz_issue)
|
|
698
|
+
or WizIssue._get_id_from_source_rule(wiz_issue)
|
|
699
|
+
or WizIssue._get_fallback_issue_id(wiz_issue)
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
@staticmethod
|
|
703
|
+
def _get_id_from_entity_snapshot(wiz_issue: Dict[str, Any]) -> Optional[str]:
|
|
704
|
+
"""
|
|
705
|
+
Get asset ID from entitySnapshot.
|
|
706
|
+
|
|
707
|
+
:param Dict[str, Any] wiz_issue: The Wiz issue
|
|
708
|
+
:return: Asset ID if found, None otherwise
|
|
709
|
+
:rtype: Optional[str]
|
|
710
|
+
"""
|
|
115
711
|
if entity_snapshot := wiz_issue.get("entitySnapshot"):
|
|
116
|
-
|
|
117
|
-
|
|
712
|
+
return entity_snapshot.get("id")
|
|
713
|
+
return None
|
|
118
714
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
715
|
+
@staticmethod
|
|
716
|
+
def _get_id_from_related_entities(wiz_issue: Dict[str, Any]) -> Optional[str]:
|
|
717
|
+
"""
|
|
718
|
+
Get asset ID from related entities.
|
|
719
|
+
|
|
720
|
+
:param Dict[str, Any] wiz_issue: The Wiz issue
|
|
721
|
+
:return: Asset ID if found, None otherwise
|
|
722
|
+
:rtype: Optional[str]
|
|
723
|
+
"""
|
|
724
|
+
if "relatedEntities" not in wiz_issue:
|
|
725
|
+
return None
|
|
726
|
+
|
|
727
|
+
entities = wiz_issue.get("relatedEntities", [])
|
|
728
|
+
if not (entities and isinstance(entities, list)):
|
|
729
|
+
return None
|
|
730
|
+
|
|
731
|
+
for entity in entities:
|
|
732
|
+
if entity and isinstance(entity, dict):
|
|
733
|
+
if entity_id := entity.get("id"):
|
|
734
|
+
return entity_id
|
|
735
|
+
return None
|
|
736
|
+
|
|
737
|
+
@staticmethod
|
|
738
|
+
def _get_id_from_asset_paths(wiz_issue: Dict[str, Any]) -> Optional[str]:
|
|
739
|
+
"""
|
|
740
|
+
Get asset ID from common asset ID paths.
|
|
126
741
|
|
|
127
|
-
|
|
742
|
+
:param Dict[str, Any] wiz_issue: The Wiz issue
|
|
743
|
+
:return: Asset ID if found, None otherwise
|
|
744
|
+
:rtype: Optional[str]
|
|
745
|
+
"""
|
|
128
746
|
asset_paths = [
|
|
129
747
|
"vulnerableAsset.id",
|
|
130
748
|
"entity.id",
|
|
@@ -137,30 +755,136 @@ class WizIssue(WizVulnerabilityIntegration):
|
|
|
137
755
|
for path in asset_paths:
|
|
138
756
|
if asset_id := get_value(wiz_issue, path):
|
|
139
757
|
return asset_id
|
|
758
|
+
return None
|
|
140
759
|
|
|
141
|
-
|
|
760
|
+
@staticmethod
|
|
761
|
+
def _get_id_from_source_rule(wiz_issue: Dict[str, Any]) -> Optional[str]:
|
|
762
|
+
"""
|
|
763
|
+
Get asset ID from source rule as fallback.
|
|
764
|
+
|
|
765
|
+
:param Dict[str, Any] wiz_issue: The Wiz issue
|
|
766
|
+
:return: Asset ID if found, None otherwise
|
|
767
|
+
:rtype: Optional[str]
|
|
768
|
+
"""
|
|
142
769
|
if source_rule := wiz_issue.get("sourceRule"):
|
|
143
770
|
if rule_id := source_rule.get("id"):
|
|
144
771
|
return f"wiz-rule-{rule_id}"
|
|
772
|
+
return None
|
|
145
773
|
|
|
146
|
-
|
|
774
|
+
@staticmethod
|
|
775
|
+
def _get_fallback_issue_id(wiz_issue: Dict[str, Any]) -> str:
|
|
776
|
+
"""
|
|
777
|
+
Get fallback asset ID using the issue ID.
|
|
778
|
+
|
|
779
|
+
:param Dict[str, Any] wiz_issue: The Wiz issue
|
|
780
|
+
:return: Fallback asset ID
|
|
781
|
+
:rtype: str
|
|
782
|
+
"""
|
|
147
783
|
return f"wiz-issue-{wiz_issue.get('id', 'unknown')}"
|
|
148
784
|
|
|
785
|
+
@staticmethod
|
|
786
|
+
def _get_asset_identifiers(wiz_issue: Dict[str, Any]) -> tuple[str, Optional[str]]:
|
|
787
|
+
"""
|
|
788
|
+
Get both asset_identifier and issue_asset_identifier_value consistently.
|
|
789
|
+
Ensures both values refer to the same asset entity.
|
|
790
|
+
|
|
791
|
+
:param Dict[str, Any] wiz_issue: The Wiz issue
|
|
792
|
+
:return: Tuple of (asset_identifier, issue_asset_identifier_value)
|
|
793
|
+
:rtype: tuple[str, Optional[str]]
|
|
794
|
+
"""
|
|
795
|
+
# Try each potential source in order
|
|
796
|
+
asset_id, provider_id = WizIssue._try_entity_snapshot(wiz_issue)
|
|
797
|
+
if asset_id:
|
|
798
|
+
return asset_id, provider_id
|
|
799
|
+
|
|
800
|
+
asset_id, provider_id = WizIssue._try_related_entities(wiz_issue)
|
|
801
|
+
if asset_id:
|
|
802
|
+
return asset_id, provider_id
|
|
803
|
+
|
|
804
|
+
asset_id, provider_id = WizIssue._try_common_asset_paths(wiz_issue)
|
|
805
|
+
if asset_id:
|
|
806
|
+
return asset_id, provider_id
|
|
807
|
+
|
|
808
|
+
return WizIssue._get_fallback_identifiers(wiz_issue)
|
|
809
|
+
|
|
810
|
+
@staticmethod
|
|
811
|
+
def _try_entity_snapshot(wiz_issue: Dict[str, Any]) -> tuple[Optional[str], Optional[str]]:
|
|
812
|
+
"""Try to get identifiers from entitySnapshot."""
|
|
813
|
+
if entity_snapshot := wiz_issue.get("entitySnapshot"):
|
|
814
|
+
if entity_id := entity_snapshot.get("id"):
|
|
815
|
+
provider_id = WizIssue._get_provider_id_from_entity(entity_snapshot)
|
|
816
|
+
return entity_id, provider_id
|
|
817
|
+
return None, None
|
|
818
|
+
|
|
819
|
+
@staticmethod
|
|
820
|
+
def _try_related_entities(wiz_issue: Dict[str, Any]) -> tuple[Optional[str], Optional[str]]:
|
|
821
|
+
"""Try to get identifiers from relatedEntities."""
|
|
822
|
+
if "relatedEntities" in wiz_issue:
|
|
823
|
+
entities = wiz_issue.get("relatedEntities", [])
|
|
824
|
+
if entities and isinstance(entities, list):
|
|
825
|
+
for entity in entities:
|
|
826
|
+
if entity and isinstance(entity, dict) and (entity_id := entity.get("id")):
|
|
827
|
+
provider_id = WizIssue._get_provider_id_from_entity(entity)
|
|
828
|
+
return entity_id, provider_id
|
|
829
|
+
return None, None
|
|
830
|
+
|
|
831
|
+
@staticmethod
|
|
832
|
+
def _try_common_asset_paths(wiz_issue: Dict[str, Any]) -> tuple[Optional[str], Optional[str]]:
|
|
833
|
+
"""Try to get identifiers from common asset paths."""
|
|
834
|
+
asset_paths = ["vulnerableAsset", "entity", "resource", "relatedEntity", "sourceEntity", "target"]
|
|
835
|
+
|
|
836
|
+
for path in asset_paths:
|
|
837
|
+
if asset_obj := wiz_issue.get(path):
|
|
838
|
+
if asset_id := asset_obj.get("id"):
|
|
839
|
+
provider_id = WizIssue._get_provider_id_from_entity(asset_obj)
|
|
840
|
+
return asset_id, provider_id
|
|
841
|
+
return None, None
|
|
842
|
+
|
|
843
|
+
@staticmethod
|
|
844
|
+
def _get_fallback_identifiers(wiz_issue: Dict[str, Any]) -> tuple[str, None]:
|
|
845
|
+
"""Get fallback identifiers when no asset entity is found."""
|
|
846
|
+
# Try source rule as fallback
|
|
847
|
+
if source_rule := wiz_issue.get("sourceRule"):
|
|
848
|
+
if rule_id := source_rule.get("id"):
|
|
849
|
+
return f"wiz-rule-{rule_id}", None
|
|
850
|
+
|
|
851
|
+
# Final fallback - use the issue ID
|
|
852
|
+
issue_id = wiz_issue.get("id", "unknown")
|
|
853
|
+
return f"wiz-issue-{issue_id}", None
|
|
854
|
+
|
|
855
|
+
@staticmethod
|
|
856
|
+
def _get_provider_id_from_entity(entity: Dict[str, Any]) -> Optional[str]:
|
|
857
|
+
"""Extract provider ID from an entity object."""
|
|
858
|
+
return entity.get("providerId") or entity.get("providerUniqueId") or entity.get("name")
|
|
859
|
+
|
|
149
860
|
@staticmethod
|
|
150
861
|
def _format_control_description(control: Dict[str, Any]) -> str:
|
|
151
862
|
"""
|
|
152
863
|
Format the control description with additional context.
|
|
864
|
+
Handles different description field names for different source rule types.
|
|
153
865
|
|
|
154
866
|
:param Dict[str, Any] control: The control data
|
|
155
867
|
:return: Formatted description
|
|
156
868
|
:rtype: str
|
|
157
869
|
"""
|
|
158
870
|
formatted_desc = []
|
|
159
|
-
|
|
871
|
+
|
|
872
|
+
# Try different description field names based on source rule type
|
|
873
|
+
description = (
|
|
874
|
+
control.get("controlDescription")
|
|
875
|
+
or control.get("cloudEventRuleDescription")
|
|
876
|
+
or control.get("cloudConfigurationRuleDescription")
|
|
877
|
+
or control.get("description", "")
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
if description:
|
|
160
881
|
formatted_desc.append("Description:")
|
|
161
882
|
formatted_desc.append(description)
|
|
162
883
|
|
|
163
|
-
|
|
884
|
+
# Try different recommendation field names
|
|
885
|
+
recommendation = control.get("resolutionRecommendation") or control.get("remediationInstructions", "")
|
|
886
|
+
|
|
887
|
+
if recommendation:
|
|
164
888
|
if formatted_desc:
|
|
165
889
|
formatted_desc.append("\n")
|
|
166
890
|
formatted_desc.append("Resolution Recommendation:")
|
|
@@ -206,8 +930,8 @@ class WizIssue(WizVulnerabilityIntegration):
|
|
|
206
930
|
if not name:
|
|
207
931
|
return f"Wiz-{service_type}-Config"
|
|
208
932
|
|
|
209
|
-
#
|
|
210
|
-
service_match = re.match(r"^([A-Za-z\s]
|
|
933
|
+
# Safe regex pattern that looks for service name at start
|
|
934
|
+
service_match = re.match(r"^([A-Za-z\s]{1,50}?)\s+(?:public|private|should|must|needs|to)", name)
|
|
211
935
|
if not service_match:
|
|
212
936
|
return f"Wiz-{service_type}-Config"
|
|
213
937
|
|
|
@@ -241,7 +965,9 @@ class WizIssue(WizVulnerabilityIntegration):
|
|
|
241
965
|
|
|
242
966
|
# Fallback to control name prefix
|
|
243
967
|
if name:
|
|
244
|
-
prefix_match = re.match(
|
|
968
|
+
prefix_match = re.match(
|
|
969
|
+
r"^([A-Za-z\s]{1,50}?)\s+(?:exposed|misconfigured|vulnerable|security|access)", name
|
|
970
|
+
)
|
|
245
971
|
if prefix_match:
|
|
246
972
|
prefix = "".join(word.capitalize() for word in prefix_match.group(1).strip().split())
|
|
247
973
|
return f"Wiz-Control-{prefix}"
|
|
@@ -262,7 +988,7 @@ class WizIssue(WizVulnerabilityIntegration):
|
|
|
262
988
|
return "Wiz-Event"
|
|
263
989
|
if not name:
|
|
264
990
|
return f"Wiz-{service_type}-Event"
|
|
265
|
-
event_match = re.match(r"^([A-Za-z
|
|
991
|
+
event_match = re.match(r"^([A-Za-z]+(?: [A-Za-z]+)*)\s+(detection|event|alert|activity)", name)
|
|
266
992
|
if not event_match:
|
|
267
993
|
return f"Wiz-{service_type}-Event"
|
|
268
994
|
|
|
@@ -304,7 +1030,28 @@ class WizIssue(WizVulnerabilityIntegration):
|
|
|
304
1030
|
"""
|
|
305
1031
|
wiz_id = wiz_issue.get("id", "N/A")
|
|
306
1032
|
severity = self.get_issue_severity(wiz_issue.get("severity", "Low"))
|
|
307
|
-
|
|
1033
|
+
|
|
1034
|
+
# Get status with diagnostic logging
|
|
1035
|
+
wiz_status = wiz_issue.get("status", "OPEN")
|
|
1036
|
+
logger.debug(f"Processing Wiz issue {wiz_id}: raw status from node = '{wiz_status}'")
|
|
1037
|
+
status = self.map_status_to_issue_status(wiz_status)
|
|
1038
|
+
|
|
1039
|
+
# Enhanced status mapping logging
|
|
1040
|
+
logger.debug(f"STATUS MAPPING: Wiz issue {wiz_id} - '{wiz_status}' -> {status}")
|
|
1041
|
+
|
|
1042
|
+
# Check if we're creating a closed issue (which might not appear in your count)
|
|
1043
|
+
if status == regscale_models.IssueStatus.Closed:
|
|
1044
|
+
logger.debug(
|
|
1045
|
+
f"CLOSED ISSUE: Issue {wiz_id} will be created as CLOSED (status='{wiz_status}') - this won't appear in open issue counts"
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
# Add diagnostic logging for unexpected issue closure
|
|
1049
|
+
if wiz_status.upper() not in ["RESOLVED", "REJECTED"]:
|
|
1050
|
+
logger.warning(
|
|
1051
|
+
f"Unexpected issue closure: Wiz issue status '{wiz_status}' mapped to Closed status "
|
|
1052
|
+
f"for issue {wiz_id} - '{wiz_issue.get('sourceRule', {}).get('name', 'Unknown rule')}'. "
|
|
1053
|
+
f"This may indicate a mapping configuration issue."
|
|
1054
|
+
)
|
|
308
1055
|
date_created = safe_datetime_str(wiz_issue.get("createdAt"))
|
|
309
1056
|
name: str = wiz_issue.get("name", "")
|
|
310
1057
|
|
|
@@ -315,8 +1062,8 @@ class WizIssue(WizVulnerabilityIntegration):
|
|
|
315
1062
|
# Get control labels from security subcategories
|
|
316
1063
|
control_labels = self._parse_security_subcategories(source_rule)
|
|
317
1064
|
|
|
318
|
-
# Get asset identifier
|
|
319
|
-
asset_id = self.
|
|
1065
|
+
# Get asset identifier and consolidated provider IDs
|
|
1066
|
+
asset_id, issue_asset_identifier_value = self._get_consolidated_asset_identifiers(wiz_issue)
|
|
320
1067
|
|
|
321
1068
|
# Format description with control context
|
|
322
1069
|
description = self._format_control_description(source_rule) if source_rule else wiz_issue.get("description", "")
|
|
@@ -344,6 +1091,7 @@ class WizIssue(WizVulnerabilityIntegration):
|
|
|
344
1091
|
severity=severity,
|
|
345
1092
|
status=status,
|
|
346
1093
|
asset_identifier=asset_id,
|
|
1094
|
+
issue_asset_identifier_value=issue_asset_identifier_value,
|
|
347
1095
|
external_id=wiz_id,
|
|
348
1096
|
first_seen=date_created,
|
|
349
1097
|
last_seen=safe_datetime_str(wiz_issue.get("lastDetectedAt")),
|